rea:用命令行解决碎片笔记管理与检索难题
1. 为什么会有 rea我被自己的碎片笔记逼疯了事情是这样的某段时间我在同时跟几个知识密集型的小项目一个是文献整理一个是选题策划还有一个是给团队写内部教程。每天经手的内容量非常大但真正能沉淀下来的却很少。今天看到一篇好文章复制到备忘录明天读到一段访谈截图发给自己的文件传输助手后天把临时想法随手敲在系统自带记事本里。等到真正要用的时候却什么都想不起来只能一个文件夹一个文件夹地翻越翻越焦虑。于是我决定自己写一个叫 rea 的命令行小工具。名字没什么深意就是 Read Everything Again 的缩写翻译过来是把读过的内容再提取一遍。它的定位很简单不替代任何笔记软件不搞云端同步不做花哨界面只负责把输入内容变成带时间戳、带标签、能全文搜索的记录之后任何时刻输入一个词就能把相关上下文捞出来。reа 适合谁适合像我这样以键盘为生、长期和大量文字材料打交道、需要随时记录和检索笔记的人。如果你也经历过摘抄一时爽找的时候火葬场那这篇文章里的设计思路、踩坑过程和实现细节应该能给你不少参考。1.1 之前的工具链出了什么问题我之前的记录方式很原始一个文件夹里放纯文本笔记文件名等于标题内容想怎么写就怎么写。听起来好像没什么问题但真正用起来全是坑。首先是命名不一致。心情好的时候文件名写《关于需求的几点思考》心情不好的时候直接写《1234》过了一个月我自己都不知道 1234 是什么。其次是内容之间没有关联。一条笔记提到记忆锚点这个概念另一条笔记提到索引卡片我明知它们有关联但纯文本文件不会自动把这两条信息串起来。最后是检索能力太弱。想找上次看到那个关于注意力残留的描述只能靠系统搜索一个文件一个文件地试运气好三分钟运气不好半小时起步。我也试过各种笔记软件。有的软件确实漂亮标签、双链、图谱一应俱全但记录一条想法至少要点五六下鼠标频繁切换窗口特别打断思路。有的软件走极简路线记录很方便可搜索逻辑又太弱只能搜标题搜不到正文。更重要的是我不太想把所有半成品思考放在某个服务里有些内容还处于非常早期的阶段我自己都没想明白价值更不希望被其他机制用奇怪的方式归纳。1.2 对 rea 的功能取舍我先砍掉了什么很多人一上来就想做一个大而全的记录工具我第一版也差点陷入这个陷阱列了十几个功能。但冷静下来才发现真正让我痛苦的不是缺功能而是记录太慢和找不回来这两个点。所以 rea 的第一版直接砍掉了不少东西砍掉图形界面。所有交互都通过命令行完成一条命令几秒钟就能记完完全不需要鼠标。砍掉云同步。数据只存在本地一个文件里没有账号登录没有网络请求工具永远处于可用状态。砍掉双链、脑图、卡片统计这些高级玩法。第一版只做三件事记录、检索、回顾。砍掉编辑器集成。很多笔记工具喜欢做沉浸式编辑但我的需求只是快速捕获不是在一个工具里完成长文写作。这样砍完之后rea 的核心命令只剩三个add负责记录find负责检索digest负责定期回顾。事实证明这三个月度最高的功能撑起了我 80% 的使用场景。2. rea 的核心设计一个命令三种姿势2.1 数据格式目录、文件、还是数据库动手之前我纠结了一阵子数据存储方式。第一种方案是一个目录下放无数个纯文本文件这种方式的好处是文件可读性好用任何编辑器都能打开坏处是检索效率堪忧文件一多索引就难做。第二种方案是干脆用 JSON 存一个大数组实现最简单但数据量上来之后加载速度会变慢而且并发写入容易出问题。我最后选了最常见、也最稳妥的做法用一个本地嵌入式数据库来存。数据表就三张一张存笔记主内容一张存标签一张做笔记和标签的关联表。这样做的理由有三个第一数据库自带索引和聚合查询能力检索几千条笔记毫无压力第二标签关联关系可以用标准表连接实现避免手工维护一堆目录结构第三数据备份只需要复制一个文件迁移成本非常低。表结构大致是这样-- 笔记主表 CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL, content TEXT NOT NULL, source TEXT ); -- 标签表 CREATE TABLE tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ); -- 笔记与标签的关联表 CREATE TABLE note_tags ( note_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (note_id, tag_id) );如果你刚开始做类似的工具不需要一上来就设计得非常复杂但标签独立成表这个习惯越早养成越好。我一开始图省事把标签以逗号分隔存进同一个字段后来为此吃了一个大亏具体在第四章会细说。2.2 三个核心命令的心智模型add很简单就是把一条内容塞进库同时打上标签和来源rea add 标题不重要记忆锚点才重要 -t 方法 -s 某篇行业访谈 rea add 把复杂的问题拆成可验证的小步骤 -t 原则这里我把-s设计成可选参数。因为有些内容来自网页文章有些来自书籍有些纯粹是自己脑子里的念头。没有来源的时候不必强迫自己补上越强迫越不想记。find是真正的价值所在rea find 记忆锚点 rea find -t 方法 rea find 拆解 -t 原则它允许按正文内容搜索也允许按标签过滤。带标签筛选时做的是精确匹配而不是简单的模糊匹配这一点后面会展开讲。digest用于定期回顾。它会输出最近几天内记录的内容按时间倒序排列rea digest --days 3 rea digest --tag 原则 --after 2025-01-01输出格式很简单2025-06-03 21:12 [方法] 标题不重要记忆锚点才重要 2025-06-04 09:05 [原则] 把复杂的问题拆成可验证的小步骤我没有加颜色、没有加进度条、没有加各种花哨的分隔线。命令行工具就该有命令行工具的克制。2.3 一个完整的使用流程实例假设我在读一篇关于拖延症的文章看到里面有句话特别认同马上打开终端rea add 拖延不是因为懒而是因为目标太大导致启动成本太高 -t 心理 -s 一篇关于行动力的文章过两天写选题策划稿需要找一句关于启动成本的素材我输入rea find 启动成本终端立刻把这段话和标签、来源、时间一起列出来。就算我完全不记得原文在哪个平台的哪个链接里只要线索还在就能顺藤摸瓜找回去。我还会每天固定一个时间跑一次rea digest --days 1把当天记的东西扫一遍。这个动作不是整理更像是给自己做一次记忆回放很多当时随手记下的念头会在这种回放里莫名其妙地串成一条线。3. 实现中的关键细节从原型到能长期用3.1 检索功能为什么不用正则硬扛第一版找内容时我确实起过用正则表达式的念头把所有记录读进内存然后逐条用正则匹配关键词。听起来很直接但实际有潜在问题。一是性能。几百条记录时还好一旦过万每次搜索都要全量加载内存和 CPU 都扛不住。二是匹配方式不好控制。正则做的是模式匹配要做包含某个词的搜索需要把关键词转义成普通字符再逐层处理大小写、空格、换行等细节很容易出错。三是后续扩展空间有限如果以后想支持按标签筛选 按时间范围 按关键词组合查询用代码一层层嵌套正则判断就会非常痛苦。我最终的做法是用数据库的LIKE查询关键词作为筛选字段被数据库自身的索引机制处理。核心代码大概是这个模式import sqlite3 from pathlib import Path DB_PATH Path.home() / .rea / library.db def find_notes(keyword, tagNone): conn sqlite3.connect(DB_PATH) sql SELECT DISTINCT n.id, n.created_at, n.content FROM notes n LEFT JOIN note_tags nt ON n.id nt.note_id LEFT JOIN tags t ON nt.tag_id t.id WHERE n.content LIKE ? OR n.source LIKE ? params [f%{keyword}%, f%{keyword}%] if tag: sql AND t.name ? params.append(tag) sql ORDER BY n.created_at DESC; cursor conn.execute(sql, params) return cursor.fetchall()注意这里用的是?占位符而不是把关键词直接拼进 SQL 字符串。这不止是防止注入攻击的问题更重要的是能避免关键词里的%、_这类字符被当成通配符导致明明搜的是普通文本结果却莫名其妙匹配出一堆噪音。3.2 时间戳与回顾功能的设计逻辑digest命令听起来很复杂其实核心就是一个时间过滤查询。我在记录数据时存的是 ISO 8601 格式的本地时间字符串比如2025-06-04 09:05:23这样有几个好处。第一字符串比较大小就能等于时间排序不需要在每次查询时做复杂的日期解析。第二格式直观直接打开数据库文件看数据也能看懂。第三便于后续做统计报表把时间字符串转换成年月日做分组非常简单。digest --days 3的实现原理其实就是在 SQL 里加一个条件from datetime import datetime, timedelta past datetime.now() - timedelta(daysdays) cursor conn.execute( SELECT id, created_at, content FROM notes WHERE created_at ? ORDER BY created_at DESC, (past.strftime(%Y-%m-%d %H:%M:%S),) )为了让digest真正被用起来我把命令设计成可以不带参数运行默认只输出当天的记录。这样每天睡前只要输入rea digest就能快速过一遍。反思自己刚做了一个小工具时总忍不住加各种参数后来才知道默认行为越接近高频使用场景工具被使用的概率越大。3.3 配置文件的演进一开始我把数据库路径和默认时间范围写死在代码里。这没什么不对但有一天我换了一台新的工作设备想把 rea 整个目录挪过去才发现路径写死会产生不少麻烦。后来我加了一个简单的配置文件放在~/.rea/config里用最简单的键值对格式library_path ~/.rea/library.db digest_days 7代码启动时会先读配置文件如果某个键没有设置就使用硬编码默认值。这一步很小但让工具从我自己临时写的脚本变成了可以交给别人使用的正经工具。配置解析不建议自己手写复杂语法除非有特殊需求否则一个赋值符号加一行注释就足够。4. 我踩过的三个坑以及完整的排查思路4.1 坑一中文检索不出来的真凶有一次我在rea find 标题时明明数据库中有一条记录包含标题两个字结果返回为空但用英文关键词搜索又一切正常。这个现象非常诡异我当时第一反应是 SQL 语句写错了但反复检查发现LIKE语法没有任何问题。后面的排查过程是这样的。先在代码里加了一行调试输出把用户输入的关键词和数据库里的内容都打印出来print(repr(keyword)) print(repr(content))结果两个值都显示正常keyword 是标题content 也是包含标题的完整文本。这时候问题被缩小到数据层面看起来没问题但查询就是匹配不上。接着我直接打开数据库手工执行了一条SELECT * FROM notes WHERE content LIKE %标题%发现这条查询是能返回结果的。也就是说同样的 SQL 在数据库交互终端里能查到但通过 rea 工具查不到。至此可以确定问题出在 Python 代码和数据库之间的连接配置上。检查之后发现插入数据之前我对原文做了一次编码转换那个转换过程把中文变成了 Unicode 转义序列比如\u6807\u9898这种字面量而不是真正的标题两个字。所以数据库里存的内容看着是标题但底层字节串可能被错误编码过。数据库终端手工查询时用的是另一套输入自然表现不同。解决方案是重新梳理导入流程在所有入口处统一强制使用 UTF-8 读取输入并且使用参数化查询把原文编码干净后再入库同时对历史数据写了一个小小的迁移脚本把被错误转义的字段解码回正常中文。这个坑给我的教训是遇到中文搜索不出来的问题不要总盯着 SQL 和正则很多时候真正的源头在数据入库那一步的编码转换上。4.2 坑二标签误匹配导致结果噪音太多rea 的第二个版本之前标签一直是存在 notes 表里的一个文本字段内容像方法,原则,心理这样用逗号拼接。后来我发现一个严重问题rea find -t 方法搜出来的结果有一大半根本没有这个方法标签只是正文里碰巧出现了方法两个字。原因非常典型用LIKE %方法%去匹配标签字段本质上是子串匹配而不是精确匹配。标签为方法论的记录能被匹配正文中包含方法两个字但标签完全无关的记录也能被匹配。对用户来说搜索标签的期待是把某个特定主题下的所有笔记找出来而不是把所有提到这个词的内容都找出来。解决这个问题的路径也很清晰放弃标签存一个字段里的设计改成标准关联表结构。查询标签时不再做LIKE而是走精确匹配连接SELECT n.id, n.created_at, n.content FROM notes n JOIN note_tags nt ON n.id nt.note_id JOIN tags t ON nt.tag_id t.id WHERE t.name ?;结构比之前复杂了一点但用户体验回归正常。这里也引申出一个通用原则任何表示归属关系的数据都值得用关联表做精确匹配用逗号分隔字符串看着省事后面一定会为搜索结果付出代价。4.3 坑三误以为丢了数据其实是事务没提交还有一次同事在用 rea 批量导入一批老笔记时程序中途报错退出了。他重新运行rea find发现之前导入的记录一条都不见了。当时我们都以为数据丢了差点直接放弃了数据库方案。后来排查才发现数据库文件还在数据表结构也还在但所有新插入的记录确实没有写进去。原因很简单Python 的数据库连接在开启事务后如果程序在commit()之前抛了异常退出所有未提交的写操作都会自动回滚。排查过程中我做了两步验证。第一步检查数据库文件是否存在以及文件大小有没有变化排除文件损坏问题。第二步用一个新的数据库连接打开同一个文件查询一条刚导入的记录发现确实查不到但没有报出任何错误。这时候我隐约意识到问题不在写没写进去而在写进去之后根本没提交。修复方案很直接在批量导入代码里把所有写入操作包进一个事务上下文with conn: conn.execute(INSERT INTO notes(created_at, content, source) VALUES (?, ?, ?), ...) conn.execute(INSERT INTO note_tags(note_id, tag_id) VALUES (?, ?), ...)这样做的含义是只有块内所有操作都成功数据才会被提交如果中间出现异常整个事务自动回滚不会出现写到一半的脏状态。这个坑给我的启发是任何写数据库的程序都必须把提交事务当成写入流程的一部分来设计而不是写完了才想起来。5. rea 未来不打算做的事以及怎么自己扩展5.1 为什么不做同步、不做浏览器插件很多朋友用了 rea 之后第一个建议是给它加个同步功能吧换了电脑怎么办 我每次都会认真想一下然后仍然决定不加。同步功能一旦引入面对的问题就变成了多设备冲突怎么处理以哪个设备的时间为准离线编辑怎么办存储格式还能不能保持简单这些问题的解决成本远远大于收益。对于单机工具来说换电脑时把~/.rea目录整体打包带走就已经是最稳妥的同步方案。浏览器插件我其实纠结过。毕竟很多记录场景发生在阅读网页时。但如果做了浏览器插件用户就不可避免地依赖浏览器生态每次浏览器更新都可能影响插件行为。相比这个不确定性我更愿意用浏览器快捷键 → 选中内容 → 复制 → 终端执行 rea add这样的流程。缺少一些便利但胜在稳定。5.2 给进阶用户的扩展思路如果你也想做一个类似的命令行笔记工具我建议从这四个方向扩展第一增加 JSON 输出格式。在rea find后面加一个--format json参数输出标准结构化数据这样工具就能被其他脚本调用比如和自动化工作流结合。第二定期生成回顾报告。digest目前只是把最近记录列出来如果再加一个统计功能按标签显示一周内新增笔记数量的分布就能直观看到自己最近在关注什么方向。第三支持从剪贴板直接读取。rea add -c代表从剪贴板读取内容省去复制粘贴到命令行的过程。这个功能我后来补上了使用频率很高。第四导入其他工具导出的文本文件。很多人手里有早年积累的纯文本笔记做一个rea import子命令把旧文件批量入库迁移成本就会低很多。最后分享一个我自己的使用习惯每天睡前我都会在终端跑一次rea digest不用刻意整理只是快速扫一遍当天记下的内容。有时候觉得某条笔记已经不需要了就顺手清理有时候看到两条笔记之间有关联就当场新建一条把它们串起来。这个动作已经成了我日常收尾的一部分比任何花哨的统计报表都管用。