# geo-news-python **Repository Path**: Humoonruc/geo-news ## Basic Information - **Project Name**: geo-news-python - **Description**: 国际新闻简报自动生成系统 - **Primary Language**: Python - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-24 - **Last Updated**: 2026-09-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # geo-news-python 国际新闻简报自动生成系统。从新闻爬取 → AI 标注分发 → 语义分组 → 去重整合 → 格式化排版 → 导出 Word,全流程自动化。 ## 项目概述 **核心思路**:将多信源国际新闻自动采集、标注、分发到地区和领域文件,经 AI 语义分组形成话题组,再由 AI 逐话题去重整合生成结构化简报,最终合并输出 Markdown + Word 文档。 **一次完整周报的全流程**(约 10–15 分钟): ``` login → crawl (5路并行) → export → auto-tag (并行) → 人工初审(删除不重要条目) → distribute(规则分发 + 热点专题 + 兜底)→ group(AI 语义分组) → 人工调组(合并/拆分话题) → to-xml → dedup-news → final-format → abstract → merge → weekly-summary → pandoc .docx → archive ``` 三个 `.bat` 文件对应三个阶段,双击即可运行: | 文件 | 做什么 | 输入 → 输出 | |------|--------|------------| | `crawl.bat` | 登录 + 五路爬取 + 导出 + AI 标注 | 新闻网站 → `content/source/output_*.md` | | `preprocess.bat` | 分发 + 热点分类 + AI 语义分组 | `output_*.md` → `content/geo_news/` + `content/eco_news/` | | `consolidate.bat` | 去重整合 + 合并 + 周报摘要 + Word + 归档 | 分组文件 → `content/output/consolidate_total.{md,docx}` | | `weekend.bat` | 收尾:归档(始终)→ 分发检查 → 清空工作目录 | 准备下一周 | > **数据流设计原则**:标注信息嵌入数据本身(XML 标签),而非仅依赖 prompt 约束。AI 无法绕过数据结构中的硬约束。 --- ## 快速开始 ### 安装 ```powershell python -m pip install httpx beautifulsoup4 lxml playwright "openai<3" python-dotenv jieba python -m playwright install chromium ``` > **openai 需锁定 `<3`**:3.x 依赖 httpx2/httpcore2 新版 HTTP 栈,在 Python 3.14 下 > 程序退出时会抛 `RuntimeError: generator didn't stop after athrow()` > (httpcore2 的异步生成器清理 bug;任务本身已完成,但报错会污染输出、误判为失败)。 > 2.x 的调用方式完全一致,且走 httpx/httpcore 稳定栈。 > 转换 Word 需要安装 [pandoc](https://pandoc.org/)。 ### 配置 1. 项目根目录创建 `.env`,写入 DeepSeek API Key: ``` DEEPSEEK_API_KEY=sk-xxx ``` 如需华尔街见闻 VIP 内容,追加账密: ``` WALLSTREETCN_USER=你的手机号或邮箱 WALLSTREETCN_PASS=你的密码 ``` 2. 编辑 `settings.py`: - `CRAWL_START_TIME` — 兜底爬取起点(首次使用设历史起点如 `2026-01-01 00:00`)。日常增量时每个信源实际从自己 DB 最新时间继续,此值仅在信源无数据时兜底 - `CRAWL_WINDOW_START` — 固定窗口起点(默认空=增量模式)。补爬历史时设此值(如 `2026-08-01 00:00`),配合 `CRAWL_END_TIME` 组成固定范围,库中已有条目自动跳过;用完改回空字符串 - `OPINION_START_TIME` / `OPINION_END_TIME` — 参考资料(观察者网)的导出时间窗口,每周末手动更新 3. 首次运行需要人工过点选验证码(华尔街见闻)。运行 `crawl.bat` 时 Phase 0 自动弹出有头浏览器,手动完成验证即可。登录态保存在 `data/wallstreetcn_profile/`,后续可复用。 ### 运行 ```powershell crawl.bat # 爬取 → 导出 → 标注 # → 人工快速浏览 content/source/output_*.md,删掉不重要条目 preprocess.bat # 分发 + 话题分组 # → 人工检查话题组是否合理,必要时合并/拆分 consolidate.bat # 整合 → 去重 → 格式化 → Word → 归档 ``` 最终产物:`content/output/consolidate_total.md` + `.docx` + `weekly_summary.md`。 每周结束时运行 `weekend.bat` 收尾:归档当前整合稿(始终执行),并在 source 全部清空后清空工作目录、准备下一周(见 Phase 4)。 --- ## 工作流详解 ### Phase 1: `crawl.bat` — 新闻采集 ``` Phase 0: login-wallstreetcn → 有头浏览器登录,保存 profile Phase 1: 五路并行爬取(人民网 / 联合早报 / 观察者网 / 财联社 / 华尔街见闻) 写入双库 SQLite:geo_news.db(地缘) + eco_news.db(经济) Phase 2: 从 SQLite 导出 Markdown → content/source/output_{source}.md Phase 3: AI 自动标注元数据(并行处理 5 个信源) ``` **标注字段**(AI 自动补齐 7 个):领域 / 二级领域 / 三级领域 / 地区 / 摘要 / 标签 / 文本性质 标注结果直接写入 `output_*.md` 文件,每条新闻末尾追加 `>> 标签:xxx` 等元数据行。 ### Phase 2: `preprocess.bat` — 分发与分组 **第一步:分发**(`distribute_news.py`,三阶段) | 阶段 | 内容 | 目标 | |------|------|------| | Phase 1 规则分发 | 已标注的「政治和地缘」按地区 → `geo_{region}.md`;「经济和金融」先匹配「二级+三级」组合规则,未命中再按二级领域 → `eco_*.md` | 地区/领域文件 | | Phase 2 热点专题 | 剩余有摘要条目 → LLM 一次调用判断是否属于 AI+/新能源/气候/商业航天等热点专题(默认 DeepSeek,可经 `AI_PROVIDER` 切换) | `eco_{topic}.md` | | Phase 3 兜底 | 仍未分发的条目 → `eco_others.md` | 零遗漏 | >`config/hot_topics.toml` 定义热点专题(目前 5 个:AI+、新能源产业链、气候异常(厄尔尼诺/极端天气)、商业航天、房地产),增删专题只需修改此文件 + 同步 prompt。 **第二步:AI 语义分组**(`group_news_ai.py`) 对上一步产出的所有 `eco_*/geo_*` 文件,调用 LLM(走 **flash 档**,由 `AI_PROVIDER_FLASH` 决定 Provider 与模型,可用 `--model` 覆盖)进行语义分组,将属于同一话题的新闻归为一组,形成「话题组」。每组以 `# 组名(N 条)` 为一级标题,后续整合时按组处理。 分组结果直接写回原文件: - **组内**:条目按发布时间升序排列。 - **组间**:按话题涉及的「国家/地区」聚类排序——从组内条目的标题/标签/摘要识别主要国家,同一国家的组相邻(如印太文件里日本话题组相邻、印度话题组相邻)。该排序是确定性代码逻辑(`COUNTRY_KEYWORDS` 词典),不依赖 AI 返回顺序。 - **中东特殊**:`geo_MiddleEast.md` 中「美伊冲突」大话题的组优先排列,组间按组内最早发布时间升序(旧→新,呈现事件时间线);其余涉及黎巴嫩、巴勒斯坦、也门等国家的组再按国别聚类。 ### Phase 3: `consolidate.bat` — 整合与输出 ``` Step 1: quick(to-xml → dedup-news → final-format → abstract,6 个地区并行处理,并发上限 6;dedup 组内再并发 4 路,并复用增量缓存) Step 2: merge — 合并各地区摘要稿 → consolidate_total.md(优先读 _abstract.md,降级 _fmt.md) Step 3: weekly-summary — AI 生成 750–850 字周报摘要 Step 4: pandoc(--lua-filter=scripts/fost.lua)— Markdown 转 Word (.docx) Step 5: archive — 归档到 SQLite(data/annotated_news.db) ``` **quick 子流程**(每个地区独立执行): | 步骤 | 工具 | 作用 | |------|------|------| | to-xml | `metadata_to_xml.py` | 将人工标注(`>> 标签:xxx`)转为结构化 `` XML 标签 | | dedup-news | `dedup_news.py` (v4-pro) | 同组新闻去重/精简,保持信息密度,展开 FOST 评论。**组内并发 4 路 + 增量缓存**(内容未变的组直接复用) | | final-format | `final_format.py` (v4-flash) | 格式规范化:统一标题层级、控制段落长度、动态字数预算。批量运行时**文件间并发** | | abstract | `abstract.py` (v4-flash) | 正文简化为 5-8 句摘要,FOST 评论压缩为 2-3 句。批量运行时**文件间并发** | **Word 输出(Step 4 pandoc + `scripts/fost.lua`)**: - 每个 `#### FOST:评论` 段渲染为自定义段落样式 `FOSTComment`:字体/字号/首行缩进与模板 Heading 4 一致(蓝色楷体),但不属于标题样式,**不会出现在 Word 导航窗格** - FOST 段前自动空一行;FOST 段后追加一个"**返回目录**"超链接段:宋体 9 号、蓝色、右对齐,点击跳转到文档第一页(书签 `_Top`,埋于文档开头第一个标题) - `FOSTComment` 样式预置在 `content/output/template.docx` 的 styles.xml 中(`basedOn` 指向 heading 4 的 styleId `41`,仅将大纲级别覆盖为正文级,因此不在导航窗格显示) - 注意:`template.docx` 被 Word 打开时 pandoc/python 无法写入,转换前需先关闭 Word 中的相关文档 ### Phase 4: `weekend.bat` — 收尾 ``` Step 1: 归档标注新闻到 SQLite(始终执行,无论 source 是否清空) Step 2: 检查 content/source/ 下 4 个源文件是否全部为空(确认已全部分发) Step 3: 清空 opinion/、eco_news/、geo_news/ 中所有 .md 文件(仅 source 全部清空后执行) Step 4: 提示修改 settings.py 中 OPINION_START_TIME / OPINION_END_TIME(同上) ``` > **归档优先**:Step 1 的归档始终执行,确保当前整合稿不会因 source 未清空而丢失(归档为增量去重、幂等)。若 source 仍有未分发内容,归档后即告警停止,跳过 Step 3/4,等完成分发后再跑一次本脚本。正常周(source 已清空)行为与旧版一致。 --- ## 输出示例 各地区整合稿格式: ```markdown ## 特朗普 80 岁生日庆典与政治秀:白宫格斗赛与独立日集会预告 据华盛顿综合电,6 月 14 日,特朗普以举办 UFC 格斗赛"UFC Freedom 250"的 独特方式庆祝 80 岁生日。耗资 6000 万美元,14 名 UFC 明星在白宫草坪的 "The Claw"场馆中对决。 #### FOST:特朗普将 80 岁生日、UFC 格斗赛与独立 250 周年庆典深度绑定, 是一场精心计算的政治表演。这不仅向核心选民集中动员和效忠确认,更意在 打破传统政治礼仪,强化其"反建制斗士"形象。 ``` 每周摘要示例: ```markdown 在美洲,特朗普办 UFC 格斗赛庆 80 岁生日。民调显示乡村支持率降至 50%。 麦康奈尔再次住院。纽森指控特朗普政治调查其家人。美联储维持利率不变…… 在欧洲,瑞士举行限制千万人口公投。英军首次主导拦截俄"影子舰队"油轮。 俄乌大规模空袭升级,基辅世界文化遗产遭重创…… 在中东,美伊宣布达成历史性和平协议,特朗普严批以色列空袭搅局…… ``` --- ## 核心设计 本章按数据流顺序,从采集到输出,逐层说明架构决策和边界条件。 --- ### 一、数据采集层 #### 1.1 双库架构 `geo_news.db`(地缘:人民网/联合早报/观察者网)和 `eco_news.db`(经济:财联社/华尔街见闻)独立存放: - 两类新闻的下游处理逻辑不同——地缘按地区分发,经济按二级领域分发 - 隔离避免单文件膨胀,也方便按需迁移 #### 1.2 并发安全与两级去重 **WAL 模式**:所有 SQLite 连接启用 `PRAGMA journal_mode=WAL` + `busy_timeout=5000`,因五路爬虫通过 `asyncio.gather` 并行执行,多进程可能同时写入。`insert_news` 内部有 3 次指数退避重试兜底极端锁冲突。 **两级去重**——SQLite 约束 + 爬虫侧 break 逻辑各有不同粒度: | 去重函数 | 去重键 | 适用场景 | 原因 | |----------|--------|----------|------| | `get_existing_keys` | 标题 + 链接 | 联合早报 | 时间仅在详情页存在,去重检查时不可用(待重构为先抓详情再判断) | | `get_existing_keys_full` | 标题 + 发布时间 + 链接 | 其余 4 个爬虫 | 防"旧闻换时间重发"或"快讯改标题转完整报道"导致误判 | ```python # SQLite UNIQUE 约束仍为 (标题, 链接)——写入去重粒度较粗 # break 逻辑用三元组,更严格——确保不跳过多条可能的新内容 ``` **假阴性边界**:观察者网/财联社的去重用列表页时间,但入库可能用详情页更精确时间 → key 不匹配 → 文章被重新处理。这是**安全的"假阴性"**:最多浪费一次请求,不会丢内容。 #### 1.3 五源爬虫策略 | 信源 | 技术方案 | 关键约束 | |------|----------|----------| | **华尔街见闻** | Playwright 浏览器内 `fetch()` 调 API | 反爬最复杂:登录入口为首页 modal(旧 `/signin` 已 404);检测 headless 隐藏登录入口,首次必须用有头模式手动过验证码;VIP 文章需展开 `.mask` 遮罩层后提取正文;登录态通过 `launch_persistent_context` 保存在 `data/wallstreetcn_profile/` 跨次复用 | | **财联社** | Playwright `page.on("response")` 拦截 API | API 响应结构不统一(翻页纯数组 vs assembled `{"data":{"depth_list":[...]}}`),代码通过候选 key 深度遍历自适配;无限滚动依赖点击"加载更多"按钮而非 `scrollTo` | | **联合早报** | Playwright 无限滚动 | `#publish_time` 只在详情页 → 去重仅能用标题+链接;IP 自动跳转域名(`.cn`/`.com`/`.sg`),爬取时需关闭 VPN | | **观察者网** | httpx 优先 + Playwright 回退 | body < 2000 字符判定为 JS 渲染页(风闻社区),自动切换 Playwright;多页文章自动探测(`_1.shtml→_2.shtml` 或 `.shtml→_2.shtml`);列表页/详情页时间不一致,优先详情页 | | **人民网** | httpx + BeautifulSoup | 最简单,纯静态 HTML;正文含"《 人民日报 》"的转载条目在 auto_tag 阶段自动移除 | #### 1.3.1 华尔街见闻爬虫:反爬对抗全流程 华尔街见闻是五个信源里反爬最复杂的。以下是本项目已踩过的坑与对应对策(按请求生命周期排序)。 **1. 登录态** - 登录入口是首页弹窗 modal(旧版 `/signin` 已 404)。 - 网站检测 headless 会隐藏登录入口 → 首次必须用有头模式(`settings.WALLSTREETCN_HEADLESS=False`)手动过点选验证码。 - 登录态用 `launch_persistent_context(user_data_dir=data/wallstreetcn_profile/)` 持久化,跨次复用,无需每次登录。 - `main()` 检查 `_login()` 返回值;登录失败(尤其 headless 下找不到入口)会明确告警,不再静默继续白爬。 - 若运行中出现 `⚠️ 命中「下载华尔街见闻App」拦截页`,几乎可确定是登录态失效 → 有头模式 `cli.py login-wallstreetcn` 重登一次。 **2. User-Agent 伪装(致命点)** - 必须用真实浏览器 UA 覆盖 Playwright 默认值:`launch_persistent_context(user_agent=REAL_UA)` 中的 `REAL_UA` 去掉了默认的 `HeadlessChrome` 字样。 - 若不覆盖,服务端直接返回「下载华尔街见闻App」兜底层:SSR `` 有时还是文章标题(迷惑性极强),但 `<body>` 只有 logo + "读懂全球市场" + 下载链接,正文 0 字。日志表现为 `body文本长度≈17`、`<p>总数=0`。 - 这是最容易误判的根因——看起来像"React 没渲染"或"超时",实际是服务端根本没吐正文。 **3. 详情页导航:禁用 `networkidle`** - `page.goto(wait_until="networkidle")` 在实时行情站永远超时(WebSocket/行情轮询不断,连续 500ms 无请求的条件满足不了)→ 每篇 30s 超时、全部 `正文获取失败`。 - 改为 `wait_until="domcontentloaded"`。 **4. 标题校验:轮询等待 SPA 注水** - 华尔街见闻是 Next.js SPA,SSR 默认 `<title>` 是「华尔街见闻」,文章标题由前端异步注入,可能晚于 domcontentloaded 才出现。 - 用 `wait_for_function("(key)=>document.title.indexOf(key)!==-1", arg=预期标题前8字, timeout=15000)` 轮询,直到标题变成预期文章标题(等价于「不再是默认标题」)。 - 比「固定 sleep 3s」更精准:注水快则几乎不等待,慢则最多等 15s;超时仍未匹配视为错误页/登录墙,跳过该篇。 **5. 正文渲染等待:轮询内容就绪** - `domcontentloaded` 后正文是 React 异步渲染,`page.content()` 可能取到空。 - 用 `wait_for_function` 轮询直到文章容器(`.article__content`/`.rich-text`/`article` 等)或任意 `<p>` 含 >50 字文本才提取,超时 15s。 - 命中 VIP 遮罩层 `.mask` 的文章需先点击展开再提取。 **6. 诊断手段** - `_fetch_detail` 提取后打印诊断:`content长度 / 容器命中 / <p>总数 / 有文本<p> / body文本长度`。 - 正文为空时 dump 页面 HTML 到 `data/debug_wsc_{id}.html`,便于排查真实 DOM(确认是否命中拦截页)。 - 命中 `下载华尔街见闻App` 或 `读懂全球市场` 关键词时,直接打 `⚠️ 命中拦截页` 告警。 **排错速查表** | 现象 | 根因 | 解法 | |------|------|------| | 详情页 `Page.goto Timeout 30000ms exceeded` | `networkidle` 在行情站永不等到 | 改 `domcontentloaded`(见 3) | | `正文获取失败`,`body≈17` 字、`0` 个 `<p>` | UA 含 `HeadlessChrome` / 登录态失效 → 命中「下载App」拦截页 | 覆盖真实 UA;有头重登(见 2、1) | | `页面标题不匹配` 反复出现 | SPA 注水慢,初始等待不足 | 已改轮询等标题就绪(见 4) | | `提取结果 content长度=0` 但标题正常 | React 未渲染完 | 已改轮询等正文就绪(见 5) | | 日志无 `登录成功`/`已有有效登录态` | 登录态失效或 headless 隐藏入口 | 有头 `cli.py login-wallstreetcn` 重登 | #### 1.4 增量导出:保留人工标注 `export_news.py` 的核心价值是**增量导出时不覆盖已有标注**: - 目标 MD 已存在且链接匹配 → 复用全部 15 行 `>> ` 元数据 - 仅新条目生成空白元数据行,auto_tag 只标注新增部分 - 元数据行数不匹配(旧版代码遗留)自动识别并重建 **正文清理**:`_sanitize_content()` 将 `~~~`(围栏代码块)→ `---`(水平线),防止财联社快讯中的连续波浪线触发 Markdown 解析器吞掉后续内容。`_clean_summary()` 去除摘要内部换行,防止破坏元数据行结构。 #### 1.5 失败条目重试清单(`src/failed_items.py`) **要解决的问题**:各爬虫都是增量模式(起点 = 该信源 DB 最新发布时间)。若某条抓取失败 (详情页超时、正文为空、时间解析失败),它不会入库;而本轮其他条目成功入库后**起点被推进**, 下次运行扫到起点那条就 `break` → **早于新起点的失败条目再也扫不到,永久漏抓**。 实测(2026-09-14 华尔街见闻):单轮 8 条失败中只有 1 条能靠增量机制补回,其余 6 条永久丢失。 **机制**:失败条目落盘到 `data/failed_items.json`,下轮运行优先重抓、成功即移除。 | 时机 | 动作 | |---|---| | 运行开头 | `cleanup()` 清掉「其实已入库」的残留(人工补抓过);`load()` 取出待重抓条目,**放在待处理列表最前** | | 抓取失败 | `mark_fail()` 累加 `attempts` 并记录原因 | | 抓取成功 | `mark_ok()` 从清单移除 | | 运行结束 | 打印清单状态;达到 `MAX_ATTEMPTS`(5 轮)的条目不再自动重抓,改为告警提示人工处理 | **5 个爬虫均已接入**:华尔街见闻、人民网、观察者网、联合早报、财联社。 运行日志中可见 `🔁 失败重试清单:并入 N 条待重抓`(有重抓时)与 `📋 失败清单:N 条待下轮自动重抓`(收尾统计)。 > ⚠️ 配套修正:观察者网原先会把**空正文**也写进 DB,链接一旦入库,下轮 `cleanup()` > 就会把它当成"已抓过"清掉,永远补不回来。现已改为**空正文不入库 + 记入清单**。 --- ### 二、AI 语义理解层 #### 2.1 自动标注(auto_tag) 每条新闻在采集后自动调用 LLM(默认 DeepSeek v4-flash,可经 `.env` 的 `AI_PROVIDER` 切换智谱)填写 7 个字段,嵌入元数据行供后续所有 AI 环节参考: | 标注字段 | 作用 | 规则 | |----------|------|------| | **领域 / 二级领域 / 三级领域** | 分发阶段路由依据 | 政治和地缘 → 地区;经济和金融 → 二级领域 | | **地区** | 地缘新闻分发到 6 大区 | 非洲 / 美洲 / 中国 / 欧洲 / 印太 / 中东 六选一 | | **摘要** | 告知 AI "这条的核心意义" | 仅**需标注的条目**(其他字段为空)会清空旧摘要后 AI 重写,长文覆盖 2-3 个具体判断 + 关键数据,短文 1-2 句 | | **标签** | 描述性关键词,辅助语义分组 | 3-5 个关键词,分号分隔 | | **文本性质** | 区分新闻与分析评论 | 新闻 / 分析评论 | > 另有 **重要性 / 强制独立 / 信源倾向 / 主题 / 分析师定性** 5 个字段为人工标注(初审时按需填写),供下游语义分组、去重整合、FOST 评论展开使用,auto_tag 不会自动填写。 执行策略:BATCH_SIZE=4 条一批,MAX_CONCURRENT=2 并发(每信源子进程并发,5 信源并行总并发=10);`max_tokens=16384`(V4-Flash 思考模式会先消耗推理 token 再输出正文,过小易把输出挤占为截断);失败条目逐条重试 `AUTO_TAG_MAX_RETRIES=3` 轮兜底(也可用 `--retry N` 覆盖)。 #### 2.2 AI 语义分组 vs TF-IDF 新闻分组由 `group_news_ai.py`(v4-pro)完成,对每个地区/领域文件进行语义理解,输出 `# topic_label(N 条)` 组标题。早期使用 TF-IDF + 余弦相似度,已升级为 AI 语义分组: | 维度 | TF-IDF | AI 语义分组 | |------|--------|------------| | 原理 | 词频统计 + 余弦相似度 | 理解事件语义、因果链、参与方关系 | | 同事件识别 | ❌ "美国加征关税"与"中方回应"词重叠低,判为不相关 | ✅ 理解这是同一事件的两个视角 | | 假关联过滤 | ❌ "美联储加息"与"美联储人事变动"词重叠高,误判为相关 | ✅ 区分同一参与方的不同事件 | | 因果链 | ❌ 无法理解 A 是因 B 是果 | ✅ "OPEC 减产→油价涨→美国释储"归一组 | | 参数调优 | 需手动调三个参数,不稳定 | 无需调参 | 新闻分组本质是**语义理解**任务,不是相似度计算。从 TF-IDF 到 AI,是从"看起来像"到"真的相关"的跨越。 此外,标注字段 `强制独立=是` 的条目在代码层(`group_news_ai.py`)被强制各自单独成组——既不送入 AI 语义分组,也不并入任何主题预分组或 AI 组,纯代码层面保证深度长文/评论文章不被短新闻稀释。 **组间排序**:分组完成后,代码按每组涉及的「国家/地区」对话题组做聚类排序(同国家相邻),保证同一国别的话题聚在一起,便于按国别通读。主国家从组内条目的标题/标签/摘要中识别(标题命中权重最高,其次标签、摘要),`COUNTRY_KEYWORDS` 词典维护在 `group_news_ai.py`,新增国家只需追加关键词。中东文件另有特殊规则:美伊冲突大话题组优先并按时间排序,其余组按国别聚类(详见 `group_news_ai.py` 的 `_sort_groups_by_country`)。 #### 2.3 热点专题热插拔 `config/hot_topics.toml` + `prompt/hot_topics.md` 联动,增删专题只需改 TOML + 同步 prompt,代码自动适配。`distribute_news.py` 动态读取 TOML 生成目标文件和 LLM 分类逻辑,Phase 2 支持 `--skip-hot-topic <key>` 跳过特定专题。 --- ### 三、数据预处理层 #### 3.1 XML 硬约束:数据驱动 > Prompt 约束 整个 pipeline 最核心的设计决策:**不靠 prompt 传递规则,而是将标注嵌入数据本身**。 ``` 直接依赖 prompt:AI 可能忽略或误读(prompt 是软性建议) ↓ 升级为 结构化 XML 标签:AI 读到的不是"规则",而是按规则组织好的数据 ``` ```xml <meta> <标签>美欧贸易; 汽车关税; 欧盟反制</标签> <分析师定性>对欧施压信号,实际执行面临法律挑战</分析师定性> <重要性>高</重要性> <强制独立>否</强制独立> </meta> ``` 数据标签对 AI 是**不可绕过的输入**——它无法"忽略"一条数据的 `<标签>`,就像无法忽略标题或正文。Prompt 是最后一道防线,不是第一道。 #### 3.2 数据分层 | 层级 | 内容 | 路径 | 时间控制 | |------|------|------|----------| | **新闻信源**(4 个) | 人民网、联合早报、财联社、华尔街见闻 | `content/source/output_*.md` | `CRAWL_*` / `MD_*`(自动更新) | | **参考资料** | 观察者网 | `content/opinion/output_guancha.md` | `OPINION_*`(周末手动更新) | | **话题简报** | 人工撰写的一周主要话题简报 | `content/opinion/*.md`(除 `output_guancha.md`) | 手动维护 | 观察者网的数据定位: - 不做独立条目,仅在 dedup_news 中按地区分组作为额外上下文喂给 AI - AI 可在 FOST 评论中引用其深度分析,但**禁止以其为唯一信源** - 可通过 `--no-guancha` 禁用 本周话题简报的数据定位: - 人工按话题撰写,格式:`# 话题标题` + 事实梳理 + `## 评论:`(观点) - 篇幅小(每篇约 1–2KB)且天然跨地区/跨话题,故 dedup_news **整块注入**每个新闻组的 参考资料区,由 AI 判断在哪些组吸收其观点与论述角度——使各地的 FOST 评论具备 本期整体视野,而非各自就事论事 - 不做按话题筛选:简报名是概括性标题(如"习近平访美:元首外交落地与休战延长的分歧"), 与新闻组话题的关键词重合度低,按相似度硬筛反而会漏掉真正相关的内容 - 新增简报只需把 `.md` 放进 `content/opinion/`,无需改代码;`--no-briefs` 可禁用 --- ### 四、两阶段整合 "不遗漏信息"和"控制篇幅"是内在矛盾的认知任务,拆为两个独立 AI 调用,各司其职: | 维度 | dedup_news(信息完整性) | final_format(可读性) | |------|------------------------|---------------------| | 模型 | DeepSeek v4-pro | DeepSeek v4-flash | | 核心约束 | 不遗漏时间 / 主体 / 事件 / 影响 / 信源 | 每条 300–2000 字 | | 合并 | 按话题组同组合并 | 禁止合并(dedup 已完成) | | FOST 评论 | 深度展开,严禁敷衍 | 不修改 | | 信源日期 | 每条事实必标 | 不修改 | | 字数 | 无上限 | 按重要性标签动态分配预算 | 处理策略:N≥2 组 AI 合并去重,N=1 单条也走 AI 精简压缩(砍背景铺垫、仪式性表态、合并冗余段落)。可通过 `-g` 参数只处理大组。 --- ### 五、Prompt 架构 #### 5.1 三层分离 每次 API 请求按顺序拼接,各层独立维护,修改即生效: ``` [System Prompt] 角色定义,极简。例:"你是一位资深国际政治分析师。" [地区专属 Prompt] 地缘背景知识(prompt/ 下 6 份独立 .md) 放在 System Prompt 之后,确保 AI 在"拥有背景知识" 的状态下理解后续任务。 [阶段任务模板] 当前阶段指令 + Few-shot + 输出模板 + 禁止清单 [新闻正文] 经 XML 预处理的新闻数据 ``` #### 5.2 模型与参数 | 阶段 | 模型 | temperature | max_tokens | |------|------|-------------|------------| | auto_tag | v4-flash | 0.1 | 16384 | | distribute_news | v4-flash | 0.1 | 8192 | | group_news_ai | v4-flash(flash 档) | 0.3(可用 `-T` 覆盖) | 流式,不设上限 | | dedup_news | v4-pro | — | 65536 | | final_format | v4-flash | — | 65536 | | abstract | v4-flash | — | 65536 | | weekly_summary | v4-pro | — | 65536 | > **max_tokens 说明**:V4-Flash/Pro 为思考模式,`max_tokens` 是「推理 token + 正文输出」共享的总预算。过小(如 8192)会被推理挤占导致输出截断(`finish_reason=length`),故批量长文本场景统一放大到 16384~65536。`group_news_ai` 用流式调用且不设上限,天然无截断问题。 #### 5.3 AI Provider 开关(deepseek / zhipu) DeepSeek 偶发卡顿/限流时,可通过 `.env` 的 `AI_PROVIDER` 开关临时切换到智谱 GLM。统一由 `src/llm_provider.py` 的 `get_provider()` 处理——读取开关、选择模型 / Base URL / API Key / 思考模式参数。 ##### 双模型策略 两个 Provider 采用**对齐的档位语义**,切换 `AI_PROVIDER` 时两档模型同步切换: | 档位 | 适用任务 | DeepSeek | 智谱 GLM | 智谱单价(每百万 tokens) | |------|---------|----------|----------|------------------------| | `flash` | 简单/批量任务(自动标注、热点分类、格式化、摘要、语义分组) | `deepseek-flash` | `glm-5.3-flash` | 见下方价目 | | `pro` | 复杂/重推理(去重合并、周报生成) | `deepseek-flash`(V4 Pro 已于 2026-09-14 12:00 全量路由到 V4.1 Flash) | `glm-5.3-flash` | 见下方价目 | > 智谱价格取自官方定价页(2026-09-07)。`glm-4.7-flash` 为免费档,但**限流较严**(实测会返回 `429 该模型当前访问量过大`),因此 flash 档默认**关闭思考**以提速并降低触发概率。 ```env # .env # ── Provider 开关:两个档位各自独立,可混搭 ── AI_PROVIDER=deepseek # 全局兜底(未设分档开关时两档都用它) # AI_PROVIDER_FLASH=zhipu # 简单任务单独指定(取消注释生效) # AI_PROVIDER_PRO=deepseek # 复杂任务单独指定(取消注释生效) ZHIPU_API_KEY=xxx # 用到 zhipu 时需要 ZHIPU_THINKING=enabled # 思考模式 enabled/disabled(两档统一覆盖,不填则按档位默认) ZHIPU_REASONING_EFFORT=high # 只能 low/high/max(填 medium 会报 400 code 1210,整批全挂) # 分档模型(留空则用上表默认值) ZHIPU_MODEL_FLASH=glm-4.7-flash # 免费档 ZHIPU_MODEL_PRO=glm-5.3-flash DEEPSEEK_MODEL_FLASH=deepseek-flash # = DeepSeek-V4.1-Flash(旧名 deepseek-v4-flash 已下线,仍可调用) DEEPSEEK_MODEL_PRO=deepseek-flash # 注:V4 Pro 自 2026-09-14 12:00 起全量路由到 V4.1 Flash # flash 档建议关闭思考(更快、更省、显著降低免费档 429) ZHIPU_THINKING_FLASH=disabled ``` ##### 内容审核回退(auto_tag / final_format / abstract 共用) 内容审核拦截是**平台级判定、与模型无关**,且**同一 Provider 重试多少次结果都一样**,只能换 Provider: - DeepSeek:`400` + `Content Exists Risk` - 智谱:`400` + `code: 1301`「系统检测到输入或生成内容可能包含不安全或敏感内容」 `auto_tag` / `final_format` / `abstract` 内置跨 Provider 回退;后两者还带**分片降级**,三级处理任何一级都不丢内容: | 级别 | 动作 | 结果 | |---|---|---| | 1 | 整篇请求被拦 → 换备用 Provider | 通过则正常完成 | | 2 | 备用也拦 → **按 `## 话题` 拆片**(每片 ≤ `CHUNK_TOPICS` 个话题)逐片重试 | 只剩含敏感表述的片被拦 | | 3 | 某片仍被拦 → **继续二分细分**(6→3→1),直到单个话题 | 精确定位到真正敏感的那几个话题 | | 4 | 单话题仍被拦 → 该话题**保留原文** | 内容完整,仅该话题未经 AI 清洗 | 审核判定针对**整篇输入**,所以拆小后命中率显著下降:25K 字符的 China 稿整篇必拦, 拆片后通常只剩 1~3 个话题被拦。最终状态为 `partial`,汇总会给出「N/M 话题已 AI 清洗」的统计。 **为什么先整片、被拦才细分**:单片请求也要带完整规则 prompt,话题越多则重复开销越小。 先整片试(快),被拦再二分定位(准),兼顾速度与粒度——实测 12 话题、仅 1 个敏感时 共 9 次请求,最终 11/12 话题得到清洗。细分阶段只试主 Provider (备用方在顶层已证明拦同一批内容,每片都试会显著拖慢)。 **被拦话题会在终端逐条列出**(标题 + 文件行号),便于手动定位后单独处理: ``` [CHUNK] 分片结束:18/19 话题已 AI 清洗,1 个保留原文 ⚠️ 以下 1 个话题被审核拦截,已保留原文(未经 AI 清洗): 1. [geo_China_dedup.md:L123] 某话题标题 → 可手动改写这几处敏感表述后单独重跑,或直接接受原文 ``` `auto_tag` 是逐条标注、本就按 `BATCH_SIZE` 分批,只需第 1 级。 失败处理:`final_format` / `abstract` 有失败(`blocked` / `err`)时返回**非 0** 中止, **不会**拿上一次的旧产物静默顶替——避免 merge 读到过期文件,产出「看起来正常、实则缺内容」的 Word。 ```env LLM_FALLBACK_PROVIDER= # 留空 = 自动取「另一个 Provider」(主 DeepSeek 则回退智谱) ``` 备用 Provider 未配置 API Key 时会自动禁用回退并给出提示,不影响主流程。 日志标记:`[BLOCKED] 改用 … 重试` → `[CHUNK] 分片降级` → `[OK [PARTIAL]]`。 > ⚠️ 旧变量 `AUTO_TAG_FALLBACK_PROVIDER` 已废弃:它的值恰等于主 Provider 时回退会被静默禁用(旧默认值 `deepseek` 在当前主配置下正是这种情况),统一改用 `LLM_FALLBACK_PROVIDER`。 ##### 两个开关:按任务自由混搭 `AI_PROVIDER_FLASH` 与 `AI_PROVIDER_PRO` 可分别指定,实现「简单任务走便宜的、复杂任务走强的」: | 组合 | 写法 | 适用场景 | |------|------|---------| | 简单=智谱、复杂=DeepSeek | `AI_PROVIDER_FLASH=zhipu` + `AI_PROVIDER_PRO=deepseek` | **最省钱**:标注类输出密集走智谱,dedup 留 DeepSeek 吃缓存与低峰半价 | | 简单=DeepSeek、复杂=智谱 | `AI_PROVIDER_FLASH=deepseek` + `AI_PROVIDER_PRO=zhipu` | DeepSeek 卡顿时保住复杂任务 | | 统一切换 | 只写 `AI_PROVIDER=zhipu` | 临时全量切换 | 解析顺序:`AI_PROVIDER_{TIER}` > `AI_PROVIDER`(全局兜底)> `deepseek`。只设其中一个分档开关时,另一档自动回退到全局值。 > ⚠️ 若把 `ZHIPU_MODEL_FLASH` 改成 `glm-5.3-flash`(它更快、也不限流),必须同时删掉 `ZHIPU_THINKING_FLASH=disabled` 或改为 `enabled`——该模型不支持 `thinking=disabled`,传了会报 `400`(code 1210)。 代码侧按档位取值: ```python from src.llm_provider import get_provider cfg = get_provider("flash") # 简单任务:DeepSeek v4-flash / 智谱 glm-4.7-flash cfg = get_provider("pro") # 复杂任务:DeepSeek v4-pro / 智谱 glm-5.3-flash ``` 优先级:`{PROVIDER}_MODEL_{TIER}` > `{PROVIDER}_MODEL`(旧变量,两档统一覆盖,向后兼容)> 内置默认值。思考模式同理:`ZHIPU_THINKING_{TIER}` > `ZHIPU_THINKING` > 档位默认(flash=disabled / pro=enabled)。 **全部 7 个 LLM 模块均已接入开关**(改 `.env` 即全链路切换): | 模块 | 说明 | 档位 | |------|------|------| | `auto_tag` | 标注元数据 | `flash` | | `distribute_news` | 热点专题分类 | `flash` | | `group_news_ai` | AI 语义分组 | `flash` | | `final_format` | 格式规范化 | `flash` | | `abstract` | 摘要稿生成 | `flash` | | `dedup_news` | 去重整合 | `pro` | | `weekly_summary` | 周报摘要 | `pro` | > **接入原则**:所有模块统一通过 `src/llm_provider.get_provider(tier)` 取配置(模型 / Base URL / API Key / 思考参数)。新增 LLM 模块时务必走这一入口,不要各自硬编码,否则该模块将无法响应 `AI_PROVIDER` 开关。 > > **缓存说明**:`dedup_news` 的增量缓存键包含模型名,切换 Provider 后模型名变化会自动使缓存失效,不会读到另一个模型的旧产物。 **实测参考(2026-09-07,同一 5 条文件)**: | Provider | 档位 | 模型 | 耗时 | |----------|------|------|------| | DeepSeek | flash | `deepseek-v4-flash` | 7s | | 智谱 | flash | `glm-4.7-flash`(免费) | **47s** | > 免费档可用但**慢 6~7 倍**,且限流较严(会返回 `429 该模型当前访问量过大`)。仅在 DeepSeek 卡/限流时临时切换,日常建议用 DeepSeek。 #### 5.4 六条设计原则 1. **语义分组写进数据**:group_news_ai 生成 `# topic_label(N 条)` → dedup_news 按组处理 2. **指令定义到原子维度**:列明必须包含的五个维度(时间、主体、事件、直接影响、信源) 3. **输出格式给模板**:信源日期格式、FOST 结构、标题层级全部模板化 4. **Few-shot 优于抽象描述**:嵌入理想输出 vs 不合格输出的对比示例 5. **数据分层**:观察者网仅做补充参考,禁止以其为唯一信源;人民网/早报为主体 6. **动态字数预算**:每类新闻字数 = 总预算 ×(该类条目数 / 总条目数) 所有 prompt 文件独立存储在 `prompt/` 目录。调优流程:运行 → 检查日志 → 修改 .md → 重跑,无需改代码。 > ⚠️ **prompt 日志目前只有 `weekly_summary` 会写**(`logs/prompts-{date}.log`)。auto_tag / dedup_news / final_format / abstract / distribute_news / group_news_ai 都**不写**——调这几个模块时只能看控制台输出与 `logs/info-*.log`。 --- ### 六、编排与自动化 #### 6.1 时间窗口自推进 爬取与导出采用**按信源独立推进**的时间机制: - **爬取**:每个爬虫起点 = 该信源在 DB 中的最新发布时间(`storage.get_latest_time(信源)`),互不拖累;`CRAWL_START_TIME` 仅在信源无数据时兜底 - **导出**:每个信源起点 = 其已导出 MD 文件中的最新时间(`export_news._md_latest_time()`);`MD_START_TIME` 仅在 MD 为空时兜底 - `run_news.py` 每次执行后仍会把 `CRAWL_START_TIME` / `MD_START_TIME` 更新为各信源最大值,但只作兜底,不影响各信源独立增量。同时清除 `__pycache__/settings.*.pyc`——NTFS mtime 精度约 2 秒,不清缓存会导致子进程读到旧值 **固定窗口补爬**:如需补爬某个时间点之后的历史数据,临时在 `settings.py` 设 `CRAWL_WINDOW_START="2026-08-01 00:00"`(可选 `CRAWL_END_TIME` 限制终点),跑 `crawl.bat` 后改回空字符串。窗口模式同时作用于爬取与导出,库中已有条目自动跳过;导出时会与 MD 已导出范围取并集,不丢失已有条目。 观察者网的 `OPINION_START_TIME` / `OPINION_END_TIME` 不自动更新,需每周末手动修改。 #### 6.2 并行与子进程隔离 - **爬取**(Phase 1):`asyncio.create_subprocess_exec` × 5,任一失败即 `sys.exit(1)` - **标注**(Phase 3):同上,但失败仅 warn 不退出(标注可事后补) - **quick 整合**:`ThreadPoolExecutor` 并行处理(geo 地区模式 / eco 组模式均如此),**并发上限 6**(`settings.QUICK_MAX_WORKERS`,实际取 `min(任务数, 6)`),每个地区/文件以**子进程**运行(`subprocess.run`)而非直接函数调用——DeepSeek SDK 的 `AsyncOpenAI` 在多线程共享时可能出现 event loop 冲突,子进程隔离避免此问题。上限取 6 是因为 geo 恰好 6 个地区,原值 5 会让第 6 个地区排队空等 - **dedup 组内并发**:地区子进程内部,一个文件的话题组用 `asyncio.Semaphore` + `gather` **并发处理**(上限 `settings.DEDUP_CONCURRENCY`,默认 4)。这是提速的关键——改造前组内完全串行,关键路径等于组数最多的地区(中东 31 组);并发后关键路径 ≈ `ceil(组数 / 4)` - **final-format / abstract**:批量运行时文件级 `gather` 并发 - **to-xml**:全地区一次性处理(纯 IO 本地转换,不需并行) #### 6.3 API 限流重试(429) 所有调用 DeepSeek 的模块统一封装 `chat_with_retry` 助手,命中 `RateLimitError`(HTTP 429)时自动**指数退避重试**: - 覆盖 6 个模块共 8 个调用点:`dedup_news`(2)、`final_format`、`weekly_summary`、`distribute_news`、`auto_tag`(2)、`group_news_ai`(流式) - 重试策略:最多额外 `MAX_API_RETRIES` 次(默认 5),第 n 次等待约 `BASE_BACKOFF × 2ⁿ` 秒(默认基数 2.0 → 2/4/8/16/32s)叠加 0~1s 随机抖动,避免多 worker 同时重试造成请求雪崩 - 参数集中在 `settings.py`(`MAX_API_RETRIES` / `BASE_BACKOFF`),改一处即全局生效 - 仅对 429 重试;其他异常原样抛出,交由各模块已有的降级/校验逻辑处理(如 dedup 失败降级、group 降级为手动分组) #### 6.4 整合阶段耗时分布 整合阶段是整条流水线的耗时主体,且几乎全部消耗在 LLM 调用上: | 阶段 | 模型 | 调用次数 | 并发方式 | |------|------|---------|---------| | to-xml | — | 0 | 本地转换,可忽略 | | **dedup-news** | **v4-pro**(思考,max_tokens 65536) | **每组 1 次**(geo 全量约 100+ 组) | 地区间 6 路 × 组内 4 路 | | final-format | v4-flash | 每文件 1 次(校验失败可 ×3) | 文件间并发 | | abstract | v4-flash | 每文件 1 次(可 ×3) | 文件间并发 | | merge / pandoc / archive | — | 0(仅 weekly-summary 调 1 次) | 本地,秒级 | **阶段计时**:dedup / final-format / abstract 三个阶段的汇总都会打印总耗时(如 `=== dedup 阶段总耗时:X 分钟`)。dedup 还额外打印缓存命中数、每文件的组处理耗时与进度计数,便于事后定位瓶颈。 **并发调优**:在途请求数 ≈ 并行地区数 × `DEDUP_CONCURRENCY`。跑完后看控制台是否频繁出现 `[429]` 退避日志——几乎不出现 → 可调高 `DEDUP_CONCURRENCY` 到 6~8;频繁出现 → 下调到 2~3,用稳定性换速度。 #### 6.5 dedup 增量缓存 dedup 是唯一会反复处理相同内容的阶段,因此加了按内容指纹的增量缓存: - **缓存键** = `sha256(CACHE_VERSION │ 模型 │ system prompt 全文 │ 话题 │ 观察者网参考 │ 组内全部条目文本)`。模型、提示词、话题、参考资料、正文**任一变化都会自动失效**,不会读到脏缓存 - **存储**:`content/work/.dedup_cache/<指纹>.md`,一组合并稿一个文件。该目录位于已被 `.gitignore` 覆盖的 `content/work/` 下,无需额外配置 - **跨周保留**:`weekend.bat` 只清 `opinion/`、`eco_news/`、`geo_news/`,不清 `content/work/` - **产物顺序不受并发影响**:合并稿按组索引回填后重建文件,与串行执行结果一致 缓存开关: | 场景 | 用法 | |------|------| | 单次跳过缓存 | `--no-cache`(本次不读也不写) | | 强制全部重生成 | `--clear-cache`(先清空再继续) | | 改了提示词或模型参数 | 递增 `dedup_news.py` 的 `CACHE_VERSION`,旧缓存整体失效 | | 只想看能命中多少 | `--dry-run`(**只读不写**,不污染缓存也不产生费用) | > **注意**:缓存无自动过期。若怀疑某组合并质量不佳且内容未变,用 `--clear-cache` 或删除 `content/work/.dedup_cache/` 强制重跑该内容。 --- ## 项目结构 ``` geo-news-python/ ├── settings.py 全局配置 ├── .env API Key + 华尔街见闻账密 ├── cli.py 统一命令入口 │ ├── crawl.bat 新闻采集(login → crawl → export → tag) ├── preprocess.bat 分发 + 语义分组 ├── consolidate.bat 整合 → 合并 → 摘要 → Word → 归档 ├── weekend.bat 收尾:归档(始终)→ 检查 → 清空 → 提示更新时间 │ ├── src/ │ ├── storage.py SQLite 存储(双库:geo_news.db / eco_news.db) │ ├── llm_provider.py LLM Provider 统一配置(AI_PROVIDER 开关:deepseek/zhipu) │ ├── crawler/ │ │ ├── crawl_people.py 人民网爬虫 │ │ ├── crawl_zaobao.py 联合早报爬虫 │ │ ├── crawl_guancha.py 观察者网爬虫 │ │ ├── crawl_cls.py 财联社爬虫 │ │ ├── crawl_wallstreetcn.py 华尔街见闻爬虫 │ │ ├── login_wallstreetcn.py 华尔街见闻登录(有头浏览器) │ │ ├── run_news.py 采集全流程编排器 │ │ ├── export_news.py SQLite → Markdown 导出 │ │ └── auto_tag.py AI 自动标注元数据 │ ├── preprocess/ │ │ ├── distribute_news.py 三阶段分发(规则 + 热点 + 兜底) │ │ └── group_news_ai.py AI 语义分组 │ └── pipeline/ │ ├── metadata_to_xml.py 标注 → XML 预处理 │ ├── dedup_news.py 同组新闻 AI 去重精简(v4-pro) │ ├── final_format.py 格式规范化(v4-flash) │ ├── abstract.py fmt → 摘要稿(v4-flash) │ ├── merge.py 合并各地区整合稿(优先读 _abstract.md) │ ├── weekly_summary.py AI 周报摘要 │ └── archive_annotated_news.py 增量归档到 SQLite │ ├── scripts/ │ ├── clean_logs.py 清理过期日志 │ ├── weekly_cleanup.py 每周收尾清理 │ └── fost.lua pandoc filter:FOST 段落样式(FOSTComment)+ 返回目录超链接 │ ├── prompt/ AI 提示词(独立 .md 文件,改后即生效) │ ├── auto_tag.md 标注规则 │ ├── group_news_ai.md 语义分组规则 │ ├── hot_topics.md 热点专题分类规则 │ ├── dedup_geo.md geo 去重精简规则 │ ├── dedup_eco.md 经济类去重精简规则(量化数据零损耗) │ ├── final_format.md 格式规范化规则 │ ├── 每周摘要模板.md 周报模板 │ └── prompt_{地区}.md 6 份地区地缘背景 │ ├── config/ │ ├── hot_topics.toml 热点专题定义(增删即生效) │ └── eco_routes.toml 二级+三级领域定向分发规则(增删即生效) │ ├── content/ │ ├── source/ output_{信源}.md(爬取输出,分发后清空) │ ├── opinion/ 参考资料(观察者网) │ ├── geo_news/ 地缘新闻(6 地区) │ ├── eco_news/ 经济新闻(按领域 + 热点专题) │ ├── work/ 中间产物(XML、dedup 过程文件) │ └── output/ 最终简报 + Word │ ├── data/ SQLite(geo_news.db / eco_news.db / annotated_news.db) └── logs/ 分级日志 + API 调用记录 ``` --- ## 配置参考 `settings.py` 关键配置项: | 配置 | 说明 | 自动更新 | |------|------|----------| | `CRAWL_START_TIME` | 爬虫起点(兜底,仅信源无数据时用) | ✅ 每次 `run_news.py` 后自动更新 | | `MD_START_TIME` | 新闻导出起点(兜底,仅 MD 为空时用) | ✅ 与 `CRAWL_START_TIME` 同步更新 | | `CRAWL_WINDOW_START` | 固定爬取/导出窗口起点(空=增量,非空=补爬历史) | ❌ 手动设置,用完改回空 | | `OPINION_START_TIME` | 参考资料导出起点 | ❌ 每周末手动改 | | `OPINION_END_TIME` | 参考资料导出终点 | ❌ 每周末手动改 | | `WALLSTREETCN_HEADLESS` | 华尔街见闻浏览器是否无头(False=有头可手动过验证码,True=后台静默;首次登录保持 False) | ❌ 默认 True | | `WSC_DETAIL_CONCURRENCY` | 华尔街见闻详情页并发 tab 数(默认 1 串行;改 2 约 1.5-2 倍提速,但可能触发反爬,建议先小批量验证) | ❌ 默认 1 | | `CLS_HEADLESS` | 财联社浏览器是否无头(True=后台静默、不遮挡桌面) | ❌ 默认 True | | `GROUP_NEWS_AI_TEMPERATURE` | AI 语义分组温度(越低越精细保守,越高越粗放) | ❌ 默认 0.3,命令行 `-T` 可覆盖 | | `DEDUP_CONCURRENCY` | dedup 单个文件内并发处理的话题组数上限 | ❌ 默认 4,命令行 `--concurrency` / `-c` 可覆盖 | | `QUICK_MAX_WORKERS` | `quick` 并行处理的地区/文件数上限 | ❌ 默认 6(geo 恰好 6 个地区) | | `MAX_API_RETRIES` / `BASE_BACKOFF` | 429/超时重试次数与退避基数 | ❌ 默认 5 / 2.0 | | `DB_PATH` / `ECO_DB_PATH` | 双库路径 | — | | `REGIONS` | 6 个地区标识 | — | > 新闻信源和参考资料的时间窗口独立控制:新闻导出用 `MD_*`,观察者网导出用 `OPINION_*`。 --- ## CLI 参考 所有命令通过 `python cli.py <command>` 调用。按业务流程排序: ```bash # ── Phase 0: 登录 ── python cli.py login-wallstreetcn # 有头浏览器登录(保存 profile) # ── Phase 1-2: 爬取与导出 ── python cli.py crawl people # 单信源爬取 python cli.py crawl zaobao python cli.py crawl guancha python cli.py crawl cls python cli.py crawl wallstreetcn python cli.py news # 全流程:爬取 → 导出 → 标注 python cli.py export-news # 仅导出 # ── Phase 3: 标注与分发 ── python cli.py auto-tag --source cls # AI 标注 python cli.py distribute-news # 分发(规则 + 热点 + 兜底) python cli.py distribute-news --dry-run # 预览 python cli.py distribute-news --source cls # 仅指定信源 # ── Phase 4: 语义分组 ── python cli.py group-news-ai # 全部分组 python cli.py group-news-ai -f eco_ai_plus.md # 仅指定文件 python cli.py group-news-ai -T 0.5 # 调整分组粗细(默认 0.3,越低越精细) # ── Phase 5: 整合 ── python cli.py to-xml # 标注 → XML python cli.py to-xml America,Europe # 指定地区 python cli.py dedup-news # 全量:6 地区 geo + 全部 eco python cli.py dedup-news --prefix geo # 仅地缘(6 地区) python cli.py dedup-news --prefix eco # 仅经济类(eco_*_xml) python cli.py dedup-news MiddleEast # 仅指定地区 python cli.py dedup-news -g 5 # 仅处理 ≥5 条的大组 python cli.py dedup-news --dry-run # 预览(同时报告有多少组可命中缓存) python cli.py dedup-news --no-guancha # 不用观察者网参考 python cli.py dedup-news -c 2 # 调低组内并发(429 频繁时) python cli.py dedup-news --no-cache # 本次不用增量缓存(不读不写) python cli.py dedup-news --clear-cache # 先清空缓存再跑(强制全部重新生成) python cli.py final-format # 格式规范化 python cli.py final-format Africa # 指定地区 python cli.py abstract # fmt → 摘要稿(正文 5-8 句,FOST 2-3 句) python cli.py abstract Africa # 指定地区 python cli.py merge # 合并各地区(优先 _abstract.md) python cli.py weekly-summary # 周报摘要 python cli.py archive # 归档到 SQLite # ── 快捷入口 ── python cli.py quick # to-xml → dedup → format → abstract(全部地区) python cli.py quick China,America # 指定地区快速整合 python cli.py full # 一键到底(含 abstract) # ── 维护工具 ── python cli.py clean-logs # 清理 30 天前日志 weekend.bat # 每周收尾(归档始终执行,清空/更新时间仅 source 清空后) # 调试 $env:LOG_LEVEL="DEBUG" python cli.py quick America ``` --- ## 最佳实践 ### 逐地区迭代工作流 6 个地区一次性全量整合难一次到位,推荐逐地区迭代: 1. **逐地区运行**:`python cli.py quick America` → 检查 → 满意锁定 2. **不满意则微调**:修改 `prompt_{地区}.md` 或源文件标注,重跑该地区 3. **全部满意后合并**:`python cli.py merge` + `python cli.py weekly-summary` > 每个地区达到理想状态就锁定,不给后续改动破坏已合格结果的机会。 ### 标注策略 | 症状 | 优先排查 | 解法 | |------|----------|------| | 话题被遗漏 | group_news_ai 分组结果 | 检查是否被误分入其它话题组,必要时人工调组 | | 篇幅失控 | 重要性 + 摘要 | 降重要性 + 精炼摘要 | | 合并混乱 | group_news_ai 分组 + dedup_news prompt | 先确认话题组划分是否合理,再收紧 dedup prompt | | 评论敷衍 | 分析师定性 | 补充一句有方向的定性 | | 深度长文消失 | 强制独立 | 设 `强制独立=是` | > **原则**:能在数据层解决的(标注 → 预处理),不要写到 prompt 里。 ### Prompt 调优 所有 prompt 作为独立 `.md` 文件外化在 `prompt/` 目录下,修改后直接生效。(`logs/prompts-{date}.log` 目前**仅 `weekly_summary` 会写**,其余模块不记录 prompt。) 调优流程:运行 → 检查日志定位问题 → 修改对应 `.md` → 重新运行。 --- ## 日志与维护 - **分级日志**:`logs/{error,warn,info,debug}-YYYY-MM-DD.log`,爬虫默认 DEBUG,其余 INFO - **API 记录**:`logs/prompts-{date}.log`(**仅 `weekly_summary` 写入**,其他模块暂无) - **清理**:`python cli.py clean-logs` 删除 30 天前日志 - **归档**:`python cli.py archive` 增量归档到 `data/annotated_news.db`(去重键 = 发布时间 + 信源 + 链接) - **收尾**:`weekend.bat` — 归档(始终)→ 检查分发完整性 → 清空工作目录 → 提示更新时间参数 --- ## 迁移指南(Node.js → Python) 数据格式完全兼容。旧命令对照: | Node.js | Python | |---------|--------| | `politics.bat` | `crawl.bat` | | — | `preprocess.bat` | | `consolidate.bat` | `consolidate.bat` | | `npm run crawl:politics` | `python cli.py news` | | `npm run consolidate` | `python cli.py quick` | | `npm run preprocess` | `python cli.py distribute-news` + `python cli.py group-news-ai` | | `npm run merge` | `python cli.py merge` | | `npm run weekly-summary` | `python cli.py weekly-summary` | | `npm run clean-logs` | `python cli.py clean-logs` | | — | `python cli.py archive` | | — | `python cli.py login-wallstreetcn` | | — | `weekend.bat` |