系列·叁。做事件驱动的朋友都知道,新闻管线的难点不在"抓",而在"抓完之后":同一条快讯被五个媒体转一遍怎么办?几千条里哪条和你持仓相关?这篇用 news-mcp 跑一条真实管线。
三层架构:Bronze / Silver / Gold
Bronze(采集) Silver(去重) Gold(事件化)
六个源独立抓取 → SimHash 近重复判定 → 事件分类 + 板块 + 情绪
新浪/财联社/华尔街 汉明距离 ≤ 3 判重 规则法(可升级 LLM)
见闻/龙虎榜/热榜/公告 24h 指纹窗口
设计上的两个刻意选择:
- 零第三方依赖:整个服务只用 Python 标准库(urllib + sqlite3),Docker 镜像极小,也永远不用担心依赖冲突
- 与 Go 实现的语义逐字节对齐:SimHash 连"只取 rune 低 8 位"这种 Go 怪癖都照搬了,为的是和主系统(Athena)的指纹一致——跨系统判重时才不比错
真实跑一遍(2026-09-29 盘后)
import sources, core
raw = sources.fetch_sina(num=30) # 新浪滚动
arts = sources.fetch_cls(limit=30) # 财联社快讯
== 新浪采集 == 30 条
== 财联社采集 == 0 条 ← 财联社源这次不可达,后面细说
拿 30 条新浪新闻进 Silver 去重:
== SimHash 去重 ==
{"input": 30, "output": 30, "deduped": 0, "seen_total": 30}
0 条重复——单源抓取时这个结果是健康的(新浪自己的滚动列表已经去重过了)。去重的威力在多源合并时才体现:同一个"央行降息"快讯,新浪、财联社、华尔街见闻各转一遍,标题只差几个标点,SimHash 汉明距离 ≤ 3 就吸收掉。seen_hashes 由调用方传入传出,服务本身无状态,你想用 Redis 做跨进程去重也行。
再进 Gold 事件分类:
== 事件分类 ==
{"input": 30, "events": 12, "dropped_no_type": 18, "llm_used": false}
[MACRO_POLICY] GTCFX:实际利率上行考验黄金韧性 9月29日,黄金回调再次把持有成本推到
板块: - 情绪: +0.00
[INDUSTRY] Anthropic招股书披露宏大AI愿景,成本激增
板块: AI算力,电力 情绪: +0.00
[CORPORATE] 里昂:康师傅控股重申"跑赢大市"目标价13.70港元
板块: - 情绪: +0.50
[CORPORATE] 解禁潮下,谁在卖出GPU四小龙?
板块: - 情绪: +0.67
[INDUSTRY] M4Markets:算力扩张提升稳定电源价值
板块: AI算力,电力 情绪: +0.50
分类是规则法(关键词表 + 判定顺序对齐 Athena 的 Go 实现):公司/宏观/贸易/地缘/行业/市场六大类,同时抽出 sh60*/sz00* 形态的股票代码、映射板块("算力"→AI算力、"储能"→光伏),情绪分 = (正面词数 − 负面词数)/(总词数+1)。18 条没有命中任何关键词的被丢弃——这是和 Go 版一致的行为,宁可漏判不可错判。
每个源都会挂:财联社案例
这次 fetch_cls 返回 0 条。逐源失败是常态(接口改版、反爬升级、网络抖动),所以每个源的 fetcher 都做成可注入的:
def fetch_cls(limit=30, fetcher=_http_json):
...
测试时传假 fetcher,生产挂了也不影响其他源。这个"单源失败静默跳过"的策略对采集是对的,但对 Gold 层要小心——如果某天六个源全挂,输出是"今天没有新闻"而不是"采集失败",这是所有新闻管线共同的坑,监控时要对采集量为零报警,而不是对错误报警。
已知的三个优化点(写给你参考,也是贡献机会)
- 去重是 O(n²):每篇文章要和全部历史指纹比汉明距离,
seen_hashes只增不减。量不大没关系,量上来要换 LSH 分桶(64 位指纹切 4 段,近重复必同桶,桶内再比对) - 24h 窗口没实现:
SIMHASH_RETENTION_SEC常量定义了,但过期淘汰留给调用方做——用 Redis 的话记得加 TTL - 分类升级路径已留好:
gold_process(..., llm_classifier=...)参数就是给 LLM 分类器留的位,传进去就切换(LLM 优先、失败自动降级规则法),这是下一篇要讲的决策模型天然的位置
采集侧的并行化(多 channel/多 code 同时抓)刚合入不久,30 条新闻的全流程现在秒级完成。
下一篇进因子挖掘:怎么让 AI 帮你挖因子,又不让它把未来函数埋进你的回测里。