数据同步指南
选股宝脱水研报数据(内网 MySQL,只读)→ 本地 SQLite(
data/xgb_wiki.db)的同步使用说明。 实现代码:src/sync.py(编排)+src/etl.py(搬运);入口scripts/sync_data.py。 命令速查与日常操作见 运行手册 §1,本页是完整说明。
1. 概览
内网 MySQL (xuangubao, 只读) 本地
┌─────────────────────────┐ ┌────────────────────────────────┐
│ live_tuoshui_news │ │ data/xgb_wiki.db │
│ tuoshui_msgs │ ───→ │ (report/theme/… + 聚合 + 日志)│
│ plate_rank_infos │ 同步 │ data/backups/*.bak 同步前备份 │
└─────────────────────────┘ │ xgb_wiki.sync.log 文件日志 │
└────────────────────────────────┘
每次同步固定走五个阶段,任何一步失败都不会让正式库处于中间状态:
环境检查 ──失败──→ 停止并报告(不碰数据,退出码 2)
│通过
↓
备份当前库(data/backups/,保留 1 份)
↓
同步写入 *.tmp-sync 临时库(增量拉取 / 全量重建 + 聚合重算)
↓
数据校验 ──失败──→ 删除临时库(= 回退到同步前,退出码 3)
│通过
↓
原子替换正式库 + 写同步日志(sync_log 表 + .sync.log 文件)
回退原理:正式库在整次同步中只被读、不被写(所有写入都落在临时库副本上), 校验失败时删掉临时库即回到同步前状态,不存在"回退到一半"的情况。
2. 快速开始
python scripts/sync_data.py # 日常增量同步(默认)
python scripts/sync_data.py --check # 只做环境检查,不同步
python scripts/sync_data.py --full # 全量重建(慎用,见 §9)
python scripts/sync_data.py --log 10 # 查看最近 10 条同步日志
| 场景 | 命令 |
|---|---|
| 日常数据更新 | python scripts/sync_data.py(约 5~20 秒) |
| 出门在外想确认能不能同步 | python scripts/sync_data.py --check |
| 首次建库 / 换机器初始化 | python scripts/sync_data.py --full |
| 改了 chain_map.json、开放字典、提取器代码 | python scripts/sync_data.py --full |
| 只想看库有多旧 | python scripts/sync_data.py --log 1,或查 etl_meta.ref_date |
| 只改了时间窗口径(不涉及新数据) | python scripts/refresh_trends.py(无需 MySQL) |
退出码:0 成功;2 环境检查失败;3 数据校验失败(已回退)。
3. 增量同步规则
同步的范围与判定全部由水位驱动:etl_meta.wm_live_id / wm_digest_id
记录上次同步时源表主键的最大 id(含已删除行)。旧库没有水位时,首次同步自动按
report.MAX(src_id) 引导,无需人工干预。
| 源库变化 | 是否同步 | 处理方式 |
|---|---|---|
| 新增研报(id > 水位) | ✅ | 逐行解析写入:题材/个股/券商引用/评级事件/图片/大涨题材段,与全量同一套提取逻辑 |
| live 研报被删除(is_deleted=1) | ✅ | 本地连同 report_theme / report_stock / report_broker / report_image / rating_event 一并剔除 |
| 板块字典变化(分类调整/新板块) | ✅ | plate_rank_infos 每次全量刷新,题材分类同步更新 |
| 已同步研报的内容被编辑 | ❌ 不检测 | 源表无可靠更新时间戳;确有需要时跑 --full |
| digest 研报被删除 | ❌ 不检测 | 源表语义为追加型;个别误文影响极小 |
每次增量同步后固定执行的重算(保证与全量口径一致,秒级耗时):
- 券商引用/评级事件自愈重建:
report_broker/rating_event由已存正文全量重建 (历史 lastrowid bug 曾产生大量孤儿引用,此步骤即自愈,见 §8 排障) - 聚合:题材出现次数/首末次/衰减热度、题材×个股共现(theme_stock)
- 时间窗:cnt_5d/10d/20d/30d、streak_days、theme_daily(
src/trends.py) - 观点单元:近 7 日研报正文分节解析回填
report_section(每日简报数据源,src/daily.py) - 推荐原因:近 7 日提及回填 reason/reason_type(
src/viewpoint.py,修复了旧流程手动跑的断档问题) - 链映射:
data/chain_map.json重新载入
全量(--full)与增量的差异只有一点:不读旧库,从源库拉全部行重建临时库。
环境检查、校验、回退、日志完全相同。
4. 环境检查(同步前)
任一项失败:打印全部错误后停止,不触碰任何数据,退出码 2。
| # | 检查项 | 失败信息关键词 | 处置 |
|---|---|---|---|
| 1 | data 目录存在且可写 | 数据目录不可写 |
检查磁盘/权限 |
| 2 | 磁盘剩余空间(≈2× 库大小 + 100MB) | 磁盘空间不足 |
清理 data/backups/ 等 |
| 3 | MySQL 可连接(SELECT 1) | MySQL 连接失败 |
确认在内网/VPN;检查 .env 的 DB_* |
| 4 | 源表与所需列齐全 | 源表缺失 / 缺列 |
上游 schema 变更,需人工评估 src/sync.py 里的 REQUIRED_MYSQL_COLUMNS |
| 5 | 增量要求本地库已存在且可用 | 本地库不存在(提示 --full) |
首次同步改用 --full |
| 6 | DB_PASSWORD 为空 | 警告 DB_PASSWORD 为空 |
仅提醒,不阻断(内网免密场景正常) |
5. 数据校验(同步后)
在临时库上执行;E 类 / X 类失败即回退(退出码 3,错误明细打印并记入日志), W 类只记警告。
| 编号 | 校验内容 | 典型触发原因 |
|---|---|---|
| E0 | SQLite PRAGMA quick_check |
磁盘/文件损坏 |
| E1 | 水位一致:本地 MAX(src_id) ≤ 新水位 = 源表 MAX(id) | 拉取遗漏、并发写入 |
| E2 | 计数守恒:同步后篇数 = 同步前 + 新增 − 剔除 | 写入丢失或重复 |
| E3 | 外键孤儿不新增(七组引用对比同步前基线) | 提取/写入 bug |
| E4 | 新行 title / display_time 非空率 ≥ 99% | 源数据格式变化 |
| E5 | 聚合一致:occurrence_count、theme_daily 与明细重算一致 | 聚合中断 |
| X1 | 新研报抽样与 MySQL 逐字段比对(title/summary/content/时间/付费) | 网络截断、两端不一致 |
| W1 | ref_date 较同步前回退 | 源端删除了最新研报 |
| W2 | 新行含显著早于水位的时间戳 | 源端回补历史数据(正常,仅提醒) |
| W3 | 历史遗留外键孤儿(本次未新增) | 老数据问题,不阻断;可全量重建清理 |
6. 同步日志
每次同步(成功、环境失败、校验回退)各留一条记录,两处双写:
- 库内
sync_log表(字段见 数据模型):可用 SQL 分析,python scripts/sync_data.py --log N人读展示最近 N 条 - 文件
data/xgb_wiki.sync.log(gitignore):库不可写时仍能留痕,每行一条
$ python scripts/sync_data.py --log 3
#5 2026-09-01T21:27:19 [incremental] success live+0 digest+0 删-0 研报7797→7797 5s
#4 2026-09-01T21:27:02 [incremental] success live+17 digest+13 删-0 研报7767→7797 5s
#3 2026-09-01T21:20:52 [incremental] failed_rolled_back live+0 digest+0 删-0 研报7767→0 5s
✗ E3 外键孤儿新增 report_broker→broker:9695 → 9746 行
首页展示的 built_at 与 etl_meta.ref_date(最新一篇研报时间)也可用来判断库的新鲜度。
7. 备份与手动恢复
每次同步(含失败)前自动把当前库复制到 data/backups/xgb_wiki.db.<时间戳>.bak,
只保留最近 1 份(gitignore)。正常情况无需手动恢复——校验失败即回退,成功则新库已过校验。
确需手动恢复时:
# 1) 停掉 Web 服务(避免文件占用)
# 2) 用备份覆盖正式库
cp data/backups/xgb_wiki.db.20260901_212703.bak data/xgb_wiki.db
# 3) python scripts/run_tests.py 确认库完好
8. 故障排查
| 现象 | 原因 | 处置 |
|---|---|---|
退出码 2,MySQL 连接失败 |
不在内网 / VPN 断 / .env 凭据错 |
连内网后先 --check;凭据对照 2_xuangubao 项目 |
退出码 3,E3 外键孤儿新增 … |
提取或写入逻辑引入坏引用 | 看错误里的表名;若为 report_broker/rating_event 会自动重建自愈,持续出现说明提取器改坏,跑 run_tests.py 定位 |
退出码 3,X1 字段不一致 |
本地与源库内容对不上(网络截断/源端秒改) | 直接重跑一次同步;复现则对比错误字段 |
退出码 3,E2 计数不守恒 |
拉取中断或 id 重复 | 重跑;复现检查水位是否被人工改过 |
os.replace: PermissionError |
Web 服务(或别的进程)占用库文件 | 停掉 5010 端口进程再同步:netstat -ano | findstr :5010 → taskkill //F //PID <pid> |
| W3 警告一直存在 | 老库历史孤儿(如 lastrowid bug 遗留的 report_stock 孤儿) | 不影响使用;介意的话 --full 重建(注意 §9 图片解析代价) |
| 研报数不涨但源库明明有新数据 | 水位被意外推高(如人为改库) | 查 etl_meta 的 wm_* 与源表 MAX(id);必要时 --full |
历史上的 get_broker_id lastrowid bug(2026-09-01 修复):老 ETL 在
INSERT OR IGNORE 未插入时会拿到 report 表的大 rowid 当 broker_id,累计产生约
9700 条孤儿引用。现由每次增量的自愈重建清理,E3/X1 双校验防复发。
9. 注意事项与红线
--full会把image_asset重置为未解析——丢失已付费的 LAS 图片解析结果 (约 390 张,计费 + 1 QPM,见 运行手册 §3)。日常更新一律用增量;--full仅用于建库、chain_map/字典/提取器改版。- 同步前停掉 Web 服务(
python app.py):替换库文件时若被进程占用会失败 (失败也安全,重跑即可)。 - MySQL 为只读账号,严禁任何写操作;同步代码只 SELECT。
.env凭据不入库;data/backups/与*.sync.log已 gitignore。- 同步会修改 git 管理的
data/xgb_wiki.db——同步成功后按需提交(正文来自付费产品,仓库保持私有)。