从自研到停服:个人稍后读工具Iris的技术选型与反思
1. 动手写 Iris 之前,我到底被什么问题卡了两年
1.1 收藏过的链接成千上万,真正读过的没几个
我先交代一下背景。过去两三年里,我养成了一个非常坏的习惯:只要看到一篇觉得有用、有点意思、或者只是标题诱人的长文,第一反应不是读,而是先存起来。浏览器书签、笔记软件、稍后读 App、聊天工具里发给自己的链接,到处都有。等真想去读的时候,问题就来了——不是在哪个设备上没同步,就是链接已经 404,又或者当时存的只是一条 URL,连原标题都没留下来,根本想不起来当初为什么要存它。
这种分散,说到底不是工具数量的问题,而是没有一个地方能让我把链接安稳地放下来,并且能在想读的时候很快捞出来。每次想要整理收藏,都像在清理一个塞满了旧文件的柜子:你不知道里面有什么,不敢随便扔,可留着又占地方。更讽刺的是,我的阅读量并没有因为收藏增加而变大,反而越存越不敢打开。后来我才慢慢意识到,这不只是行为问题,更是一个信息管理问题:收藏这个动作太容易,取出和消化的路径太曲折,系统失衡是必然的。
我在决定写 Iris 之前的半年里,尝试过很多方法:定时整理书签、每周清一次 Pocket、把所有链接统一丢进一个笔记文件。每次都坚持不了两周。原因很一致——整理本身变成了另一项待办,而不是在帮我减少待办。我需要的其实不是一个"更强大的收藏夹",而是一个能把我从收藏后遗症里捞出来的工具:它要能把正文存下来、能搜索、能离线读,并且不依赖某个我不想再绑定的账号。
1.2 现成的工具各有各让人劝退的地方
市面上能做的事情很多,我也不想全盘否定它们。这里我把实际用过的几类方案拉出来做个对比,你会看到为什么我最后觉得只有自写一条路足够干净。这个表不针对任何产品,只是我个人的取舍过程。
| 方案类型 | 优点 | 让我放弃的点 |
|---|---|---|
| 浏览器书签 | 零成本、跨设备同步 | 只存 URL,原文消失;整理靠文件夹,搜不了正文;失效链接没人提醒 |
| 老牌稍后读服务 | 抓取稳定、阅读界面好 | 数据在别人服务器上,导出迁移麻烦;免费档功能越收越紧;全文搜索有上限 |
| 笔记软件剪藏 | 和笔记体系打通 | 剪藏结果依赖网页结构,经常只存到一段残缺正文;文件多了之后一样乱 |
| 开源自托管方案 | 数据可控、社区活跃 | 功能覆盖我需求的同时引入很多我用不到的东西,部署不止一个容器,升级像背债 |
我不想点名批哪个产品,但核心问题非常统一:它们要么替你活着(中心化),要么让你替它们活着(自托管但重运维)。我要的其实是一个非常窄的场景——把链接丢进去,得到一份干净的、可搜索的、长期属于我的阅读材料。这个定位听起来很小,实际做起来却没有任何现成方案能完全对齐。这也是 Iris 被写出来的起点。
1.3 动手之前,我先给"理想工具"画了一条边界线
写代码之前,我用一页纸把需求列清楚了。这页纸后来成了 Iris 唯一的长期文档,也是我最后能理性停掉它的依据。当时写的需求只有五条:
- 提供一个入口,把 URL 保存进去,自动抓取标题、正文和关键 metadata。
- 能对正文做全文检索,而不是只搜标题和 URL。
- 提供适合阅读的排版视图,可以离线打开缓存内容。
- 所有数据存在本地,通过文件同步即可备份,不依赖账号体系。
- 不要点赞、评论、标签云、社交推荐、统计面板,这些一律不做。
这五条里没有一条是"我要做得多酷",反而是五条都在说"我要少做什么"。现在回头看,这条边界才是 Iris 能活十个月的原因,也是我最后敢于承认它该停的原因。很多人写工具项目时喜欢不断加功能,好像功能多就等于价值大,但实际上对个人项目而言,每条功能都是一笔运维负债。边界越清楚,项目越健康。这句话我是在 Iris 之后才真正理解的。
2. 十个月里,Iris 经历了三次推倒重来,技术选型是怎么一步步定下来的
2.1 第一版用 Python 快速验证,三个月后我主动弃了
Iris 的第一版非常快就写出来了。那是一个标准的 Python + Flask + SQLite 应用,加一个最简单的爬虫脚本:收到 URL 后,根据 meta 标签提取标题,再用正则去捞正文里的<p>和<article>,跑在树莓派上的内网服务里。两周后它已经能完成"存链接 -> 提取正文 -> 关键词搜索"这条主线。当时我挺兴奋,觉得问题已经解决一半了。
但跑了三个月,问题像暗礁一样浮出来:Python 依赖链太冗长,换一台机器部署要重新折腾虚拟环境;爬虫逻辑随着网站结构变化频繁崩,正则和 BeautifulSoup 混着写,维护成本越来越高。最要命的是,我自己一个月后再去看当时的解析代码,已经不敢改了。这不是 Python 语言的问题,是我的设计问题——一开始只顾着加上传下载,没考虑提取正文这件事的复杂度。到了第四个月,我做出决定:别修补了,重写一遍,换掉整个架构。这个决定比硬修多花了我一个多月,但回过头看,及时推倒重来比带着坏味道继续叠功能要划算得多。
2.2 第二个版本换 Go + SQLite,这是我做过最划算的取舍
重写时我选 Go 和 SQLite,理由非常务实。Go 编译出来是单个二进制,扔到哪都能跑,不需要装依赖、管理 venv、祈祷某个库在特定架构上编译通过。对个人工具来说,部署简单带来的愉悦感远大于某些语法糖。这方面我举个例子:当时我在一台旧笔记本和一台小主机上都跑 Iris,Go 版本发布时只需把二进制复制过去,重启进程,五秒钟完事;对比 Python 版本升级时那串 pip install 和兼容性测试,一个月省下的时间够我干很多其他事。
SQLite 也是同样的逻辑。个人项目的数据规模再大也到不了需要单独数据库服务器的程度,PostgreSQL 那套权限、连接池、备份工具链对一个人来说完全是负担。SQLite 有一个被严重低估的优点:备份就是复制文件。Iris 的所有文章索引、正文、元数据都在一个.db文件里,我用sqlite3 .backup或直接文件同步都能完成整套归档。这个特性在最后停服时帮了我大忙,我后面还会细说。
| 对比项 | Python + PostgreSQL(第一版方向) | Go + SQLite(第二版实际) |
|---|---|---|
| 单机部署 | 依赖多,环境易碎 | 单个二进制,复制即用 |
| 数据备份 | 需要 pg_dump 与恢复演练 | 复制.db文件或使用 SQLite 在线备份 |
| 全文检索 | 要额外接搜索引擎或模糊查询 | SQLite FTS5 已够用 |
| 日常升级 | 依赖变更容易踩坑 | 替换二进制即可 |
选型的核心不是"哪个技术更高级",而是哪个组合在你所处的规模下运维成本最低。以 Iris 这种我一个人用、数据量几千条的场景,Go + SQLite 就是那个最优解。
2.3 抓正文是最大的坑:从正则匹配到可维护的提取管线
做稍后读类工具,最核心也最劝退的环节就是网页正文提取。第一版我用正则匹配<p>,后来发现不同网站的结构差异巨大,有的正文在<article>里,有的在<div>的嵌套里,有的根本没有标准标签。而且很多网站的页面同时包含大量页头、页脚、推荐位、"相关阅读"模块,正则这种招式根本顶不住。
第三次重写时,我把抓取逻辑整理成了一条管线:下载 HTML -> 解析 DOM -> 提取结构化数据(标题、作者、发布时间、meta description)-> 寻找正文节点 -> 清洗成干净的 HTML -> 转成内部阅读格式。正文提取不再靠一条正则,而是用一个带评分的算法:遍历候选节点,根据标签语义、文本长度、链接密度、段落数量给每个节点打分,找出"最像正文"的那块。这样写下来确实比第一版复杂,但它把复杂性收拢到一个模块里,其他部分不用跟着乱。
这里有个实操坑必须提:很多正文在 HTML 里其实是以 JSON 数据埋的,尤其是那些用前端框架渲染的站点,你直接抓到的 HTML 里可能根本没有正文,只有一段包含完整内容的 JavaScript 变量。所以我刚跑通提取管线时做了一个小工具,把同一个站点用不同 UA 抓回的结果并排比对,挑了五个我最常看的网站做对照测试,确认提取结果稳定后才敢拿它当主力阅读工具。对想自建类似工具的朋友,我强烈建议你在第一版就别追求网上所有网站都能解析,先把目标锁定在你自己真正会读的五到十个站上,比做一个看起来全能的空壳有用得多。
2.4 全文检索、中文分词与离线缓存:让数据真正能被用起来
存下来的正文如果不能搜,那它和死文件没什么区别。我一开始天真地以为用 SQLite 的LIKE查询就能对付,数据量小的时候确实能跑,但一旦到了几千条正文,每次搜索都要全表扫描,速度开始变得不可接受。后来 I switched 到 SQLite 的 FTS5 扩展,建了一张虚拟表来索引正文和标题。建表语句大概长这样:
CREATE VIRTUAL TABLE articles_fts USING fts5( title, summary, body, content='articles', content_rowid='id', tokenize='unicode61' );用 FTS5 之后,查询速度上了一个量级,而且它支持 BM25 排序和高亮命中上下文,做稍后读产品完全够用。不过这中间有一个中文用户都会撞上的坑:SQLite 默认的 unicode 分词器对中文处理很弱,它会把一整句中文当成一个 token,搜"读书"搜不到"周末读书计划"。我试过几个现成分词方案,最后选择了一个不引入外部服务的最简做法:入库时对中文文本按长度滑窗切分,形成 2 到 4 字的词组后再写入 FTS 表;英文则按空格和标点切。索引体积因此膨胀了不少,但换来的是本地就能跑的分词效果,不需要启动一个独立的搜索服务。
离线缓存是另一个关键体验。抓下来的正文经过清洗后,我会把处理好的 HTML 放进一个独立磁盘目录,文件名用内容 hash,避免重复抓取。阅读器打开文章时优先读缓存,只有缓存不存在时才重新抓取。这样在网络不稳定的时候依然能看之前存过的文章,也变相降低了对目标网站的抓取频率——这对长期维护很重要,频繁抓取既容易被网站限流,也是一种不太礼貌的打扰。
3. 决定停掉 Iris,真正让我下决心的几个真实临界点
3.1 陌生人的 issue 开始把我从"自己用"推向"做产品"
Iris 本来是给自己用的,但后来我把它开源了,陆陆续续收到一些 star 和 issue。这种外部反馈起初让我很开心,但慢慢开始变味。有人提交 issue 说希望支持多用户,有人说想要完整的浏览器插件,有人希望加一个 Docker 镜像,还有人问能不能做 Android App。这些需求单独看都合理,但对我来说都是额外的维护契约。
我一度被这种"被需要感"拖着走了两三周。那段时间我每天想的不是"这个功能我自己要不要用",而是"提 issue 的人会满意吗",结果代码量涨了两千行,但我的实际阅读时间反而降到了历史最低。后来我才看清楚一个事实:一个个人项目一旦开始为想象中的"用户"做功能,它就容易变成一份没有薪水的工作。对个人工具来说,最重要的用户只有一个,就是你自己。
3.2 目标网站改版和抓取规则失效,是一场永远打不完的仗
我维护 Iris 期间,至少有四五个常看的网站改过版。每一次改版都会让我那套评级算法在某些页面上回归失效:有的站把正文挪进了新的容器,有的开始返回压缩过的 HTML,有的直接上了更严格的风控逻辑。我不得不反复调试,有时为了一个网站的提取规则要花掉整个晚上。抓取库、正文提取、超时重试、处理登录墙,这些在别人做好的商业稍后读服务里是后台团队在扛的事,在个人项目里全部变成我一个人的责任。
最让我清醒的是一个很具体的瞬间:我在排查一个网站上保存失败的问题时发现,那个页面我已经存过三次了,但每次都是因为正文提取失败而存成了残缺内容。那一刻我意识到,Iris 已经从"帮我读书"变成了"让我修 bug"的东西。任何工具,如果它的维护成本超过了它省下的时间,它就在啃你的时间存量。这个不等式会越来越恶化,不可能靠更勤奋的维护逆转。
3.3 移动端体验是自托管类项目最深的坑
作为主要阅读载体,手机对我的使用频率至关重要。但自托管项目天生输在这条线上:我不可能为一个自己用的工具去维护 iOS 和 Android 两个原生应用,也不太可能让所有数据像商业产品那样无缝推送过来。我试过给 Iris 套一个移动端友好的 PWA 外壳,也试过用 iOS 快捷指令把网页分享进来,体验都只能算"凑合能用"。
真正让人绝望的是,移动端的坑是连环的:手机端要阅读流畅,最好有本地缓存,但缓存怎么同步、怎么清理?想推送更新,就要做推送服务,谁来维护?为了让我在手机上看一篇文章,我可能需要同时维护服务器端、前端、抓取器和一套移动端逻辑。这个成本已经和一个小型创业产品的早期版本差不多了。对一个只服务我一个人的项目来说,这个成本不可能长期成立。
3.4 数据涨到一定程度后,架构开始拿我的时间抵债
前面说过,我用 SQLite 是看重它的简单。但"简单"是有边界的。当 Iris 里的文章条数快到一万时,数据库体积连同离线缓存一起膨胀到了几百 MB,FTS 索引占用的空间也在同步增长。备份虽然依然只是复制文件,但复制几百 MB 文件的时间让我开始在意。更要命的是,全文检索在索引量增大后,某些查询的响应明显变慢,我需要开始考虑调优、合并索引、优化 FTS 配置。我承认这些问题都有标准的解法,但标准解法需要时间,而时间对于一个工具项目来说是机会成本。
这里有个很微妙的信号:如果你发现修复技术债比完成自己的工作还要占用精力,这就说明架构规模已经超过了实际需求规模。Iris 的架构没有错,错的是我用一个重服务的形态去承载一个本来可以很轻的需求。当初那五条需求里没有任何一条需要一台永远运行的服务器来完成。
3.5 真正该停的时刻,是我发现自己其实在逃避阅读
最诚实的一条原因是,我在 Iris 上的投入变成了某种拖延:我花大量时间打磨工具,却很少真的用它去读完一篇收藏的文章。我开始统计阅读日志时发现,连续三周里我每天维护 Iris 的时间平均一小时以上,但真正通过它完成深度阅读的次数,每周不超过两次。这个数据让我没法继续骗自己了。
工具项目最舒适的温度是"帮你完成工作",而不是"成为工作本身"。当我对同事提到 Iris 时会说"我在做一个阅读工具",但心里清楚它更像一个逃避阅读的借口。这个项目本身没有错,错的是它在我生活里占据的位置。认清这一点之后,停掉它不再是一个艰难的决定,而是一个必然的收尾。
4. 停服后的收尾动作,以及我给下一轮工具项目立的三条规矩
4.1 收尾的第一步:先把数据完整导出,再谈关闭
真正动手停止 Iris 之前,我做的最重要的一件事不是删库,而是完整导出所有数据。我把文章表、原始 URL、抓取时间、缓存文件全部打包,导出一个 SQLite 快照和一个 JSON 目录,另外把清洗后的 HTML 缓存整个复制到冷存储里。这三样东西合起来大概占了 400 MB,它们代表了我过去十个月的阅读轨迹,就算将来 Iris 不在了,我也随时可以把这些数据导入任何其他工具里。
这个动作我建议所有准备停用个人项目的人都别跳过。数据是你的时间换来的,哪怕当时觉得没价值,将来想要的时候再找就难了。我给它的备份命令写成了一行脚本,原理很简单:sqlite3 iris.db ".backup iris_backup.db",再把缓存目录打一个 tar 包。没有比这更轻的归档方式了。关停服务本身只需要 5 分钟,但导出数据这件事帮我确认了一个心态:我放弃的是一个代码库,不是一段被这些代码记录过的生活。
4.2 仓库归档:我要让后来者一眼看懂这个项目已经"走完了"
我在 GitHub 上将 Iris 仓库标记为只读归档,更新了 README,在项目介绍的开头就写明:这是一个已完成生命周期的实验项目,当前不再接受新功能,不推荐作为生产依赖使用,主要价值是供后来者理解一种可能的实现方案。这样做的原因很简单:我不想让任何陌生人浪费一个晚上,把一个已经过期、带有大量已知缺陷的代码当成还活着的项目来依赖。
归档之后,我给仓库补了一份更清晰的架构说明和配置示例,也保留了我当时用过的提取流程文档。我甚至收到过新 issue,但那都是归档状态之前提交的,我统一回复了"本项目已停止维护"并关闭。这是负责任的做法:开源不是把代码丢出去就不管了,停止维护同样需要把边界划清楚,避免给别人留下一种"还能跑、还能改"的错觉。
4.3 我给自己立的三条规矩,写给下一轮的每一个工具项目
Iris 停掉之后,我花了一些时间复盘,最后沉淀成三条规矩。以后我如果再写工具,一定先过这三条:
规矩一:单机优先,先把一个人的完整流程跑通。不要一上来就做网络服务、账号体系、多端同步。个人项目最危险的起点是"万一别人也要用呢",这个念头的代价极高。先以命令行或本地服务的方式让流程闭环,如果每天真觉得好用,再考虑加一层网罩。
规矩二:每个新功能都要能回答"我每周会用几次"。如果一个功能的答案是一个月一次甚至不确定,就不要做。Iris 里那些 IDE、主题开关、阅读进度统计,最后都没怎么用过,但它们消耗了我大量调试时间。功能数量从来不是价值,使用频次才是。
规矩三:允许项目走到终点。个人工具和开源商业项目不一样,它不是为了永远活着而存在。当项目已经完成它该解决的问题——或者已经明确无法继续帮你解决问题——那么归档和停服不是失败,而是最理性的收尾。比硬撑着维护一个没人需要、自己也不再用的服务,诚实很多。
现在再回头看 Iris,我一点都不后悔当初动手写它。那四千多行代码让我学会了很多:SQLite FTS5 的边界、正文提取的复杂性、个人项目维护的隐性成本,还有什么时候该说停。它停下来之后,我的阅读方案反而退回到了最朴素的一种——一个本地笔记目录加一个全文搜索命令。以前收藏的文章被我一条条清掉了,每周的阅读时间逐渐回来了。对一个工具来说,能让我重新把时间花在内容而不是工具本身上,这大概就是它最后的价值。