项目代号 REA是我最近完整做下来的一个纯本地运行的阅读辅助工具。REA 并不是什么正式产品的名字它最初只是我在笔记本里随手写的三个字母后来被我自己解释成 Read Extract Anytime——随时抓取正文、随时提取要点。这个工具要解决的说白了就是我攒了两千多条书签却基本没回看过的问题。文章越存越多、入口越散越乱真正需要重读的时候又根本找不到。REA 把整条链路收拢成三件事收集、去重、产出可读摘要全部在本地完成。这篇文章就是我踩完各种浅坑深坑之后的完整记录适合同样被碎片阅读压得喘不过气、又愿意花一点点命令行时间来换长期秩序的人参考。1. 先把需求说清楚REA 准备解决哪些阅读痛点1.1 一切从“收藏了等于读过了”开始我的阅读行为其实没什么特别的早上刷到一篇行业分析觉得写得不错放进稍后读中午同事转了一篇工具教程先存起来晚上又看到同一个主题的另一篇解读也顺手保存。攒了两个多月之后我发现一个很尴尬的事实——我的“稍后读”列表已经超过五百条但真正回头打开的不到十分之一。原因也很简单列表一长就没法排序每一条都得点进去才知道内容是什么打开之后正文被各种弹窗和推荐位挤得七零八落读起来极其浪费时间。这件事让我意识到问题不在于收藏这个动作本身而在于收藏之后没有一个低成本的“回看入口”。如果每条内容能自动变成“标题 摘要 关键词 来源时间”这样一条干净记录我根本不需要点进原网页就能判断要不要精读。REA 这个名字本质上就是对这一需求的回应把网页正文提取出来把重复内容剔掉把最重要的几句话放到最显眼的位置。1.2 拆需求收集、去重、提取摘要一条流水线REA 的核心需求我拆成了三条流水线而不是做一整套复杂的知识管理系统。第一是收集。收集要有统一的入口不能今天靠浏览器书签、明天靠聊天转发、后天靠备忘录。REA 的做法是提供一个最朴素的命令接口丢进来一个 URL或者丢进来一段文本工具就把它存进待处理队列。收集的通道宁可少而稳也不要为了炫技堆一堆没维护的入口。第二是去重。同一篇文章经常被不同平台转发作者改个标题、删掉一段、再换个排版就是“新内容”。这种重复条目如果不去掉摘要列表里会出现大量相似记录直接影响检索效率。去重的重点不是精确匹配而是先规范化地址、再比较正文相似度后面我会专门讲这个实现。第三是提取摘要。这一步是把长文章压缩成三五行可读信息。我的选择是不依赖联网的 AI 接口所有摘要逻辑都在本机通过分词和统计完成。这样运行成本低、速度快也不会有内容被传到第三方的问题。摘要不追求文采只求把关键概念和结论暴露出来读者看到之后能快速决定是否深入阅读。1.3 目标用户与边界设定REA 不是给所有人设计的。它默认使用者愿意打开终端执行一两条命令也接受“没有网页界面、没有花哨仪表盘”这种朴素状态。它是给那种有明确个人知识管理需求、又对数据隐私比较敏感的人准备的。与其花力气做一套多用户云端协作产品不如先把个人场景打透自己存的内容自己处理所有记录都留在本机数据库里随时可以备份随时可以删除。边界设定也很清楚不做推荐算法不做社交分享不做浏览器插件全家桶也不碰云端同步。这是我刻意划出来的线。很多类似工具到最后都变成了“什么都做但每条线都不深”的产品而 REA 只需要在收集、去重、摘要这三件事上做到极致就是最大的意义。2. 技术方案不至于折腾从“最多能做什么”反推到“最少需要什么”2.1 本地优先把数据牢牢抓在自己手里决定用纯本地方案之前我其实对比过几个云端剪藏工具。它们功能确实漂亮能自动识别网页正文、能云端全平台同步、可以多设备打开。但让我犹豫的点始终只有一个这些内容本质上都是我个人积累的资料包含很多半成品的思路和未公开的草稿放在别人的服务器上心里总不踏实。再加上云端服务一旦停止运营多年积累的存档要导出来往往又是一场体力活。本地优先意味着几个实实在在的好处。第一数据文件是一个 SQLite 数据库拷走这个文件就等于完成了整个迁移不需要调用任何导出接口。第二离线状态下所有功能照常运行在地铁上没信号也能给新存的文章生成摘要。第三处理过程完全透明每一步产出什么、写到哪儿了都可以直接查看。这些特性对我的使用习惯来说比“多端实时同步”更有价值。当然缺点也得说清楚本地方案在不同设备之间没有自动同步需要手动备份。我的做法是每周末用一条命令把数据库文件复制到移动硬盘从来没出过问题。如果未来真要多人协作完全可以在这个结构上再接一个同步后端而不是推翻重建。2.2 技术栈Python 加三件套就够REA 最终的技术栈非常简单Python 3 作为主语言requests 负责抓取网页readability-lxml 负责提取正文jieba 负责分词和关键词。存储直接用 Python 标准库里的 SQLite 模块不去额外引数据库驱动。全套依赖加起来不到十个一台普通笔记本跑下来毫无压力。为什么选 Python 而不是别的语言理由很实际生态里已经有人把“网页正文提取”这件最难的事做成了稳定的库我不需要重新发明轮子。readability-lxml 这个库是当年著名阅读器排版算法的 Python 移植版对主流新闻站、博客站、文档站的正文识别效果都相当稳定处理不了的特殊页面还可以自己再写降级逻辑。相比之下如果换成更底层的语言光处理 HTML 解析和编码问题就得花掉大量时间完全偏离了项目目标。为什么不直接用数据库服务因为核心数据量级很小。就算我每周存一百条高质量正文一年也才五千条上下SQLite 单文件在这个量级下查询速度是毫秒级的。引入 MySQL、PostgreSQL 反而增加了部署和维护成本对个人项目来说没有收益。2.3 数据流与表结构设计整个 REA 的数据流是单方向的管道URL 进入队列程序抓取 HTML用 readability 抽出正文接着规范化地址并做去重判断通过去重后分词提取摘要最后把正文、摘要、关键词一起写入数据库。每一步的输出是下一步的输入中间任何一步失败都不会污染数据库只会留下一条待重试的标记。数据库表结构我也尽量精简核心就是一张 articles 表加一张 fetch_logs 表。articles 表记录文章本身字段包括 id、url、normalized_url、title、content、content_hash、summary、keywords、created_at。fetch_logs 表记录每次抓取的结果状态码、耗时、失败原因方便后续排查。建表语句就几行CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, normalized_url TEXT NOT NULL, title TEXT, content TEXT, content_hash TEXT, summary TEXT, keywords TEXT, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE IF NOT EXISTS fetch_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, status TEXT, error TEXT, duration_ms INTEGER, created_at TEXT DEFAULT (datetime(now, localtime)) );这里有一个我后来才想明白的设计点content_hash 字段一定要存它是去重判断的第一道闸门。不同文章哪怕主题相近生成的正文 hash 也几乎不会一样真正重复的转发文章hash 值则完全相同直接用这个字段查一遍就能过滤掉大部分重复。3. 核心模块落地把文章变干净的三个关键步骤3.1 抓取与正文提取readability 是真正的救兵抓取这一步看起来简单实际最容易被“反爬”和“编码”这两件事绊倒。我的做法是尽量让自己看起来像一个普通访客设置合理的 User-Agent带上超时时间遇到 429 状态码就退避重试。下面是 REA 里最基础的一个抓取函数import requests from readability import Document import html as html_lib def fetch_article_text(url: str) - str: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } resp requests.get(url, headersheaders, timeout15) resp.raise_for_status() if not resp.encoding or resp.encoding.lower() in (iso-8859-1, utf-8): resp.encoding resp.apparent_encoding or utf-8 doc Document(resp.text) title doc.title() content_html doc.summary(html_partialTrue) content_text html_lib.unescape(content_html) content_text clear_noise(content_text) return title, content_text这段代码里有几个细节是我实际使用后补上的。一个是 encoding 处理很多网页声明的编码和实际内容不一致尤其是国内一些旧系统页面头部写的是 UTF-8正文却是 GBK不做修正提取出来的全是乱码。另一个是用 apparent_encoding 作为兜底让 requests 根据字节内容猜一次虽然会牺牲一点速度但换来的是正确率。readability-lxml 的核心方法 doc.summary() 返回的是被识别为正文的 HTML 片段。拿到这段片段后还要再做一次清洗比如过滤 script、style、noscript 标签去掉空白符和广告特征类名。我特意写了一个 clear_noise 函数来做这件事它维护一个特征列表遇到带“ad、recommend、related、footer”这类语义的 class、id 就整块删除。实测下来这个函数至少能再把正文体积压缩掉 20%也让最终的摘要质量明显提升。3.2 去重逻辑先规范化地址再计算正文相似度去重看起来应该先做正文比对实际上应该先做 URL 规范化。很多重复内容之所以逃过精确判断就是因为在链接层面带了不同的追踪参数。同一篇文章在来源站和转载站之间的链接差异可能很大但在同域内的追踪参数差异完全可以通过规则清掉。我写了一个 normalize_url专门处理这类问题from urllib.parse import urlparse, urlunparse, parse_qsl def normalize_url(url: str) - str: parts urlparse(url) query_pairs parse_qsl(parts.query) query_pairs [pair for pair in query_pairs if not pair[0].lower().startswith((utm_, spm, from, refer))] from urllib.parse import urlencode new_query urlencode(query_pairs) return urlunparse(( parts.scheme.lower(), parts.netloc.lower(), parts.path.rstrip(/) or /, parts.params, new_query, ))URL 规范化做完之后先查库里有没有相同的 normalized_url有就直接判重复。没有再用 content_hash 判断。内容 hash 我直接取正文前三千字符做 SHA-256既保证速度又不会把整篇长文都算一遍。有些网站会在正文末尾附加一堆转载声明内容主体相同的情况下这些尾部差异会让 hash 变化但影响不大因为它最多造成少量漏判不会造成误杀。如果还想再进一步可以用差分比对来计算正文相似度。我在 REA 里做了个轻量方案把两篇正文按中文标点切成句子然后用相似度算法算出一个重合比例超过 0.85 就认为是同一篇内容的不同版本。这个功能不需要每次都跑只在 content_hash 未命中时作为兜底。对几百上千条数据来说跑一遍也就是几秒钟的事。3.3 摘要与关键词不请大模型也能用的轻量方案如果目标是生成流畅的改写摘要确实得上大模型。但 REA 的目标只是“让用户快速判断这篇文章值不值得读”那就完全可以用更轻的办法。我的实现分两步先用 jieba 做全文本分词统计词频过滤停用词选出 top 关键词再取正文首段、包含最多关键词的段落、结尾段的若干句话组合成一段摘要。这个方案的逻辑基于一个实用假设大多数文章的结论要么在开头给出要么在段落里重复出现的核心关键词附近要么在结尾总结。核心代码可以压得很短import jieba.analyse def make_summary(title: str, content_text: str) - dict: merged title 。 content_text top_kws jieba.analyse.extract_tags(merged, topK8) sentences [s.strip() for s in re.split(r[。!?], content_text) if len(s.strip()) 12] if not sentences: fallback content_text[:200] return { summary: fallback, keywords: top_kws, } first_sentence sentences[0] keyword_sentence for sent in sentences: if any(kw in sent for kw in top_kws[:3]): keyword_sentence sent break last_sentence sentences[-1] summary .join([first_sentence, 。, keyword_sentence, 。, last_sentence]) return { summary: summary[:500], keywords: ,.join(top_kws), }这里有几个经验值得记下来。第一标题一定要参与分词因为标题里的词往往是全文最重要概念的浓缩参与统计后关键词权重会更合理。第二摘要拼接时要去掉首尾多余标点否则会出现“。。”这种低级错误虽然不影响阅读但显得不够专业。第三summary 截断在 500 字以内就够了再长就违背了“提供快速决策信息”的初衷。在实际效果上我对大约两百条新闻、教程、长文类网站做了测试大概七成内容的摘要能准确命中文章核心剩下的三成会出现关键词不够精准的情况。要再提高准确率就得引入语义模型了那已经不是这个轻量工具需要承担的任务。4. 把 REA 接入日常生活命令行、定时任务和浏览器数据迁移4.1 第一条命令初始化数据库并导入文章REA 的交互界面是命令行好在面向个人的工具不需要多复杂的交互两三条命令就能覆盖绝大多数场景。初始化过程我封装成一个 init_db.py 脚本执行后创建数据目录、建好数据库表然后打印出当前路径。首次把文章导入的最直接方法是运行 add_url.py 加一条链接python add_url.py https://example.com/article/2026/nice-read这条命令会依次完成抓取、提取、去重、摘要生成四个步骤然后把结果打印在终端上。如果某一篇内容被判成重复工具会输出“duplicate”并放弃写入不会产生脏数据。为了能随时看到“最近读了什么”我还加了一个 list_recent.py默认打印最近三十天的标题和摘要这个命令成了我每天早上打开电脑后的第一个操作。另外需要特别提醒的是一定不要让抓取过程直接阻塞在交互式终端里太久。我会在命令里加一个 15 秒超时超过就放弃并记录日志。原因很现实有些网址本来就存在连接慢或服务不稳定硬等会让人产生“工具卡死”的错觉。4.2 批量迁移把浏览器书签和稍后读列表导进来一个人真正想回看的文章早就在浏览器收藏夹里躺了不知道多久所以 REA 必须要有一条批量导入通道而不是让人一条条手动粘。所有主流浏览器都支持把书签导出为 HTML 文件这个文件里就是一串嵌套的列表每个书签对应一个这样格式的标记a hrefhttps://example.com/some-article标题/a解析这个文件不需要额外库用标准库的 HTMLParser 就能完成。我在 import_bookmarks.py 里实现了一个精简版本只提取 href 和链接文本然后按顺序回填到待处理队列里。关键的细节是书签文件里除了用户收藏还包括浏览器预置的网址导航、最近访问、甚至图片站列表。导入前我用一个 filter 函数把明显不相关的域名和“javascript:”“chrome-extension:”这类协议排除掉。除了浏览器书签还能导入常见稍后读服务导出的 CSV 文件。这种文件的列通常不统一有的叫 url、有的叫 link我在代码里做了字段名模糊匹配找到包含“url”“link”“http”的列就自动识别。一个看起来简单的导入功能花费的调试时间比我预想的多得多现实世界的导出文件总是有各种意想不到的空值和换行。4.3 定时任务把未读列表自动排个序收集入口打通之后下一步就是让整个流程自动化。我用系统自带的定时任务来运行 REA 的 daily_update.py这套脚本每天凌晨四点半跑一次把近两天新加入的待处理 URL 全部抓取、摘要、入库。在 Linux 或 macOS 上定时任务就是一行 crontab30 4 * * * cd /opt/rea /usr/bin/python3 daily_update.py logs/cron.log 21日志要单独输出到文件里并确保每周清理一次不然积累半年再看会很费劲。Windows 上的做法类似只是换成了“任务计划程序”编辑好触发器和操作路径即可原理没有差别。还有一点必须注意调度频率不能太高。有些站点对短时间内的密集访问会直接封锁 IP所以就算你收藏了一百条链接也不建议在几分钟内全部抓完。我在代码里加了两百毫秒的请求间隔让每次抓取之间留出缓冲时间。这个道理和吃饭差不多狼吞虎咽一顿很容易被中间的一些意外砸到嘴慢慢来反而更稳。5. 实测中常遇到的坑从报错到业务逻辑的排查实录5.1 乱码问题声明编码和真实内容完全对不上最开始我把大量乱码文本存进了数据库源头是编码判断失败。有些站点没有严格遵守 HTTP 头里的 charset 声明内容又混合了多种字符集requests 的默认逻辑拿到的是声明值而不是实际值结果正文提取出来全是问号。排查思路是先看请求响应里的原始字节前几百个判断到底是 UTF-8 还是 GB18030。我后来在代码里加了强制覆写一旦检测到 apparent_encoding 与声明编码不一致就以 apparent_encoding 为准。这个方法不能保证 100% 正确但实测已经可以把成功率拉到百分之九十几。还有一个小技巧是保存原始响应文本之前先打印前五十个字符到日志里乱不乱一眼就能看出来比靠猜可靠得多。5.2 正文提取为空遇到动态渲染和反爬页面怎么办readability-lxml 对服务端直接输出的 HTML 效果很好但对需要 JavaScript 动态渲染的页面基本无能为力。很多现代站点的主页面是一个空壳正文内容全部靠脚本加载。针对这种情况我的策略比较务实先尝试抓取原始 HTML如果 readability 提取出来的正文长度小于两百字就放弃这篇文章并把原因记录到 fetch_logs 里。这不是逃避问题而是明确告诉使用者该页面不适合用 REA 处理以免产生“存了但没完整保存”的错觉。另外一类情况是站点的反爬策略。如果一个源站返回 403、一直要求验证码REA 会重试三次间距分别是两秒、五秒、十秒。重试仍失败就标记为 failed下次定时任务不会再撞同一堵墙。这个设计表面上损失了一小部分来源的抓取实际上保护了整条数据管道不被某个顽固站点拖死。5.3 去重误杀与摘要质量初审过后还要人工复核去重误杀主要发生在两篇相似主题但完全不同作者的文章上。它们标题不一样、正文也基本不同但互相引用了大量相同的段落导致 sentence 重叠比例超过阈值被当成重复。这种误杀的危害在于它会丢掉一篇真正有效的内容比漏过一条重复更让人难受。我的补救办法是把相似度判断的阈值从 0.85 上调到 0.92并对误杀可能性较高的记录单独建一张 review_pending 表。每天跑完定时任务后我会用一条命令查看这张表人工决定吞掉还是保留。摘要质量的问题比较主观。我初期做出来的摘要经常是“开头一句话 结尾一句话”的机械堆叠读起来很别扭。调整权重后把包含关键词最多的中间段落放在第二位明显改善了可读性。如果摘要读下来还是不着边际我会回去看关键词列表通常会发现分词结果里混入了品牌词或特殊名词。把这些词加进停用词表隔一周再跑一次摘要效果就会稳定很多。5.4 问题速查表我把踩过的坑整理成了这张表很多问题在第一次出现时毫无头绪第二次出现时就能直接靠记忆定位了。下面这个表格是我项目运行过程中沉淀下来的一张速查表放在项目根目录下遇到卡壳先查它现象可能原因处理办法抓取后正文全是乱码响应头声明编码与实际内容不一致用 apparent_encoding 覆写或手动指定 GB18030readability 返回空内容页面依赖 JavaScript 渲染标记失败并跳过不强行处理请求返回 403站点反爬识别了无浏览器特征请求设置完整 UA、重试三次仍失败就放弃相同文章入库两次URL 带追踪参数或大小写不一致入库前先调用 normalize_url 规范化摘要读起来像拼凑关键词权重分配不合理把标题加入分词调整段落选择算法定时任务跑了但没新数据定时脚本工作目录与项目不一致在 cron 里显式 cd 到项目目录并加日志这张表帮我在后续的维护中省了大量重复排查的时间。遇到新问题我也会随手补进去几个月下来它已经成了项目的一部分。我用 REA 管理自己的阅读已经有一段不短的时间了回头看最值得分享的不是某个具体算法而是一种思维习惯别急着把所有能用上的技术都堆进去先想清楚自己到底被哪一个环节卡住了。我被卡住的环节是“存了没看、看了没整理”所以 REA 把全部力气都花在收集后处理上。如果你也打算做类似的工具我特别建议先把“重复会以什么样形态出现”想透再动手写代码。最后再分享一个小技巧数据库文件记得放在一个单独的目录里每次改动代码或做备份时都不会误伤数据。工具做得朴素一点没关系能用、稳定、不折腾就已经超过大多数停在收藏夹里的项目了。
阅读完成 · 觉得有帮助?