From e492965ba149ef2e2bdf7c2fabc41bc46980a923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=8E=A8=E7=90=86=E9=93=BE=E4=B8=93=E5=AE=B6?= Date: Tue, 21 Jul 2026 06:52:54 +0000 Subject: [PATCH] =?UTF-8?q?=E4=B8=BA=E6=94=BF=E7=AD=96=E9=9B=B7=E8=BE=BE?= =?UTF-8?q?=20skill=20=E6=B7=BB=E5=8A=A011=E4=B8=AA=E6=94=BF=E5=BA=9C?= =?UTF-8?q?=E7=BD=91=E7=AB=99=E7=9A=84=20Python=20=E6=8A=93=E5=8F=96?= =?UTF-8?q?=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖全部11个数据源的可复用 scraper 模块 tools/scrapers.py: - gov.cn: 国务院政策文件库(HTML 解析) - xinhuanet (news.cn): 新华社受权发布(HTML 解析) - scio (pbc镜像): 国新办新闻发布会文字实录 - pbc: 央行货币政策/公告 - mof: 财政部政策 - ndrc: 发改委产业政策 - csrc: 证监会公告(HTML 解析) - mofcom: 商务部政策(JSON API) - people: 人民日报社论(HTML 解析) - cctv: 央视新闻联播(JSONP API) - ce: 经济日报政策解读 更新 SKILL.md 引用新工具,统一入口 scrape_list()/scrape_article()。 Co-Authored-By: Claude Opus 4.6 Co-authored-by: multica-agent --- .claude/skills/policy-radar/SKILL.md | 371 ++++++++++ .claude/skills/policy-radar/tools/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 221 bytes .../__pycache__/scrapers.cpython-312.pyc | Bin 0 -> 25955 bytes .claude/skills/policy-radar/tools/scrapers.py | 632 ++++++++++++++++++ 5 files changed, 1003 insertions(+) create mode 100644 .claude/skills/policy-radar/SKILL.md create mode 100644 .claude/skills/policy-radar/tools/__init__.py create mode 100644 .claude/skills/policy-radar/tools/__pycache__/__init__.cpython-312.pyc create mode 100644 .claude/skills/policy-radar/tools/__pycache__/scrapers.cpython-312.pyc create mode 100644 .claude/skills/policy-radar/tools/scrapers.py diff --git a/.claude/skills/policy-radar/SKILL.md b/.claude/skills/policy-radar/SKILL.md new file mode 100644 index 0000000..6d7f67a --- /dev/null +++ b/.claude/skills/policy-radar/SKILL.md @@ -0,0 +1,371 @@ +--- +name: policy-radar +description: 政策雷达 — 从中国政府公开信息源抓取最新政策原文,按「李铁四步法」进行结构化深度分析,输出投资方向的判断报告。输入关键词或时间范围,自动搜索→抓取→解读→输出。 +--- + +# 政策雷达 (Policy Radar) + +> 可执行的政策解读智能体:从政策原文抓取到投资方向判断的完整工作流。 +> 基于李铁(铁哥)在中国资本市场的政策解读方法论。 + +--- + +## 1. 角色与目标 + +**角色:** 你是一个政策分析师,专为中国A股市场的投资者提供政策解读服务。你不会凭空猜测——所有判断都基于政策原文。 + +**目标产物:** 一份 `policy-analysis.md`,包含: +1. 本期抓取到的所有相关政策清单(附原文链接/引文) +2. 每条政策的四步法解读 +3. 政策连续性验证(对比上一期分析) +4. 投资方向判断(大方向/中方向/小方向) + +--- + +## 2. 可执行 SOP(六步) + +### 阶段 A: 搜集政策原文 + +**Step 1 — 确认搜索范围。** + +- 用户输入:关键词(如「科技金融」「消费」「房地产」)或时间范围(如「最近一周」「本月」) +- 默认:最近7天所有重要政策 + +**Step 2 — 搜索并抓取政策原文。** + +按以下顺序依次搜索和抓取: + +``` +优先级 1: 中央政府门户 +├── 中国政府网 (www.gov.cn) → 国务院政策文件库 +├── 国新办官网 (www.scio.gov.cn) → 新闻发布会文字实录 +└── 新华社 (www.xinhuanet.com) → 受权发布/时政 + +优先级 2: 部委官网 +├── 中国人民银行 (www.pbc.gov.cn) → 货币政策/公告 +├── 财政部 (www.mof.gov.cn) → 财政政策/预算报告 +├── 证监会 (www.csrc.gov.cn) → 主席讲话/政策公告 +├── 发改委 (www.ndrc.gov.cn) → 产业政策/规划 +└── 商务部 (www.mofcom.gov.cn) → 外贸/消费政策 + +优先级 3: 权威媒体转载 +├── 人民日报 (www.people.com.cn) → 社论/政策解读 +├── 央视新闻 (news.cctv.com) → 新闻联播文字版 +└── 经济日报 (www.ce.cn) → 部委政策解读 +``` + +**搜索方法:** 使用 WebSearch 工具,搜索格式为: +- `site:gov.cn {关键词} {年份}` +- `国新办 新闻发布会 {年份}年{月份}` +- `中共中央政治局会议 {年份}年{月份}` +- `site:scio.gov.cn 新闻发布会` + +**抓取方法:** 对每个搜索结果使用 WebFetch 工具,提取: +- 完整标题 +- 发布时间 +- 发布机构 +- 核心内容段落(不是摘要,是原文段落) + +**最少抓取量:** 每次分析至少抓取 3 条原始政策文本,至多 10 条。如无新政策,显式报告「本期无重大新政策」。 + +**Step 3 — 排除「非政策」。** + +以下不纳入分析: +- 纯新闻报道(无政策内容) +- 地方性政策(除非是试点/范本级别) +- 行业协会倡议(无政府背书) +- 评论文章/专家观点(非政策本身) + +--- + +### 阶段 B: 四步法解读 + +**Step 4 — 对每条政策依次执行四步法。** + +#### 4.1 读原文:定位核心段落 + +从抓取到的原文中,找出以下关键信息: +- **政策目标**:这份文件要解决什么问题? +- **具体措施**:提到了哪些实际手段?(财政补贴/税收优惠/牌照放开/准入限制/金融支持……) +- **措辞强度**:用了什么级别的动词?(「支持」<「推动」<「加快」<「举国」<「志在必得」) +- **量化指标**:有没有具体数字? + +#### 4.2 找新增:与上一期对比 + +将本期政策与上一期分析(如果存在)逐条对比: + +| 对比维度 | 判断标准 | +|----------|----------| +| **新增表述** | 以前没出现过的新词/新概念 → 标记为「新方向」 | +| **排序变化** | 同一议题在不同文件中位置前移/后移 | +| **措辞升级** | 「支持」→「推动」→「举国体制」 | +| **新增措施** | 以前没有的具体手段首次出现 | +| **删除内容** | 以前强调的本期不再提及 | + +**基线建立规则(硬步骤,不可跳过):** + +- **非首次运行**: 将本期政策与上一期 `policy-analysis-{上期日期}.md` 的结论逐条对比,标注每条政策的「新增表述/措辞变化/排序变化/新增措施/删除内容」。 +- **首次运行**: 必须执行「基线扫描」子步骤——搜索过去 3 个月的相关政策(至少覆盖本次关键词领域),建立一份简短的基线摘要(每条政策的核心方向+关键措辞),作为 4.2 的对比锚点。基线摘要写入报告的「基线对比」字段,不得省略。 +- **后续运行输入**: 将上一期 `policy-analysis-{日期}.md` 作为上下文输入,使 4.2 的增量判断可验证,而非依赖 agent 的历史知识。 + +#### 4.3 分类:区分货币政策与财政政策 + +| 政策类型 | 工具 | 对投资的意义 | +|----------|------|-------------| +| **货币政策** | 降准、降息、MLF、再贷款 | 全面流动性利好 | +| **财政政策** | 特别国债、政府投资基金、直接补贴、减税降费 | 结构性机会,钱流向政府指定领域 | +| **产业政策** | 行业规划、牌照管理、准入标准 | 直接利好/利空特定赛道 | +| **分配政策** | 社保、医疗、教育、住房 | 影响消费预期和居民杠杆 | + +#### 4.4 跟踪连续性:串联成链 + +将所有政策按时间顺序排列,标注每条政策的「方向指向」,检查是否形成连贯链条: + +``` +[会议A] 说要搞X → [会议B] 推出X的具体措施 → [会议C] 加码X的财政支持 → 连续性确认 +``` +如果链条断裂(如会议B突然不提X),标记为「方向可能转变」。 + +**跨赛道分支标注**: 当多条政策分属不同赛道但共享同一宏观主题(如绿色转型、扩大内需)时,用分支箭头标注关联关系,避免孤立分析。格式见第 4 节模板中的链条图示例。 + +--- + +### 阶段 C: 输出分析 + +**Step 5 — 生成结构化分析报告。** + +使用以下模板(第4节)生成 `policy-analysis.md`。 + +**Step 6 — 写入文件并汇报。** + +将报告写入工作目录,并向用户汇报核心结论(不超过300字)。 + +--- + +## 3. 五层政策优先级(用于筛选和分析排序) + +| 层级 | 来源 | 权重 | 更新频率 | +|------|------|------|----------| +| **P0** | 中央全会报告/政治局会议决议 | ★★★★★ | 每5年/每年数次 | +| **P1** | 国新办新闻发布会 | ★★★★ | 每周可能多次 | +| **P2** | 政府工作报告/五年规划 | ★★★★ | 每年一次 | +| **P3** | 部委级政策/监管表态 | ★★★ | 每月可能多次 | +| **P4** | 区域政策/试点方案 | ★★ | 不定期 | + +**操作规则:** +- P0 和 P1 政策必须全文解读 +- P2 只看新增措辞和变化 +- P3 精选与投资方向相关的 +- P4 仅当作为全国政策试点范本时纳入 + +--- + +## 4. 输出模板 + +```markdown +# 政策雷达分析报告 — {日期} + +> 分析范围:{关键词/时间范围} +> 政策抓取数量:{数量} 条 +> 基线对比:{本期/上一期日期} + +## 1. 政策清单 + +| # | 政策名称 | 发布机构 | 日期 | 优先级 | 原文链接 | +|---|----------|----------|------|--------|----------| +| 1 | ... | ... | ... | P0 | ... | +| 2 | ... | ... | ... | P1 | ... | + +## 2. 四步法逐条解读 + +### 政策 1: {标题} + +**原文摘要:** {200字以内的原文核心段落} + +**4.1 读原文:** +- 政策目标: ... +- 具体措施: ... +- 措辞强度: {动词+级别} +- 量化指标: ... + +**4.2 找新增(vs 上期):** +- 新增表述: ...(如首次出现则标注「新方向」) +- 措辞变化: ... +- 是否首次提出: 是/否 + +**4.3 政策类型:** 货币/财政/产业/分配 + +**4.4 连续性验证:** +- 与前序政策的关系: {延续/加码/转向} +- 方向一致性: {一致/存疑/转向} + +## 3. 连续性总览 + +``` +{时间线图表,支持分支标注——跨赛道政策关系用分支箭头表示} +``` + +格式要求: +- **主线链条**: 同一议题的政策按时间顺序串联(延续 → 加码 → 转向) +- **分支关联**: 跨赛道政策用分支箭头标注(如环保 ←→ 新能源同属绿色转型),格式:`↗ [赛道名] 政策X: 关联描述` +- **每条节点**: 标注政策编号 + 方向措辞 + 赛道标签 + +示例: +``` +[政策7: 适度宽松+供强需弱] (货币) + ↓ 延续 +[政策6: 宽松+框架改革] (货币) + ↓ 一致 +[政策5: 零碳园区100个] (产业·新能源) + │ ↗ [环保] 政策1: 同属绿色转型链条 + ↓ 一致 +[政策2: 消费60万亿] (财政·消费) + │ ↗ [医疗] 政策3: 同日批复,同属15FYP批量落地 + ↓ 一致 +[政策1: 县乡水体治理] (产业·环保) +``` + +**方向是否恒定:** 是/否 +**转变信号(如有):** ... +**跨赛道关联(如有):** 政策X ←→ 政策Y 同属 {赛道名} 链条 + +## 4. 投资方向判断 + +### 大方向(市场整体) +- 判断: ... +- 依据: {政策编号} 中的 ... + +### 中方向(赛道选择) +- 看好的赛道: ... +- 依据: {政策编号} 中的 ... +- 看空的赛道: ... +- 依据: {政策编号} 中的 ... + +### 小方向(赛道内部) +- 具体机会: ... +- 风险提示: ... + +## 5. 关键词 +`关键词1` `关键词2` `关键词3` ... + +## 6. 发布前自检(内嵌,发布前必须逐项打勾) + +- [ ] **原文可追溯:** 每条政策都有原文链接或原文引文(部分/降级已标注) +- [ ] **四步法齐全:** 每条政策都经过 4.1→4.2→4.3→4.4 四步,缺失项已标注「原文不可得」 +- [ ] **基线对比完成:** 非首次运行时对比了上一期报告;首次运行时执行了 3 个月基线扫描 +- [ ] **方向判断有依据:** 每个投资判断(含看空赛道)都指向具体政策编号 +- [ ] **无臆测:** 所有分析基于原文,推测部分标注「(推测,非原文)」 +- [ ] **优先级标注:** 每条政策标注了 P0-P4 优先级 +- [ ] **降级标注:** 原文获取不完整的政策已添加 ⚠️ 降级标注 +``` + +--- + +## 5. 抓取工具与方法 + +### 5.1 可复用抓取工具(推荐) + +skill 目录下提供了预构建的 Python 抓取模块 `tools/scrapers.py`,覆盖全部 11 个数据源。**优先使用这些工具,而非临时编写 curl 脚本。** + +```python +import sys +sys.path.insert(0, '.claude/skills/policy-radar') +from tools.scrapers import scrape_list, scrape_article + +# 获取某站点的最新列表 +items = scrape_list("gov.cn") # 返回 [{title, date, url}, ...] +items = scrape_list("pbc") # 央行货币政策/公告 +items = scrape_list("people") # 人民日报社论 + +# 抓取某篇文章正文 +article = scrape_article("gov.cn", url) # 返回 [{text}, ...] +``` + +**支持的站点名称:** `gov.cn`, `xinhuanet`, `pbc`, `mof`, `ndrc`, `csrc`, `mofcom`, `people`, `cctv`, `ce`, `scio` + +### 5.2 工具选择 + +| 工具 | 适用场景 | 缺点 | +|------|----------|------| +| **tools/scrapers.py(推荐)** | 直接调用预置抓取器,覆盖11个源 | 网站结构变更时需更新正则 | +| WebSearch | 通用搜索,找最新政策链接 | 依赖配额,可能 403 | +| WebFetch | 获取单个页面全文 | 依赖配额,部分站点证书问题 | +| curl + python3 | 直接 HTTP 请求 + HTML 解析 | 需处理编码和动态页面 | + +**当预置工具不可用时,使用 curl + python3 作为降级方案。** + +### 5.3 原文获取质量与降级标注 + +当抓取工具无法获取完整原文时,**必须显式降级标注**,不得让读者误以为分析覆盖了全文: + +| 获取程度 | 判定条件 | 标注格式 | +|----------|----------|----------| +| **完整原文** | 获取到政策正文 ≥ 5 个核心段落 | 无需额外标注 | +| **部分原文** | 仅获取到摘要、解读页、或 < 5 个段落 | `> ⚠️ 本文基于 gov.cn 政策解读页整理,全文见原文链接` | +| **仅标题/元数据** | 链接不可达或返回 404/空白 | `> ⚠️ 原文不可得(链接失效/需登录),本条基于二手分析,结论可信度降低` | + +**规则:** +- 降级标注写在每条政策的「原文摘要」字段开头。 +- 仅标题/元数据时,该政策的「4.1 读原文」中缺失项标注「原文不可得」,**不得臆测填充**。 +- 原文获取程度直接影响该政策在投资方向判断中的权重——仅标题的政策不应用于看空/看涨的核心依据。 + +### 5.4 抓取注意事项 + +1. **编码**: gov.cn 和 pbc.gov.cn 均使用 UTF-8;curl 直接读取二进制再 decode 即可。 +2. **超时**: 建议 `--max-time 15`,避免慢站点阻塞。 +3. **反爬**: gov.cn 无严格反爬,但连续抓取间隔 1-2 秒为佳。 +4. **动态内容**: gov.cn 政策列表页是服务端渲染,直接 curl 可获取。部委子页面(如 pbc.gov.cn 发文列表)同上。 +5. **404 风险**: 政策 URL 可通过列表页 `/zhengce/` 的 `` 确认,不要猜测 content ID 序列。 +6. **政策解读页**: gov.cn 政策解读(`/zhengce/jiedu/`)的链接格式与政策原文(`/zhengce/content/`)不同,注意区分。 + +--- + +## 6. 数据源 URL 速查 + +| 来源 | URL | 内容 | 工具名 | +|------|-----|------|--------| +| 中国政府网 | https://www.gov.cn/zhengce/ | 国务院政策文件库 | `gov.cn` | +| 新华社 | https://www.news.cn/ | 受权发布/时政 | `xinhuanet` | +| 国新办 | https://www.scio.gov.cn/xwfbh/ | 新闻发布会文字实录 | `scio` (镜像: pbc.gov.cn) | +| 央行 | https://www.pbc.gov.cn/goutongjiaoliu/113456/ | 货币政策/公告 | `pbc` | +| 财政部 | https://www.mof.gov.cn/zhengwuxinxi/ | 财政政策 | `mof` | +| 发改委 | https://www.ndrc.gov.cn/xxgk/ | 产业政策/规划 | `ndrc` | +| 证监会 | https://www.csrc.gov.cn/csrc/c100028/ | 证监会公告 | `csrc` | +| 商务部 | https://www.mofcom.gov.cn/ | 外贸/消费政策 | `mofcom` | +| 人民日报 | http://opinion.people.com.cn/ | 社论/评论 | `people` | +| 央视新闻 | https://news.cctv.com/ | 新闻联播文字版 | `cctv` | +| 经济日报 | http://www.ce.cn/ | 部委政策解读 | `ce` | + +**注意事项:** +- 国新办 (scio.gov.cn) 有 JS 反爬挑战,工具使用央行镜像站获取国新办发布会实录 +- 新华社内容已迁移至 www.news.cn(旧域名 xinhuanet.com 仅有历史内容) +- 人民日报社论版使用 opinion.people.com.cn 子域名 + +--- + +## 7. 发布前自检说明 + +> 自检清单已内嵌到报告模板第 6 节。发布前必须逐项打勾,全部通过方可提交。本节保留各检查项的详细说明供参考。 + +1. **原文可追溯** — 每条政策都有原文链接或原文引文。原文获取不完整时须有 ⚠️ 降级标注。 +2. **四步法齐全** — 每条政策都经过 4.1→4.2→4.3→4.4 四步。缺失项不得臆测填充,应标注「原文不可得」。 +3. **基线对比** — 非首次运行对比上一期 `policy-analysis-{日期}.md`;首次运行执行 3 个月基线扫描并建立基线摘要。 +4. **方向判断有依据** — 每个投资判断(含看空赛道)指向具体政策编号,格式为 `{政策编号} 中的 ...`。 +5. **无臆测** — 所有分析基于原文。推测部分必须标注「(推测,非原文)」。 +6. **优先级标注** — 每条政策在清单中标注了 P0-P4 优先级。 + +--- + +## 8. 反模式(出现即重写) + +1. **凭空解读:** 没有抓取到原文就开始分析 → 必须至少3条原文 +2. **只看标题:** 没有读原文段落就下判断 → 必须读核心段落 +3. **忽略连续性:** 孤立分析单条政策 → 必须串联对比 +4. **混淆政策类型:** 把财政政策和货币政策混为一谈 → 必须分类 +5. **过度推断:** 原文没说但分析中写了 → 删除或标注「(推测,非原文)」 + +--- + +## 9. 一句话调用 + +> 「运行政策雷达,搜索{关键词/时间范围},抓取政策原文,按四步法解读,输出 policy-analysis.md。」 diff --git a/.claude/skills/policy-radar/tools/__init__.py b/.claude/skills/policy-radar/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.claude/skills/policy-radar/tools/__pycache__/__init__.cpython-312.pyc b/.claude/skills/policy-radar/tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d4c2e321599d1934b9f9840d9b099e33560b5a6 GIT binary patch literal 221 zcmZ8bTMEK35Nv#a2!hw}Bd)ehZBaaeR|qjlA)1Y^1oy&^mES06IN1FSI(RIP3lXY_9D)zyqk2YH%o z#5Rv{zAXt(&!xmgyi0IglrrT>w#y0$oRAGE34>4+%EFp!)(Q#ksbRX5d8x~{uncBqVKwLIFXe+GHFAiiUSkR5yV19agz4gdfE literal 0 HcmV?d00001 diff --git a/.claude/skills/policy-radar/tools/__pycache__/scrapers.cpython-312.pyc b/.claude/skills/policy-radar/tools/__pycache__/scrapers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c355e801918e6564d653dd8442335a333ff2b36 GIT binary patch literal 25955 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 literal 0 HcmV?d00001 diff --git a/.claude/skills/policy-radar/tools/scrapers.py b/.claude/skills/policy-radar/tools/scrapers.py new file mode 100644 index 0000000..af63e2a --- /dev/null +++ b/.claude/skills/policy-radar/tools/scrapers.py @@ -0,0 +1,632 @@ +""" +政策雷达 - 政府网站抓取工具集 +每个函数返回标准化格式: {"title": str, "date": str, "url": str, "content": str} +列表函数返回: [{"title": str, "date": str, "url": str}, ...] +""" + +import re +import json +import urllib.request +import urllib.error +import ssl +from html import unescape + +USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" +TIMEOUT = 15 + +# 忽略 SSL 证书验证(部分政府站点使用自签名证书) +_SSL_CTX = ssl.create_default_context() +_SSL_CTX.check_hostname = False +_SSL_CTX.verify_mode = ssl.CERT_NONE + + +def _fetch(url, headers=None): + """通用 HTTP GET,返回解码后的 HTML 文本""" + hdrs = {"User-Agent": USER_AGENT} + if headers: + hdrs.update(headers) + req = urllib.request.Request(url, headers=hdrs) + with urllib.request.urlopen(req, timeout=TIMEOUT, context=_SSL_CTX) as resp: + raw = resp.read() + for enc in ("utf-8", "gbk", "gb2312"): + try: + return raw.decode(enc) + except UnicodeDecodeError: + continue + return raw.decode("utf-8", errors="replace") + + +def _extract_paragraphs(html): + """从 HTML 中提取正文段落(通用)""" + paragraphs = re.findall(r"]*>(.*?)

