多数据源采集的核心是把异构的抓取方式收敛成统一的接口:有官方 API 的源走 HTTP 请求拿 JSON,没有 API 的源用 Axios 抓 HTML 再交给 Cheerio 解析,内容源则走 RSS 订阅。三种方式在项目里对应三个 Collector 实现类,对外暴露同一套 fetch + parse 契约,采集调度器只依赖接口,不关心每个源内部怎么拿数据。聚合层负责把不同源的同一条内容用 URL 哈希去重、按热度权重合并排序,再由 AI 分析层做相关性评分。这套设计让项目后续新增小红书、掘金等源时,只写一个实现文件加一段配置,核心代码零改动。
一、三种数据源接入方式
1.1 官方 API 接入
有稳定公开接口的源优先走 API。Hacker News 直接打 Algolia 搜索接口,按热度排序取 top 条目;B 站公开接口做视频搜索;Twitter 走 TwitterAPI.io 的付费 API 做高级搜索。API 源数据结构干净,返回 JSON,解析成本低、稳定性也好。
const res = await axios.get('https://hn.algolia.com/api/v1/search', {
params: { tags: 'front_page', hitsPerPage: 50 },
});
const items = res.data.hits
.sort((a, b) => b.points - a.points)
.map((h) => ({
title: h.title,
url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,
score: h.points,
publishedAt: new Date(h.created_at),
}));
1.2 网页爬虫接入
GitHub Trending 没有官方 API,只能抓取页面解析 HTML。用 Cheerio 选择仓库列表节点,提取仓库名、描述、编程语言、star 数。搜索引擎类源(Bing、搜狗)同样走爬虫,抓搜索结果页。爬虫源最大的风险是目标站改版,选择器一旦失效整条链就断了,所以爬虫源被安全兜底包裹,解析失败时这一路返回空数组而不是抛出异常拖垮整轮任务。
const html = await axios.get('https://github.com/trending', {
headers: { 'User-Agent': UA_POOL[Math.floor(Math.random() * UA_POOL.length)] },
});
const $ = cheerio.load(html);
$('article h2 a').each((_, el) => {
const repo = $(el).attr('href').trim();
result.push({ title: repo, url: `https://github.com${repo}` });
});
1.3 RSS 订阅接入
中文科技媒体如少数派有原生 RSS,36 氪、虎嗅等没有公开接口的走 RSSHub 中转。RSS 源解析用统一的 feed 解析库,提取 title、link、pubDate。踩过的真实教训是 RSSHub 公共实例经常没数据或限流,上线前把依赖公共实例的源直接停用,改为自建实例后再接入。
1.4 三种方式对比
| 维度 | 官方 API | 网页爬虫 | RSS 订阅 |
|---|---|---|---|
| 数据格式 | JSON,结构清晰 | HTML,需解析 | XML,规则统一 |
| 稳定性 | 高,接口有契约 | 低,改版即失效 | 中,依赖源是否维护 |
| 访问限制 | 常有速率限制 | 需控频率防封 | 低频抓取较宽松 |
| 解析成本 | 低 | 高,选择器易碎 | 低 |
| 典型源 | HN、B 站、Twitter | GitHub Trending、Bing | 少数派、36 氪、虎嗅 |
二、Collector 抽象与配置化
每个数据源实现同一个接口,采集调度器只认识接口不认具体源。
interface Collector {
source: string; // 唯一标识
fetch(): Promise<RawItem[]>; // 抓取原始数据
parse(raw: unknown): RawItem[]; // 转成统一结构
}
const registry: Record<string, Collector> = {
hackernews: new HackerNewsCollector(),
githubTrending: new GithubTrendingCollector(),
sspai: new RssCollector('https://sspai.com/feed'),
// 新增源:写一个实现类,注册进来即可
};
配置化体现在源表驱动:每个源在配置里定义是否启用、抓取间隔、超时时间、单次上限。调度器遍历启用的源并行发起抓取,公共的限速、UA、重试逻辑抽成中间件统一套用,单个源故障只记录日志并跳过,不影响其他源。
三、聚合层的去重与合并
聚合分三步走:
- 对每条结果的 URL 做规范化(去锚点、去追踪参数、统一协议头);
- 取 MD5 生成唯一键,用数据库唯一约束拦截重复插入;
- 命中关键词的条目进入候选集,按源质量加权后交给 AI 分析。
数据源质量分级是聚合层的另一层筛选:arXiv、Hacker News 这类高质量源直接进入候选集,微博、搜索引擎结果这类噪音大的源需要 AI 二次验证才放行。无 AI Key 时降级到启发式模式,按热度阈值和关键词精确匹配粗筛。
四、调度与容错的工程细节
定时任务用 node-cron 驱动,默认 30 分钟一轮,热榜类源单独缩短间隔。容错覆盖四层:请求超时统一 10 秒;失败重试三次且指数退避;数据源级熔断,连续失败超过阈值暂停该源;解析异常只丢弃当前条目。整个采集层最看重的一点是”任何单点故障都不能拖垮整轮”,所有外部依赖都有兜底。
常见问题(FAQ)
Q1:网页爬虫被反爬怎么办?
轮换 UA、控制抓取频率、加代理池;严重受限的源改走第三方聚合接口。
Q2:同一内容被多个源抓到怎么去重?
URL 规范化后取 MD5 建唯一索引,数据库层拦截重复插入。
Q3:新增一个数据源要改多少代码?
只写一个实现 Collector 的类并注册,调度、去重、分析逻辑全部自动生效。