Files
WeReadBoard/migrate_to_mysql.py
T

172 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""
SQLite → MySQL 数据迁移脚本
将 weread.db (SQLite) 全部 31 张表数据迁移至 MySQL weread_dashboard 数据库。
处理逻辑:
1. 自动读取 SQLite 每张表的列名,与 MySQL 对齐
2. 时间戳文本列 (_dt, created_at, updated_at) 自动转换为 MySQL TIMESTAMP 格式
3. 每张表先 TRUNCATE 再插入,确保数据一致性
"""
import os
import sqlite3
import pymysql
from datetime import datetime
# ===== 配置 =====
SQLITE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db")
MYSQL_CONFIG = {
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "258963",
"database": "weread_dashboard",
"charset": "utf8mb4",
}
# 需要迁移的表名列表(按依赖顺序排列)
TABLES = [
# 书架数据
"books",
"shelf_albums",
"shelf_meta",
"archives",
# 阅读统计
"reading_stats",
"read_times",
"read_longest",
"read_stat_items",
"prefer_categories",
"prefer_authors",
"prefer_publishers",
"prefer_cps",
"prefer_books",
"prefer_time",
"read_rank",
"medals",
"year_reports",
# 笔记数据
"notebooks",
"book_highlights",
"book_chapters",
"book_reviews",
"book_best_bookmarks",
"book_underlines",
"public_reviews",
# 书籍详情
"book_details",
"book_progress",
# 发现/推荐
"recommended_books",
"similar_books",
"search_sessions",
"search_results",
# 系统
"fetch_meta",
]
def convert_dt(val):
"""将 SQLite 中的时间文本转换为 MySQL TIMESTAMP 兼容格式"""
if val is None or val == "":
return None
if isinstance(val, (int, float)):
return None
# SQLite datetime('now','localtime') 格式: "2026-07-17 10:00:00"
# 已经是 MySQL 兼容格式,直接返回
return str(val)
def migrate():
"""执行完整迁移"""
# 连接 SQLite
sqlite_conn = sqlite3.connect(SQLITE_PATH)
sqlite_conn.row_factory = sqlite3.Row
sc = sqlite_conn.cursor()
# 连接 MySQL
mysql_conn = pymysql.connect(**MYSQL_CONFIG)
mc = mysql_conn.cursor()
total_rows = 0
migrated_tables = 0
print("=" * 60)
print("SQLite → MySQL 数据迁移")
print("=" * 60)
for table in TABLES:
# 检查 SQLite 表是否有数据
sc.execute(f'SELECT COUNT(*) FROM "{table}"')
row_count = sc.fetchone()[0]
if row_count == 0:
print(f" {table:30s} 跳过 (无数据)")
continue
# 获取列名
sc.execute(f'PRAGMA table_info("{table}")')
columns = [col[1] for col in sc.fetchall()]
# 清空 MySQL 表
mc.execute(f"DELETE FROM `{table}`")
# 读取 SQLite 数据
sc.execute(f'SELECT {", ".join(f"\"{c}\"" for c in columns)} FROM "{table}"')
rows = sc.fetchall()
# 构建批量 INSERT
placeholders = ", ".join(["%s"] * len(columns))
col_list = ", ".join(f"`{c}`" for c in columns)
insert_sql = f"INSERT INTO `{table}` ({col_list}) VALUES ({placeholders})"
# 转换数据
batch = []
for row in rows:
values = []
for col in columns:
val = row[col]
values.append(val)
batch.append(values)
# 批量插入
mc.executemany(insert_sql, batch)
mysql_conn.commit()
total_rows += row_count
migrated_tables += 1
print(f" {table:30s} {row_count:>6} 行 ✓")
# 更新 fetch_meta
mc.execute(
"INSERT INTO fetch_meta (`key`, `value`) VALUES ('mysql_migrated_at', %s) "
"ON DUPLICATE KEY UPDATE `value` = %s",
(datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
)
mysql_conn.commit()
print(f"\n{'=' * 60}")
print(f"迁移完成! {migrated_tables} 张表, 共 {total_rows} 行数据")
print(f"MySQL: {MYSQL_CONFIG['host']}:{MYSQL_CONFIG['port']}/{MYSQL_CONFIG['database']}")
print(f"{'=' * 60}")
# 数据校验
print("\n数据校验:")
mc.execute("SHOW TABLES")
mysql_tables = [t[0] for t in mc.fetchall()]
for t in mysql_tables:
mc.execute(f"SELECT COUNT(*) FROM `{t}`")
cnt = mc.fetchone()[0]
if cnt > 0:
print(f" MySQL {t:30s} {cnt:>6} 行")
sqlite_conn.close()
mysql_conn.close()
if __name__ == "__main__":
migrate()