", html, re.DOTALL) + result = [] + for p in paragraphs: + text = re.sub(r"<[^>]+>", "", p).strip() + text = unescape(text) + if text and len(text) > 10: + result.append(text) + return result + + +# ============================================================ +# 优先级 1: 中央政府门户 +# ============================================================ + +def gov_search(keyword, page=1): + """ + 中国政府网 - 搜索 API(可搜索新闻发布会等) + URL: https://sousuo.www.gov.cn/search-gov/data?t=zhengcelibrary&q=... + 返回 JSON,包含 title/url/summary + 注意: 索引有 1-2 周延迟,最新内容可能尚未收录 + """ + import urllib.parse + q = urllib.parse.quote(keyword) + api_url = ( + f"https://sousuo.www.gov.cn/search-gov/data?t=zhengcelibrary" + f"&q={q}&p={page}&n=20&timetype=timeqb&mintime=&maxtime=" + ) + html = _fetch(api_url) + try: + data = json.loads(html) + except json.JSONDecodeError: + return [] + results = [] + search_result = data.get("searchVO", {}).get("catMap", {}).get("zhengcelibrary", {}) + items = search_result.get("listVO", []) if isinstance(search_result, dict) else [] + for item in items: + title = re.sub(r"<[^>]+>", "", item.get("title", "")).strip() + url = item.get("url", "") + date = item.get("pubtime", "")[:10] if item.get("pubtime") else "" + if title and url: + results.append({"title": title, "date": date, "url": url}) + return results + + +def gov_zhengce_list(): + """ + 中国政府网 - 国务院政策文件库 + URL: https://www.gov.cn/zhengce/ + 结构: 服务端渲染,
  • 标题日期
  • + """ + html = _fetch("https://www.gov.cn/zhengce/") + items = re.findall( + r'
  • \s*]*>\s*([^<]+)\s*\s*\s*([\d-]+)\s*', + html, + ) + results = [] + for url, title, date in items: + title = title.strip() + if not title: + continue + if url.startswith("./"): + url = "https://www.gov.cn/zhengce/" + url[2:] + results.append({"title": title, "date": date, "url": url}) + return results + + +def gov_article(url): + """ + 中国政府网 - 政策文章正文 + 正文在
    内的

    标签中 + """ + html = _fetch(url) + # 提取标题 + title_match = re.search(r'([^<]+)', html) + title = title_match.group(1).strip() if title_match else "" + # 提取发布日期 + date_match = re.search(r"发布时间[::]\s*([\d年\-./]+)", html) + date = date_match.group(1).strip() if date_match else "" + # 提取正文 + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": date, + "url": url, + "content": "\n\n".join(content_paras), + } + + +def scio_press_list(): + """ + 国新办 - 新闻发布会文字实录 + 注意: scio.gov.cn 有反爬保护,此处使用 PBC 站点作为镜像 + PBC 的 "国新办举行新闻发布会" 系列完整保留了国新办原文 + URL: https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html + """ + html = _fetch( + "https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html" + ) + items = re.findall( + r']*href="(/goutongjiaoliu/[^"]+)"[^>]*>([^<]{10,})', + html, + ) + results = [] + seen = set() + for href, title in items: + title = title.strip() + if title and href not in seen and "国新办" in title: + seen.add(href) + results.append( + { + "title": title, + "date": "", + "url": "https://www.pbc.gov.cn" + href, + } + ) + return results + + +def xinhuanet_list(): + """ + 新华社 - 受权发布/时政 + 注意: 使用 www.news.cn(新域名),旧 xinhuanet.com 只有 2021 年旧数据 + URL: https://www.news.cn/ + 结构: 服务端渲染,标题在

    + 文章链接格式: /{channel}/{YYYYMMDD}/{32-hex}/c.html + """ + html = _fetch("https://www.news.cn/") + pattern = r'
    ]*>]*href=[\'"](https?://www\.news\.cn/[^"\']+/20\d{6}/[a-f0-9]{32}/c\.html)[\'"][^>]*>([^<]+)' + items = re.findall(pattern, html) + results = [] + seen = set() + for url, title in items: + title = title.strip() + if title and url not in seen: + seen.add(url) + date_match = re.search(r"/(20\d{6})/", url) + date = "" + if date_match: + d = date_match.group(1) + date = f"{d[:4]}-{d[4:6]}-{d[6:8]}" + results.append({"title": title, "date": date, "url": url}) + return results + + +def xinhuanet_article(url): + """ + 新华社 - 文章正文 + 标题:

    ...

    + 正文:
    内的

    标签 + """ + html = _fetch(url) + title_match = re.search(r']*>.*?]*>([^<]+)', html, re.DOTALL) + if not title_match: + title_match = re.search(r'([^<]+)', html) + title = title_match.group(1).strip() if title_match else "" + # 提取正文 + content_match = re.search(r'id="detailContent"[^>]*>(.*?)

    \s*(?:标题 + """ + html = _fetch( + "https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html" + ) + items = re.findall( + r']*href="(/goutongjiaoliu/[^"]+)"[^>]*>([^<]{10,})', + html, + ) + results = [] + seen = set() + for href, title in items: + title = title.strip() + if title and href not in seen and len(title) > 5: + seen.add(href) + # 从 URL 提取日期 (格式: 2026071518015458230) + date_match = re.search(r"/(20\d{6})\d{11}/", href) + date = "" + if date_match: + d = date_match.group(1) + date = f"{d[:4]}-{d[4:6]}-{d[6:8]}" + results.append( + { + "title": title, + "date": date, + "url": "https://www.pbc.gov.cn" + href, + } + ) + return results + + +def pbc_article(url): + """中国人民银行 - 文章正文(正文在

    标签中)""" + html = _fetch(url) + title_match = re.search(r"([^<]+)", html) + title = title_match.group(1).strip() if title_match else "" + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": "", + "url": url, + "content": "\n\n".join(content_paras), + } + + +def mof_list(): + """ + 财政部 - 财政新闻/政策 + URL: https://www.mof.gov.cn/zhengwuxinxi/caizhengxinwen/ + 结构: 服务端渲染,标题 + 日期 + """ + html = _fetch("https://www.mof.gov.cn/zhengwuxinxi/caizhengxinwen/") + items = re.findall( + r'href="([^"]*t2026[^"]+\.htm)"[^>]*>\s*([^<]{10,80})\s*\s*(?:.*?(\d{4}[-/]\d{2}[-/]\d{2}))?', + html, + re.DOTALL, + ) + results = [] + seen = set() + for url, title, date in items: + title = title.strip() + if title and url not in seen: + seen.add(url) + if url.startswith("./") or url.startswith("t20"): + url = "https://www.mof.gov.cn/zhengwuxinxi/caizhengxinwen/" + url.replace("./", "") + results.append({"title": title, "date": date or "", "url": url}) + return results + + +def mof_article(url): + """财政部 - 文章正文""" + html = _fetch(url) + title_match = re.search(r"([^<]+)", html) + title = title_match.group(1).strip() if title_match else "" + date_match = re.search(r"发布日期[::]\s*([\d年\-./]+)", html) + date = date_match.group(1).strip() if date_match else "" + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": date, + "url": url, + "content": "\n\n".join(content_paras), + } + + +def ndrc_list(page=1): + """ + 发改委 - 产业政策/规划 + URL: https://www.ndrc.gov.cn/xxgk/ + 结构: 服务端渲染,

    + 分页: index_{page-1}.html + """ + if page == 1: + list_url = "https://www.ndrc.gov.cn/xxgk/" + else: + list_url = f"https://www.ndrc.gov.cn/xxgk/index_{page - 1}.html" + html = _fetch(list_url) + items = re.findall( + r']*href="([^"]+)"[^>]*>\s*([^<]{10,})\s*\s*\s*([\d/]+)\s*', + html, + ) + results = [] + for url, title, date in items: + title = title.strip() + if not title: + continue + if url.startswith("./"): + url = "https://www.ndrc.gov.cn" + url[1:] + results.append({"title": title, "date": date, "url": url}) + return results + + +def ndrc_article(url): + """发改委 - 文章正文(正文在 div.TRS_Editor 内)""" + html = _fetch(url) + title_match = re.search(r"([^<]+)", html) + title = title_match.group(1).strip() if title_match else "" + editor_match = re.search(r'class="TRS_Editor"[^>]*>(.*?)
    \s*<', html, re.DOTALL) + if editor_match: + content_paras = _extract_paragraphs(editor_match.group(1)) + else: + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": "", + "url": url, + "content": "\n\n".join(content_paras), + } + + +def csrc_list(page=1): + """ + 证监会 - 主席讲话/政策公告 + URL: https://www.csrc.gov.cn/csrc/c100028/ + 结构: HTML 页面(API 返回空结果,需用 common/searchList 获取 HTML) + HTML URL: /common/searchList/{channelid}?_isAgg=true&_isJson=true&_pageSize=18&_template=index&page={N} + """ + channel_id = "c100028" + url = f"https://www.csrc.gov.cn/common/searchList/{channel_id}?_isAgg=true&_isJson=true&_pageSize=18&_template=index&page={page}" + html = _fetch(url) + items = re.findall( + r']*href="(/csrc/c100028/c\d+/content\.shtml)"[^>]*>([^<]{10,})', + html, + ) + results = [] + seen = set() + for path, title in items: + title = title.strip() + if title and path not in seen: + seen.add(path) + results.append( + { + "title": title, + "date": "", + "url": "https://www.csrc.gov.cn" + path, + } + ) + return results + + +def csrc_article(url): + """证监会 - 文章正文""" + html = _fetch(url) + title_match = re.search(r"([^<]+)", html) + title = title_match.group(1).strip() if title_match else "" + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": "", + "url": url, + "content": "\n\n".join(content_paras), + } + + +def mofcom_list(): + """ + 商务部 - 外贸/消费政策 + URL: https://www.mofcom.gov.cn/ + 结构: 政策列表页使用 JSON API + API: /api-gateway/jpaas-publish-server/front/page/build/unit + """ + html = _fetch("https://www.mofcom.gov.cn/") + # 首页新闻是 SSR + items = re.findall( + r']*href="([^"]+)"[^>]*>\s*([^<]{10,80})\s*', + html, + ) + results = [] + seen = set() + for url, title in items: + title = title.strip() + if title and url not in seen and len(title) > 8: + seen.add(url) + if url.startswith("/"): + url = "https://www.mofcom.gov.cn" + url + results.append({"title": title, "date": "", "url": url}) + return results + + +def mofcom_article(url): + """商务部 - 文章正文(正文在 div.wms-con 内)""" + html = _fetch(url) + title_match = re.search(r"([^<]+)", html) + title = title_match.group(1).strip() if title_match else "" + content_match = re.search(r'class="wms-con"[^>]*>(.*?)
    \s*<', html, re.DOTALL) + if content_match: + content_paras = _extract_paragraphs(content_match.group(1)) + else: + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": "", + "url": url, + "content": "\n\n".join(content_paras), + } + + +# ============================================================ +# 优先级 3: 权威媒体转载 +# ============================================================ + +def people_editorial_list(): + """ + 人民日报 - 社论/观点 + URL: http://opinion.people.com.cn/GB/8213/49160/index.html + 结构: 主页面有多个子栏目,每个子栏目页面有
  • 文章列表 + 文章URL格式: /n1/2026/0623/c223228-40745676.html (相对) 或完整URL + """ + base = "http://opinion.people.com.cn/GB/8213/49160/" + html = _fetch(base + "index.html") + # 获取所有子栏目链接(完整URL) + sub_urls = re.findall(r'href="(http://opinion\.people\.com\.cn/GB/8213/49160/\d+/index\.html)"', html) + sub_urls = list(dict.fromkeys(sub_urls)) + results = [] + seen = set() + for sub in sub_urls: + try: + sub_html = _fetch(sub) + # 解析
  • 项目(注意:人民网的链接使用单引号 href='...') + lis = re.findall(r"]*>(.*?)
  • ", sub_html, re.DOTALL) + for li in lis: + # 匹配单引号或双引号的 href + m = re.search(r"href=['\"](/n1/\d{4}/\d{4}/c[\d-]+\.html)['\"][^>]*>([^<]{10,})", li) + if m: + url_path, title = m.groups() + title = title.strip() + url = "http://opinion.people.com.cn" + url_path + if title and url not in seen: + seen.add(url) + date_match = re.search(r"/n1/(20\d{2})/(\d{4})/", url) + date = "" + if date_match: + year, md = date_match.groups() + date = f"{year}-{md[:2]}-{md[2:]}" + results.append({"title": title, "date": date, "url": url}) + except Exception: + continue + return results + + +def people_article(url): + """人民日报 - 文章正文(正文在

    标签中,class=rm_txt_zw 的容器内)""" + html = _fetch(url) + title_match = re.search(r"([^<]+)", html) + title = title_match.group(1).strip() if title_match else "" + # 正文在 p 标签中 + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": "", + "url": url, + "content": "\n\n".join(content_paras), + } + + +def cctv_list(page=1): + """ + 央视新闻 - 新闻联播文字版 + URL: https://news.cctv.com/lbj/ + 结构: JSONP API + API: https://news.cctv.com/2019/07/gaiban/cmsdatainterface/page/china_{page}.jsonp?callback=china + 注意: 需要设置 Referer 头 + """ + api_url = f"https://news.cctv.com/2019/07/gaiban/cmsdatainterface/page/china_{page}.jsonp?callback=china" + raw = _fetch(api_url, headers={"Referer": "https://news.cctv.com/"}) + # 解析 JSONP: china({...}) + json_match = re.search(r"[^(]+\((\{.*\})\)", raw, re.DOTALL) + if not json_match: + return [] + try: + data = json.loads(json_match.group(1)) + except json.JSONDecodeError: + return [] + items = data.get("data", {}).get("list", []) if isinstance(data.get("data"), dict) else [] + results = [] + for item in items: + title = item.get("title", "").strip() + url = item.get("url", item.get("link", "")) + date = item.get("focus_date", item.get("date", "")) + if title and url: + results.append( + { + "title": title, + "date": str(date)[:10], + "url": url, + } + ) + return results + + +def cctv_article(url): + """央视新闻 - 文章正文(正文在 div.content_area 内)""" + html = _fetch(url) + title_match = re.search(r"([^<]+)", html) + title = title_match.group(1).strip() if title_match else "" + content_match = re.search(r'class="content_area"[^>]*>(.*?)\s*<', html, re.DOTALL) + if content_match: + content_paras = _extract_paragraphs(content_match.group(1)) + else: + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": "", + "url": url, + "content": "\n\n".join(content_paras), + } + + +def ce_list(): + """ + 经济日报 - 部委政策解读 + URL: http://www.ce.cn/ + 结构: 服务端渲染,文章链接格式: /xwzx/gnsz/gdxw/202607/t20260721_xxxxxxx.shtml + 政策相关文章在 /gnsz/ (国内时政) 和 /xwzx/ (新闻) 栏目下 + """ + html = _fetch("http://www.ce.cn/") + items = re.findall( + r']*href="([^"]*t2026[^"]+\.shtml)"[^>]*>([^<]{10,})', + html, + ) + results = [] + seen = set() + for url, title in items: + title = title.strip() + # 过滤政策相关文章(国内时政/新闻栏目) + if "/gnsz/" not in url and "/xwzx/" not in url and "/syj/" not in url: + continue + if title and url not in seen: + seen.add(url) + if url.startswith("/"): + url = "http://www.ce.cn" + url + # 从 URL 提取日期 + date_match = re.search(r"/(20\d{6})/", url) + date = "" + if date_match: + d = date_match.group(1) + date = f"{d[:4]}-{d[4:6]}-{d[6:8]}" + results.append({"title": title, "date": date, "url": url}) + return results + + +def ce_article(url): + """经济日报 - 文章正文(正文在 div.content.clearfix 内)""" + html = _fetch(url) + title_match = re.search(r'id="NewsArticleTitle"[^>]*>([^<]+)', html) + if not title_match: + title_match = re.search(r"([^<]+)", html) + title = title_match.group(1).strip() if title_match else "" + content_match = re.search(r'class="content clearfix"[^>]*>(.*?)\s*<', html, re.DOTALL) + if content_match: + content_paras = _extract_paragraphs(content_match.group(1)) + else: + content_paras = _extract_paragraphs(html) + return { + "title": title, + "date": "", + "url": url, + "content": "\n\n".join(content_paras), + } + + +# ============================================================ +# 统一入口 +# ============================================================ + +SCRAPERS = { + "gov.cn": {"list": gov_zhengce_list, "article": gov_article, "name": "中国政府网"}, + "xinhuanet": {"list": xinhuanet_list, "article": xinhuanet_article, "name": "新华社"}, + "pbc": {"list": pbc_list, "article": pbc_article, "name": "中国人民银行"}, + "mof": {"list": mof_list, "article": mof_article, "name": "财政部"}, + "ndrc": {"list": ndrc_list, "article": ndrc_article, "name": "发改委"}, + "csrc": {"list": csrc_list, "article": csrc_article, "name": "证监会"}, + "mofcom": {"list": mofcom_list, "article": mofcom_article, "name": "商务部"}, + "people": {"list": people_editorial_list, "article": people_article, "name": "人民日报"}, + "cctv": {"list": cctv_list, "article": cctv_article, "name": "央视新闻"}, + "ce": {"list": ce_list, "article": ce_article, "name": "经济日报"}, + "scio": {"list": scio_press_list, "article": pbc_article, "name": "国新办(PBC镜像)"}, +} + + +def scrape_list(site_key, **kwargs): + """统一列表抓取入口""" + scraper = SCRAPERS.get(site_key) + if not scraper: + return [] + return scraper["list"](**kwargs) + + +def scrape_article(site_key, url): + """统一文章抓取入口""" + scraper = SCRAPERS.get(site_key) + if not scraper: + return {"title": "", "date": "", "url": url, "content": ""} + return scraper["article"](url)