数据源筛选与获取是整个 Daily AI Insight Engine 流水线的入口阶段。该阶段需要从数十个异构数据源中,定时、可靠地获取当日 AI 领域的最新文章,并将其转化为统一格式的本地 Markdown 文件,供后续的事实提取与深度分析阶段消费。
核心设计目标:
| 目标 | 说明 |
|---|---|
| 多源异构适配 | 数据源形态各异(RSS Feed、JSON API、静态 HTML、SPA 页面),需统一接入层屏蔽差异 |
| 信号-噪声分离 | 在获取阶段即完成关键词过滤和时效性筛选,避免低质量内容进入下游管道 |
| 确定性 + 幂等性 | 同一篇文章在任意时间、任意次抓取中拥有相同的唯一标识,支持去重和断点续传 |
| 可扩展性 | 新增数据源只需修改配置文件和注册解析器,无需改动编排逻辑 |
该阶段采用 "发现-采集"两阶段分离的架构模式,分为 Scout(清单生成)和 Ingest(正文抓取)两个子步骤,中间以轻量级 JSON 清单文件作为接口。
flowchart TB
subgraph Stage1[阶段一: 数据源筛选与获取]
direction TB
config[("config.yaml<br/>数据源配置")]
subgraph Scout[Step 1: Scout 清单生成]
S1["遍历启用的数据源"]
S2{"fetch_strategy?"}
S3["RSS 抓取"]
S4["API 抓取"]
S5["HTML Scrape"]
S6["Browser 渲染"]
S7["apply_filters()<br/>关键词 + 时效 + 数量"]
S8["生成 SHA-256 唯一 ID"]
S1 --> S2
S2 -->|rss| S3
S2 -->|api| S4
S2 -->|scrape| S5
S2 -->|browser| S6
S3 & S4 & S5 & S6 --> S7 --> S8
end
manifest[("data/00_manifest/<br/>{source}_{date}.json")]
subgraph Ingest[Step 2: Ingest 正文抓取]
I1["读取清单文件"]
I2["去重检查 state.json"]
I3["获取网页 HTML"]
I4["trafilatura 提取正文"]
I5["截断规则处理"]
I6["写入 .md + Frontmatter"]
I1 --> I2 --> I3 --> I4 --> I5 --> I6
end
raw[("data/01_raw/<br/>{source}/01.md ...")]
config --> S1
S8 --> manifest
manifest --> I1
I6 --> raw
end
raw --> Stage2["阶段二: Fact Extraction"]
- 解耦速率瓶颈:Scout 只需拉取文章列表(KB 级),速度快;Ingest 需要逐篇下载并清洗完整网页正文(MB 级),速度慢。分离后,即使 Ingest 中断,已生成的文章也不会丢失。
- 断点续传:manifest JSON 记录当日该数据源的全部待抓取 URL,state.json 追踪已抓取的 article_id。重跑时自动跳过已完成项。
- 可独立调试:可以单独运行 Scout 查看某源的当日文章列表,而不触发全量正文下载。
所有数据源按 "黄金三角"体系 组织为三个层级(Tier A/B/C),对应不同的内容类型和抓取策略权重。
graph LR
subgraph TierA["Tier A: 学术与技术前沿"]
arxiv["arXiv cs.AI<br/>学术论文"]
openai["OpenAI Blog<br/>官方发布"]
deepmind["Google/DeepMind<br/>研究博客"]
anthropic["Anthropic News<br/>官方发布"]
nvidia["NVIDIA Blog<br/>硬件/训练"]
huggingface["Hugging Face<br/>模型/工具"]
end
subgraph TierB["Tier B: 产品与开发者情绪"]
hn["Hacker News<br/>社区讨论"]
ph["Product Hunt<br/>产品发布"]
github["GitHub Trending<br/>开源趋势"]
bensbites["Ben's Bites<br/>AI 日报"]
zhihu["知乎发现<br/>中文社区"]
end
subgraph TierC["Tier C: 商业落地与资本动向"]
tc["TechCrunch<br/>科技新闻"]
verge["The Verge<br/>AI 报道"]
kdnuggets["KDnuggets<br/>教程/新闻"]
tldrai["TLDR AI<br/>每日聚合"]
machineheart["机器之心<br/>中文产经"]
qubit["量子位<br/>中文前沿"]
36kr["36氪<br/>创投报道"]
end
TierA --- TierB --- TierC
每个数据源在 pipeline/config.yaml 中的配置结构如下:
- name: "arxiv-cs-ai" # 唯一标识,对应对应 parser 注册表中的 key
type: academic_paper # 内容类型枚举:academic_paper | tech_blog | news_media | community_discussion
tier: A # 层级:A (核心) | B (重要) | C (补充)
enabled: true # 是否启用,false 时 scout 自动跳过
description: "..." # 人类可读的描述
url: "https://..." # 数据入口 URL
language: en # 语言代码:en | zh
fetch_strategy: rss # 抓取策略:rss | api | scrape | browser
wait_for: ".article-item" # (仅 browser) CSS 选择器,等待元素出现
filter: # 过滤规则
keywords: [...] # 关键词白名单
max_age_hours: 48 # 最大时效(小时)
score_threshold: 100 # (仅 HN) 最低评分阈值
truncation: # 正文截断规则
mode: abstract_only # 截断模式:abstract_only | first_n_chars | none
limit: 3000 # (仅 first_n_chars) 截断字符数
limit: 8 # 每源最大文章数关键设计决策:配置即代码。全部数据源元信息集中在 config.yaml 中声明,config_loader.py 模块提供带缓存的配置读取 API(get_sources(enabled_only=True)、get_source_by_name(name)),运行时不依赖硬编码的数据源列表。
每个数据源通过 fetch_strategy 字段指定抓取方式。Scout 阶段根据该字段将请求分发到对应的处理函数。
flowchart LR
src[("source.fetch_strategy")]
src -->|rss| R["_scout_rss()<br/>feedparser 解析 RSS/Atom"]
src -->|api| A["_scout_api()<br/>请求 JSON API 解析"]
src -->|scrape| S["_scout_scrape()<br/>curl 取 HTML + 专用 parser"]
src -->|browser| B["_scout_browser()<br/>Playwright 渲染 + 专用 parser"]
R --> out["List[dict]<br/>{url, title, published, summary, author}"]
A --> out
S --> out
B --> out
最简单、最可靠的策略。直接解析标准的 RSS 2.0 / Atom feed,适用于大多数博客和新闻站点。
- 技术实现:
fetch_rss_items()使用feedparser库解析 feed,自动处理published_parsed、author_detail等结构化字段,输出标准化字典列表 - 适用场景:OpenAI Blog、Hugging Face、TechCrunch、The Verge 等提供标准 feed 的站点
- 基础设施依赖:curl 子进程 + 环境代理注入
面向提供结构化数据 API 的数据源。当前以 Hacker News 为典型实现。
- 通用处理:
_scout_api()发起 GET 请求,按data.items/data.results路径解析 JSON,提取 URL、标题等字段 - HN 专用增强:
_scout_hackernews()针对 Algolia 搜索结果增加了score_threshold过滤,只保留 points ≥ 阈值的文章,避免大量低质讨论进入管线 - 降级处理:当 JSON 解析失败时返回空列表,不阻塞其他数据源
面向没有 RSS 但提供服务端渲染 HTML 的站点。Scout 获取页面 HTML 后,通过按名称查找的解析器注册表将 HTML 委托给专用解析器处理。
# pipeline/ingestion/parsers/__init__.py 中的解析器注册表
SCRAPE_PARSERS = {
"tldrai": parse_tldrai, # TLDR AI 每日聚合
"anthropic-blog": parse_anthropic, # Anthropic sitemap 解析
"machine-heart": parse_machine_heart, # 机器之心文章列表
}解析器注册表的设计优势:
scout不感知具体解析逻辑,只需按source.name查找对应的 parser 函数并调用- 新增数据源时只需在注册表中加一行映射,无需改动框架代码
- 解析器可返回空列表表达 "未匹配到内容",框架自动跳过该源
代表性解析器:
| 解析器 | 数据源 | 技术方案 |
|---|---|---|
parse_tldrai |
TLDR AI | 正则提取 Headlines & Launches 和 Engineering & Research 板块的 <a> 链接 |
parse_anthropic |
Anthropic Blog | 解析 sitemap.xml,提取 /news/ 路径,按 lastmod 排序取最新 |
parse_machine_heart |
机器之心 | 正则匹配 /articles/YYYY-MM-DD-xxx 路径并去重(已降级为 browser 策略) |
面向现代 SPA(单页应用)站点,这些站点依赖 JavaScript 动态渲染内容,curl 只能拿到空白页面或 loading 骨架。
# pipeline/ingestion/parsers/__init__.py 中的浏览器解析器注册表
BROWSER_PARSERS = {
"zhihu": parse_zhihu_browser, # 知乎发现页
"machine-heart": parse_machine_heart_browser, # 机器之心 SPA 渲染
}- 会话复用:Scout 和 Ingest 阶段分别在入口处理检测是否有 browser 策略的源。如有,提前创建
BrowserSession并在整个阶段中复用(__enter__启动,__exit__关闭),避免为每篇文章重复启动 Chromium - 等待策略:通过
wait_for配置项指定 CSS 选择器,Playwright 等待目标元素出现后再提取 HTML,确保 JS 已完成渲染 - 反检测措施:BrowserSession 默认启用真实 Chrome UA、
bypass_csp、--disable-blink-features=AutomationControlled等参数,降低被反爬虫系统拦截的概率 - 代理支持:自动读取
pipeline/config/proxy.json配置并注入浏览器 context
代表性解析器:
| 解析器 | 数据源 | 技术方案 |
|---|---|---|
parse_zhihu_browser |
知乎 | networkidle 等待后,遍历所有链接,提取 /question/ 和 /p/ 路径 |
parse_machine_heart_browser |
机器之心 | 等待 .article-item 元素,通过 query_selector_all 提取文章链接 |
Scout 阶段获取文章列表后,通过三层过滤器(责任链模式)逐级筛选,确保进入后续管道的内容质量。
flowchart LR
raw[("原始文章列表")]
F1["filter_by_keywords()<br/>关键词匹配"]
F2["filter_by_age()<br/>时效性过滤"]
F3["filter_by_limit()<br/>数量裁剪"]
final[("最终文章列表")]
raw --> F1 --> F2 --> F3 --> final
- 逻辑:标题 + 摘要拼接后统一小写,任一关键词命中即保留
- 空列表语义:当
keywords: []时跳过该过滤器,全量保留(适用于已在前端完成过滤的数据源,如 hnrss 已按 points>=100 预过滤) - 设计考量:关键词列表在
config.yaml中针对每个数据源独立配置,可根据该源的内容特点定制。例如 arXiv 关注算法论文(RLHF、diffusion、multimodal),而知乎关注社区热议(大模型、GPT、Claude)
- 逻辑:
max_age_hours定义文章的最大允许年龄。parse_datetime()支持 7 种常见日期格式(ISO 8601、RFC 2822、简写日期等),解析成功且超时的文章被丢弃 - 保守策略:无法解析日期的文章不予丢弃(宁可多保留也不丢失有效内容)
- 默认值:大多数源设为 48~72 小时,平衡覆盖面和时效性
- 逻辑:
limit字段控制每个数据源每轮最多获取的文章数。limit=0表示不限制 - 设计意图:控制下游 LLM 调用成本(Token 消耗),同时保证报告覆盖多源的多样性而非单一源的信息泛滥
文章 ID 是整个 Pipeline 的贯穿性主键,在 Scout 阶段生成,在所有后续阶段中用于去重和关联。
ID 生成算法: SHA-256(source_url) → hexdigest() → 取前 16 个十六进制字符
| 特性 | 说明 |
|---|---|
| 确定性 | 相同 URL 始终生成相同 ID,天然支持幂等性 |
| 零成本 | 纯数学哈希运算,无需 LLM 调用 |
| 足够唯一 | 64 位哈希空间,10^9 篇文章时碰撞概率 < 10^-10 |
| 阶段一致 | Scout 生成后写入 manifest JSON,Ingest 写入 Frontmatter,Extract 和 Analyze 阶段通过 Frontmatter 读取 |
旧版曾使用 MD5 前 12 位,已迁移为 SHA-256 前 16 位。
_load_state()检测到旧格式时自动重置去重列表(见ingest.py:254-273)。
Ingest 阶段负责将清单中的 URL 转化为带有丰富 Frontmatter 的本地 Markdown 文件。
sequenceDiagram
participant M as Manifest JSON
participant I as ingest.py
participant S as state.json
participant W as web_utils
participant T as trafilatura
participant FS as 文件系统
I->>M: 读取清单,获取 articles[]
loop 每篇文章
I->>S: 检查 article_id 是否已抓取
alt 已抓取
S-->>I: 存在 → 跳过
else 未抓取
I->>W: fetch_url(url)
alt 需要 browser 渲染
W->>W: Playwright 获取渲染后 HTML
end
W-->>I: HTML
I->>T: extract_metadata(html) → 标题/作者/日期
I->>T: extract_article_content(html) → Markdown 正文
T-->>I: {title, author, content, ...}
I->>I: _apply_truncation() 截断正文
I->>FS: write_frontmatter() 写入 .md 文件
I->>S: 将 article_id 加入 seen_hashes
end
end
I->>S: _save_state() 持久化去重状态
进入下游 LLM 处理阶段前,需要对正文进行适当裁剪以控制 Token 消耗。每个数据源可独立配置截断策略:
| 模式 | 行为 | 适用场景 |
|---|---|---|
none |
不截断,保留完整正文 | 评分高、内容精炼的源(如 Hacker News、GitHub Trending) |
first_n_chars |
保留前 N 个字符,在最近段落边界处截断 | 大多数博客和新闻源 |
abstract_only |
仅保留以 > Abstract 开头的 Markdown 块引用段落 |
学术论文源(arXiv),正文冗长但摘要信息密度高 |
段落边界感知截断 (ingest.py:233-239):first_n_chars 模式下,当正文超过限制时,代码会在 limit 处寻找最后一个 \n\n(段落分隔符),在段落边界处截断。如果最近的段落分隔符距离 limit 超过 50% 位置,则退化为硬截断。
每篇抓取的文章写入文件时附带标准化的 YAML Frontmatter,为后续处理阶段提供可被编程访问的元数据:
---
title: "GPT-5: What We Know So Far"
source: "https://example.com/gpt5-article"
author:
- "[[John Smith]]"
published: "2026-05-08"
created: "2026-05-08"
description: "A comprehensive overview of GPT-5 capabilities..."
tags:
- "clippings"
id: "a1b2c3d4e5f6g7h8" # Scout 阶段生成的唯一 ID
---- author 格式:采用
[[Author Name]]的 wiki-link 写法,为未来构建知识图谱和实体链接预留扩展空间 - id 字段:由 Scout 阶段的 SHA-256 ID 贯穿写入,实现跨阶段的文章追踪
- 原子写入:通过
tempfile+rename确保写入过程中进程崩溃不会损坏目标文件
去重状态存储在 data/state.json 中,结构为:
{
"seen_hashes": [
"a1b2c3d4e5f6g7h8",
"b2c3d4e5f6g7h8i9"
],
"last_ingest": "2026-05-08T14:30:00+00:00"
}- 持久化时机:每个数据源处理完毕后立即写入 state.json,而非等全部源处理完再写,防止中途崩溃丢失进度
- force 模式:
--force参数会忽略 state.json 中的去重列表,强制重新抓取全部文章
| 模块 | 文件路径 | 职责 |
|---|---|---|
| 配置加载器 | pipeline/core/config_loader.py |
加载 config.yaml,提供 get_sources()、get_source_by_name() 等查询 API,内存缓存避免重复 I/O |
| Scout 编排器 | pipeline/ingestion/scout/ |
遍历启用的数据源,按 fetch_strategy 分发抓取,调用过滤器,生成 manifest JSON |
| Ingest 编排器 | pipeline/ingestion/ingest.py |
读取 manifest,逐篇抓取正文,去重检查,截断处理,写入 Markdown 文件 |
| 过滤器 | pipeline/ingestion/filters.py |
提供关键词、时效性、数量三个独立过滤函数,被 Scout 调用 |
| 解析器注册表 | pipeline/ingestion/parsers/__init__.py |
维护 SCRAPE_PARSERS 和 BROWSER_PARSERS 两个字典,按 source.name 映射解析器函数 |
| 专用解析器 | pipeline/ingestion/parsers/{source}.py |
各数据源的定制化 HTML/DOM 解析逻辑 |
| 网络工具 | pipeline/core/web_utils.py |
curl 子进程封装、feedparser RSS 解析、trafilatura 正文提取 |
| 浏览器工具 | pipeline/core/browser_utils.py |
Playwright 生命周期管理、页面渲染、代理注入、反检测配置 |
| ID 生成器 | pipeline/core/id_utils.py |
基于 URL 的 SHA-256 确定性 ID 生成 |
| 文件工具 | pipeline/core/file_utils.py |
项目根目录解析、目录创建、原子写入、JSON 读写、序号文件生成、数据目录映射 |
| Frontmatter 工具 | pipeline/core/frontmatter_utils.py |
YAML Frontmatter 读写及标准字段构建 |
| 代理工具 | pipeline/core/proxy_utils.py |
从 pipeline/config/proxy.json 加载代理配置并注入环境变量 |
Scout + Ingest 阶段的最终产出路径与格式:
data/
├── 00_manifest/ # Scout 阶段产出
│ ├── arxiv-cs-ai_20260508.json # JSON 清单,包含 source / tier / articles[]
│ ├── hackernews_20260508.json
│ ├── techcrunch_20260508.json
│ └── ...
│
├── 01_raw/ # Ingest 阶段产出
│ ├── arxiv/
│ │ ├── 01.md # YAML Frontmatter + Markdown 正文
│ │ ├── 02.md
│ │ └── ...
│ ├── bensbites/
│ │ ├── 01.md
│ │ └── ...
│ ├── techcrunch/
│ │ ├── 01.md
│ │ └── ...
│ └── ...
│
└── state.json # 全局去重状态
数据源筛选与获取阶段的核心理念是 "在入口处做好噪声过滤,让下游专注智能分析"。通过以下设计决策实现这一目标:
- 发现与采集解耦:Scout 生成的 manifest JSON 是轻量级的 "任务清单",使得 Ingest 可以安全中断和恢复
- 策略模式统一多源差异:四种
fetch_strategy+ 按名称注册的解析器,将异构数据源的接入逻辑收敛到最小的可替换单元 - 声明式过滤管线:所有筛选规则(关键词、时效、数量)都在
config.yaml中声明,调整过滤策略无需修改代码 - 确定性贯穿主键:SHA-256 ID 从 Scout 阶段生成并贯穿全链,实现跨阶段的文章追踪和去重
- 可扩展架构:新增数据源 = 在
config.yaml中加一条配置 + 在 parser 注册表中加一行映射(仅在需要 scraper/browser 时),无需触碰编排代码