#!/usr/bin/env python3 """audit_schedule_coverage.py — 排查「定义了但没有任何调度任务采集」的孤儿表 背景(2026-09-19) ------------------ 用户报告 `stk_holdertrade` / `repurchase` 停摆约 65 个交易日。 排查结论:这两张表在 `_TYPE_TABLES` 的任何分组里都不存在, 也不在任何 `--tables` 列表里 —— **从未被任何调度任务调用过**。 `config.yaml` 里的 `tables:` 启用开关不救场:它只在 「未显式指定要抓哪些表」时才生效(`fetcher.py:239-243`), 而两个 launchd 调度脚本总是显式传 `--type` / `--tables`,开关被完全绕过。 于是「配置里写着 true」与「实际有没有在跑」是两件事,且没有任何机制对账。 本脚本就是那个对账机制:把 ① TABLE_MAP 定义的表 ② 调度脚本 + CLI 分组实际覆盖的表 ③ config.yaml 声明启用的表 三者交叉比对,输出: · 启用但无排期(真缺陷,会导致采集静默停摆) · 定义的但 config 未声明(多半是历史遗留,需人工确认是否该采集) 用法 ---- python3 Scripts/bin/audit_schedule_coverage.py python3 Scripts/bin/audit_schedule_coverage.py --root /Applications/FinDataHub.app/Contents/Resources/tushare_fetcher """ from __future__ import annotations import argparse import re import sys from pathlib import Path def parse_table_map_keys(fetcher_py: Path) -> set[str]: src = fetcher_py.read_text(encoding="utf-8") start = src.index("TABLE_MAP") # TABLE_MAP 内的键都缩进 8 空格 return set(re.findall(r'^\s{8}"([a-z0-9_]+)":\s*\(', src[start:], re.M)) def parse_cli_groups(cli_py: Path) -> set[str]: src = cli_py.read_text(encoding="utf-8") covered: set[str] = set() for _name, items in re.findall(r'"(\w+)":\s*\[([^\]]*)\]', src, re.S): covered |= {x.strip().strip('"') for x in items.split(",") if x.strip()} return covered def parse_sync_tables(sync_sh: Path) -> set[str]: if not sync_sh.exists(): return set() src = sync_sh.read_text(encoding="utf-8") covered: set[str] = set() for m in re.findall(r"--tables\s+([A-Za-z0-9_,]+)", src): covered |= {x.strip() for x in m.split(",") if x.strip()} return covered def parse_config_enabled(cfg_yaml: Path) -> tuple[set[str], set[str]]: if not cfg_yaml.exists(): return set(), set() src = cfg_yaml.read_text(encoding="utf-8") if "\ntables:" not in src: return set(), set() sec = src[src.index("\ntables:"):] enabled, disabled = set(), set() for name, val in re.findall(r'^\s{2}([a-z0-9_]+):\s*(true|false)\s*$', sec, re.M): (enabled if val == "true" else disabled).add(name) return enabled, disabled def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--root", default="/Applications/FinDataHub.app/Contents/Resources/tushare_fetcher", help="tushare_fetcher 根目录(默认取部署位)") args = ap.parse_args() root = Path(args.root) if not (root / "src/tushare_fetcher/core/fetcher.py").exists(): print(f"✗ 未找到 {root}/src/tushare_fetcher/core/fetcher.py") return 2 keys = parse_table_map_keys(root / "src/tushare_fetcher/core/fetcher.py") covered = parse_cli_groups(root / "src/tushare_fetcher/cli.py") covered |= parse_sync_tables(root / "scripts/tushare_sync.sh") # 分红由独立脚本按报告期刷新,不在 --tables 里 covered.add("dividend") enabled, disabled = parse_config_enabled(root / "config.yaml") orphan = keys - covered defect = sorted(orphan & enabled) undeclared = sorted(orphan - enabled) print(f"TABLE_MAP 定义的表 : {len(keys)}") print(f"其中被调度任务覆盖 : {len(keys & covered)}") print(f"config.yaml 启用 / 停用 : {len(enabled)} / {len(disabled)}") print() print(f"★ 真缺陷 — config 启用但无任何排期 : {len(defect)} 张") if defect: print(" 这些表声明为启用,却不在任何 --type / --tables 里 → 采集静默停摆。") for i in range(0, len(defect), 4): print(" " + " ".join(f"{t:<22}" for t in defect[i:i + 4])) print() print(f"(参考)孤儿且 config 未声明 : {len(undeclared)} 张") print(" 多半是历史遗留或从未启用的表;需人工确认是否应纳入采集。") for i in range(0, len(undeclared), 4): print(" " + " ".join(f"{t:<22}" for t in undeclared[i:i + 4])) print() print("提示:`tables:` 开关只在未显式指定表时生效,因此**不要以 config 为准判断某项是否在跑**。") return 1 if defect else 0 if __name__ == "__main__": sys.exit(main())