跳转至

数据同步指南

选股宝脱水研报数据(内网 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_atetl_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 :5010taskkill //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——同步成功后按需提交(正文来自付费产品,仓库保持私有)。