webresearcer 算是我的一个长期维护项目,1 月 25 号这天的更新是一个比较关键的节点:整条 Web 自动化调研流水线第一次完整跑通了。这里简单记录一下这个项目的设计思路、核心模块、实际部署过程,以及我踩过的一些坑。如果你也在做类似的信息采集、内容聚合或者智能简报工具,这篇应该能帮你省下不少试错时间。
1. 项目定位与整体设计思路
1.1 这个工具解决的是“研究过程”的问题
做内容研究、竞品分析、行业跟踪的人都有一个共同痛点:信息源太多,手动一个个打开网页看太费时间。webresearcer 本质上是一个个人化的 Web 调研工具,它的目标不是做一个搜索引擎,而是把“订阅来源、抓取正文、去重归档、生成摘要报告”这整条链路自动化。
一旦跑起来,每天早上只需要看它生成的摘要文件,就能快速知道各个信息源发生了什么变化。项目名里的 webresearcer 就是 web researcher 的意思,既然是内部工具,名字就没太讲究,能用就行。2026-1-25 这个日期是当天的版本记录,我习惯把每次迭代的版本号写成实际日期,方便回溯。
1.2 为什么不用现成的采集框架
很多人第一反应是:这功能用现成的 RSS 阅读器、或者是现成的爬虫框架不就行了吗?其实不行。RSS 阅读器只能解决“订阅”的问题,解决不了“去重”“全文提取”“关键词跟踪”的需求;通用爬虫框架(比如 Scrapy)适合大规模站点采集,但对个人研究场景来说太重了,配置反爬、中间件、调度器一圈下来,可能比写业务逻辑花的时间还多。
webresearcer 的核心设计原则是“够用就好”。它不追求覆盖全网,只追求把我自己关心的一批来源稳定地读进来,然后把重复内容去掉,最后生成一个可读性强的摘要。这个定位决定了技术选型可以非常简单:Python 3 + feedparser + trafilatura + SQLite FTS5,整个项目依赖很少,部署在一台普通服务器上就能跑。
1.3 整条流水线的数据流转
流水线从来源配置开始,到摘要报告结束,中间一共有五步:
- 读取订阅源配置,包括 RSS/Atom 地址和处理规则;
- 拉取新条目,对每个链接抓取正文;
- 对正文做清洗,去掉广告、导航、页脚等干扰;
- 用相似度算法去重,过滤掉已经入库的内容;
- 写入 SQLite,并基于新增内容生成摘要报告。
每一步的输出都是下一步的输入,所以排错的时候思路非常清晰:先看数据到没到,再看数据处理没处理对。实际调试中,90% 的问题都出在“数据没到”这一步,也就是网络抓取异常,这在我后面第四节会单独讲。
2. 核心模块拆解:从抓取到报告
2.1 采集层:RSS 优先,网页兜底
采集层是整个流水线的入口。我采用“RSS 优先,网页兜底”的策略,原因很简单:RSS 是结构化数据,解析稳定、效率高、对目标站点友好。现在很多资讯站虽然不主动宣传 RSS,但只要你把/feed、/rss、/atom.xml这些路径试一遍,大部分都能找到种子地址。
对于没有 RSS 的页面,我写了一个兜底逻辑:直接抓取列表页,用 CSS 选择器提取文章链接。这个逻辑不稳定,因为页面改版就可能失效,所以我对这种来源会额外加一个“健康度”标记,连续三次抓取失败就自动停用并告警。
采集频率也是需要考虑的参数。我默认设置为每 30 分钟轮询一次 RSS,每小时抓取一次非 RSS 来源。这里有一个经验值:不要对同一域名请求太频繁,尤其是有反爬策略的站点,间隔拉长到 5 分钟以上是最低要求。
2.2 清洗层:正文提取与去重
正文提取我用的 trafilatura,这是一个专门做网页正文提取的 Python 库,比传统的 readability 准确率高不少。它的用法很简单,传入 HTML 字符串就能返回正文文本和元数据。实际测试下来,对新闻、博客、技术文档这三类页面的提取效果都很稳,基本不需要正则表达式去修补。
去重是整个项目里最让我头疼的模块。最早我用的是标题完全匹配,结果发现很多网站喜欢在标题后面加站点名,比如“XXX功能上线 - 某某资讯”,同一篇文章在不同来源会有不同标题,完全匹配根本挡不住。后来我改成了 SimHash 相似度算法,把每篇文章转成一个 64 位的指纹,然后比较汉明距离。距离在 3 以内的判定为重复,这个阈值是我跑了三周历史数据后调出来的,误杀率大概在 2% 左右,可以接受。
2.3 索引层:本地全文检索
去重之后的数据会落到 SQLite 里。SQLite 自带 FTS5 全文检索扩展,对小规模个人项目来说完全够用,省掉了部署 Elasticsearch 的麻烦。我建了一个 articles 表和一个对应的 FTS5 虚拟表,每次写入数据时同步更新索引。
全文检索在摘要报告之外的价值是“按人找文章”。比如我想查一下“某个产品”最近一个月被哪些信息源报道过,直接跑一条 SQLSELECT ... FROM articles_fts WHERE articles_fts MATCH '产品名'就能拿到结果。这种临时查询能力,让我在写调研报告时省了很多时间。
2.4 输出层:摘要生成与报告
报告生成我坚持“模板优先,AI 辅助”。为什么不用 AI 全自动写摘要?因为调研场景对准确性要求很高,自动生成的内容可能出现事实性偏差,不核对就发出去容易出事。
我的做法是这样的:先用模板把新增文章按分类整理成列表,包括标题、来源、链接、发布时间和正文的前 200 字;然后对需要深度分析的文章,单独调用本地部署的小模型做摘要。这个模型不追求复杂推理,只做抽取式摘要,也就是从原文里挑最重要的几句话,而不是重新组织语言,这样能最大限度避免“编造内容”的问题。最终产物是一个 Markdown 文件,放在 reports 目录下,按日期命名。
3. 实操过程:从零搭一个能跑的版本
3.1 环境准备
我是在一台 Ubuntu 22.04 的服务器上部署的,配置很低,2 核 4G 就够用。代码用 Python 3.10 编写,依赖就几个:
pip install feedparser trafilatura beautifulsoup4SQLite 用系统自带的版本即可,Python 内置了 sqlite3 模块,不需要额外安装数据库服务。定时任务我用 cron 实现,没有引入 Celery 或者 APScheduler,因为调度需求太简单了,没必要为两行配置引入一个重依赖。
3.2 数据模型与关键代码
数据库设计非常朴素,核心就是两张表。第一张是 sources,记录订阅源信息:
CREATE TABLE sources ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, url TEXT NOT NULL, kind TEXT DEFAULT 'rss', enabled INTEGER DEFAULT 1, last_fetch_at TEXT, fail_count INTEGER DEFAULT 0 );第二张是 articles,记录抓取到的文章:
CREATE TABLE articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_id INTEGER NOT NULL, title TEXT NOT NULL, url TEXT NOT NULL, content_text TEXT, content_hash TEXT, simhash TEXT, published_at TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE VIRTUAL TABLE articles_fts USING fts5( title, content_text, content='articles', content_rowid='id' );FTS5 的content=参数是关键,它让虚拟表和原表建立关联,更新原表数据时可以用触发器自动同步到虚拟表。我写了一个简单的处理函数,负责从抓取到入库的整个流程:
def process_entry(source, entry): # entry 来自 feedparser,里面包含 link、title、published 等字段 if is_duplicate(entry.title, entry.link): return {"status": "duplicate", "title": entry.title} html = fetch_url(entry.link) text = trafilatura.extract(html) if not text: return {"status": "empty", "title": entry.title} sim = simhash(text) if is_similar(sim): return {"status": "similar", "title": entry.title} save_article(source.id, entry, text, sim) return {"status": "ok", "title": entry.title}每一步都返回一个明确的状态,这样我在跑批量历史数据的时候,可以清楚看到每条记录被拦在哪一步。
3.3 调度与参数调优
cron 配置是这样写的,每 30 分钟执行一次采集,每天凌晨 1 点生成报告:
*/30 * * * * cd /opt/webresearcer && python3 collect.py >> logs/collect.log 2>&1 0 1 * * * cd /opt/webresearcer && python3 report.py >> logs/report.log 2>&1采集时间窗口是一个值得调优的参数。一开始我设置为 5 分钟一次,发现大量请求被目标站限流,日志里全是 429 状态码;改成 30 分钟之后稳定了很多,信息及时性也没有明显下降。对个人调研来说,30 分钟已经足够,真有什么重大新闻你肯定先于 RSS 从别处知道了。
去重阈值我也做了几次调整。SimHash 的汉明距离阈值初始设成 5,结果发现不同文章也会被误判为相似,因为技术资讯和新闻稿的用词本来就接近;把阈值降到 3 之后,误判率明显下降。这个参数每个项目都可能不同,建议从 3 起步,跑一周数据后再看准确率。
4. 常见问题与排查实录
4.1 抓取超时与反爬
最频繁的问题是抓取超时。有些 RSS 源很久没更新,服务器响应变得极慢,默认的 10 秒超时根本不够。我遇到过一个源,请求发出后 40 秒才返回数据,占住了采集线程,导致整个队列被堵住。
解决办法是给所有网络请求设置两个超时层:连接超时 5 秒,读取超时 30 秒。trafilatura 底层用的是类似 requests 的会话,可以这样设置:
import trafilatura downloaded = trafilatura.fetch_url(url)但 trafilatura.fetch_url 的超时参数不好控制,所以我改用了 requests 自己做抓取、再做正文提取:
import requests resp = requests.get(url, timeout=(5, 30), headers={ "User-Agent": "Mozilla/5.0 (compatible; webresearcer/1.0)" }) text = trafilatura.extract(resp.text)关于 User-Agent,我的建议是尽量伪装成一个普通的浏览器标识,而不是暴露爬虫身份。有些站点对 UA 很敏感,直接返回 403。当然,这也要遵守目标站的 robots 协议和平台规则,做一个有礼貌的采集者,这是底线。
4.2 正文提取不干净
trafilatura 对大多数页面表现良好,但有几个例外:视频网站的文章页、动态渲染的 SPA 页面、以及带登录墙的付费内容。SPA 页面返回的 HTML 里经常只有一段 JS 代码,trafilatura 提取出来的正文是空字符串。
我补了一个兜底方案:如果提取结果为空或者长度少于 300 字,就尝试用 BeautifulSoup 分析article、main、.content这类常见容器标签。如果还是空,就标记为“需人工查看”,在报告里单独列出来。这套兜底逻辑不算完美,但能把成功率从 80% 提到 92% 左右。
4.3 相似文章误杀
SimHash 在短文本上的表现不如长文本。对于 500 字以下的内容,指纹的区分度变差,两篇完全不相关的短文也可能算成相似。我观察到这个问题后,对短文本走了另一条路:直接用规范化后的标题做精确匹配,只有标题完全相同才判重。
这个策略听起来比 SimHash 简单,但实际操作里更稳。短文本场景本来就是“同一篇稿子被多个源转载”,标题基本一致,精确匹配足够了;等到长文本再上 SimHash 处理“标题不同但内容相似”的深度转载场景,分工明确。
4.4 数据膨胀与磁盘占用
跑了一个月后,我发现数据库文件涨到了 1.5GB 左右。原因有两方面:一是很多文章存储了完整正文,一篇几千字很正常;二是 FTS5 索引本身就占用额外空间。对于个人项目来说,1.5GB 不算大问题,但如果是长期积累,就需要加一个清理策略。
我加了一个简单的归档机制:180 天前的文章只保留标题、URL、摘要和发布日期,正文清空。这样在查询历史时,仍然能看到“什么时候、哪个源、发了什么”,只是无法阅读全文,既控制了体积又保留了检索价值。清理任务同样挂在 cron 里,每月跑一次。
提示:清理前一定要备份数据库。我在上线清理脚本的第三天就吃了亏,一条 delete 语句把昨天的数据误删了一部分,幸好有备份才恢复。备份一条命令就够了:
sqlite3 data.db ".backup backup-$(date +%Y%m%d).db"。
根据我个人的使用体会,webresearcer 这类工具最大的价值不是“抓得多”,而是“沉淀得久”。数据积累到三个月之后,你再去做行业盘点、竞品回顾,会发现以前手动搜索几个小时的内容,现在一条 SQL 就出来了。后续我还计划给它加上分类标签自动生成和跨来源时间轴的功能,让每一篇文章在时间维度上找到自己的位置。如果你也在做类似的东西,我建议先把采集和去重这两块打磨扎实,地基稳了,上层功能都只是时间问题。