news 2026/10/24 7:35:01

极简日记应用caveman:纯文本存储与git同步的设计实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
极简日记应用caveman:纯文本存储与git同步的设计实践

每次有人瞄到我电脑里那个叫caveman的文件夹,第一反应基本都是笑:都什么年代了,还拿这么原始的办法记日记?等他们真看清楚里面的东西,又沉默了——三个 Markdown 文件、一个纯文本统计脚本、一套 git 自动提交的备份任务,居然比他们手机里那些推送消息满天飞的日记 App 坚持得更久。这篇就聊聊这个叫 caveman 的极简日记应用:它为什么叫这名字,存储和同步是怎么设计的,核心功能怎么从零长出来,以及我砍掉了哪些主流笔记软件里看起来理所应当的功能。如果你想自己动手做一个类似的、用纯文本管理个人日记的小工具,这篇应该能给你不少实际参考。

1. 为什么我给极简日记应用起了"caveman"这个名字

1.1 一个反讽:caveman 其实是最不"原始"的做法

起名那会儿我正被主流日记应用折磨得够呛。打开一个笔记本应用,第一件事是先等它加载完启动页,再跳过新手引导弹窗,最后面对一排工具栏发呆——字体、颜色、插入图片、思维导图、模板商店。我只是想记一句"今天下雨,心情一般",结果要跟一个功能庞大的客户端先周旋两分钟。

caveman 这个名字其实是带着一点自我解嘲的反讽。洞穴人不需要智能恒温系统,他们只需要一堆干柴和一块燧石就能生火。日记这件事的本源也一样:记录,保留,回头翻看。除此以外的九成功能,都只是数字时代自己给自己制造出来的问题。我把工具砍到只剩干柴和燧石,结果反而发现,这种"返祖"的做法才是最省心的——它不产生新问题,只解决原本的问题。

1.2 这个项目是给"写日记的人"做的,不是给"笔记爱好者"做的

建项目之前我认真想过一个问题:日记(journal)和笔记(notes)的需求本质到底差在哪。笔记是聚合性的,需要分类、标签、双链、全文检索,信息越积越多,检索效率决定工具价值。日记是连续性的,按时间顺序一条条往下写,核心是"今天有没有留下点什么",而不是"过去的知识有没有被索引"。

想清楚这一点之后,caveman 的定位就非常明确了:它不为知识管理场景服务,不跟 Notion 这类工具竞争,它只服务那些想保持记录习惯、但被重型工具劝退的人。典型使用场景是睡前打开终端,敲一行命令,写三五行文字,关闭。整个过程不超过两分钟,和打开手机备忘录差不多快,但数据主动权完全在自己手里。

后来有朋友问我"caveman 适不适合用来记工作日志",我直接说不太适合,你要的是结构化模板和跨星期汇总,那个交给正经笔记软件更合适。工具最怕的不是功能少,而是服务对象不清晰。

2. 存储与同步的"返祖设计":纯文本 Markdown 加本地优先

2.1 为什么是 Markdown 而不是数据库

这是整个项目最重要的决策,也决定了后面所有功能的实现难度。caveman 的存储层就是一层文件系统目录,没有任何数据库,日记本体全是.md文件。

选择纯文本的原因很朴素:可存活时间最长的数据格式,一定是人类可直接阅读的格式。数据库文件依赖软件版本、依赖导出工具、依赖平台兼容性,一旦哪天应用不再维护,导出数据这件事本身就会变成一个麻烦。而 Markdown 文本文件反过来——就算一百年后没有任何软件认识它,用系统自带的记事本打开,剥掉极少数标记符号,内容依然完整可读。

另外一个被很多人忽略的点是,纯文本让"数据所有权"变得具体可感。存在数据库里的日记,你只是拥有使用权;存在目录里的一堆.md文件,你就是字面意义上的拥有者。你可以随时复制、修改、对比、迁移,不需要任何工具的配合。

2.2 目录结构与文件命名:让档案自己说话

caveman 的存储结构长这样:

caveman/ ├── entries/ │ ├── 2025/ │ │ ├── 2025-01-15.md │ │ ├── 2025-02-03.md │ │ └── 2025-03-28.md │ └── 2026/ │ └── 2026-01-01.md ├── stats/ │ └── monthly.md └── index.md

文件命名直接采用 ISO 日期格式YYYY-MM-DD.md,这个选择不是随手定的。它带来两个免费的好处:一是所有系统级的文件排序天然等于时间排序,不需要额外的索引;二是"按某个月写日记"这种最常见的回溯需求,直接变成对目录的一次轻量扫描,连数据库查询都不用写。

每个文件内部的头部结构也保持极简:

# 2025-01-15 天气: 小雨 心情: 3/5 今天终于把拖延了三个月的体检预约做了。 并没有想象中那么难,只是之前一直不想面对那个页面。

日期字段从文件名读取,天气和心情是两个可填可不填的元信息,放在正文第一段位置。没有 YAML front matter,没有复杂的 schema,目的就是为了让人打开文件就能直接写,不要被结构框住。

2.3 同步方案:我用 git 而不是自建云服务

很多人一听说"本地存储"就问:那换电脑怎么办?多设备怎么办?这个问题 caveman 的答案也很原始——用 git。

冲突处理机制也自然有了边界:真正同时编辑同一个文件的概率极低,就算发生了,git 的冲突标记至少能让你手动取舍,而不是眼睁睁看着云端把某个版本悄悄覆盖。

提示:这里其实藏着一个同步思路的转变。不是"我的数据在云端,所以随时能拿到";而是"我的数据在本地,同步只是为了给另一个本地副本搭桥"。前者是中心化的依赖,后者是对等的连接,心理安全感完全不一样。

3. 从需求到落地:核心功能这样一步步长出来

3.1 第一版 MVP 只做了三件事:写、读、数

caveman 第一版命令行界面只实现了三个命令:

命令作用
caveman new创建今天的日记文件,如果已存在则直接打开
caveman show <date>按日期读取某一天的日记,支持模糊匹配
caveman stats统计本月的记录天数和累计字数

我用 Python 写的,核心依赖只有两个:typer负责命令行参数解析,edit依赖系统的$EDITOR环境变量完成文本编辑。没有 GUI,没有后台服务,没有常驻进程。

new命令的逻辑简单得有点原始:

# caveman/cli.py(第一版核心逻辑) @cli.command() def new(date: str = None): target = date or datetime.date.today().isoformat() path = entries_dir() / f"{target}.md" if not path.exists(): # 文件不存在才创建头部信息,已有文件则直接进入编辑 path.write_text(f"# {target}\n\n", encoding="utf-8") # 调用系统的默认编辑器,让用户在不学习新操作的前提下完成输入 subprocess.run([os.environ.get("EDITOR", "vi"), str(path)])

这段代码现在看依然觉得舒服。它没有做任何额外的"贴心"操作,只是确保文件存在,然后把人交给一个已经用了几十年的编辑工具。这其实就是 caveman 哲学在代码层面的延伸——不在用户和文本之间再加一层自己的编辑器。

3.2 安全渲染:被 markdown 库的默认行为坑了一次

当我想给 caveman 加一个--preview参数(在浏览器里预览某一天的日记)时,踩到了第一个真正的坑:Python 生态里常用的 Markdown 渲染库,默认情况下并不会过滤原始 HTML 标签。

这意味着什么?如果你在某一天的日记里粘贴了一段包含<script>标签的内容——可能是复制网页时带进来的,也可能是一时好奇测试——预览时它会被浏览器当作脚本执行。虽然日记是你自己写的,威胁模型很小,但"自己的工具允许执行任意 HTML"这件事本身就不对。

解决办法是两层过滤叠加:

# caveman/render.py(简化版) import mistune, bleach def render_diary(md_text: str) -> str: # 第一步:markdown 转 HTML html_body = mistune.html(md_text) # 第二步:只允许无害标签,其余全部转义 allowed_tags = ["p", "h1", "h2", "h3", "ul", "ol", "li", "blockquote", "code", "pre", "em", "strong", "a"] return bleach.clean(html_body, tags=allowed_tags, strip=True)

bleach.clean会把不在白名单里的标签直接剥掉并转义剩余内容,相当于给渲染通道加了一道保险闸。这件事给我的教训是:任何涉及用户输入的"展示"功能,默认都应当按"不可信输入"来处理,不管这个输入是不是你自己写下的。

3.3 "月度回望":自动生成的统计归档文件

stats命令后来升级成了一项我每天都期待的小功能:每月自动生成一份monthly.md,里面汇总当月写了多少篇、总字数、平均字数、最长的一篇是哪天。生成的逻辑不复杂,本质是对目录做一次前缀过滤:

# caveman/stats.py(核心逻辑) def monthly_stats(year: str, month: str) -> dict: prefix = f"{year}-{month}" # 形如 "2025-07" files = [] # 遍历 entries 目录,匹配 YYYY-MM 前缀 for md in (entries_dir() / year).glob("*.md"): if md.stem.startswith(prefix): files.append(md) total_chars = 0 longest_day = None for f in files: text = f.read_text(encoding="utf-8") chars = len(re.sub(r"\s", "", text)) # 忽略空白字符 total_chars += chars if longest_day is None or chars > longest_day["chars"]: longest_day = {"date": f.stem, "chars": chars} return {"count": len(files), "total_chars": total_chars, "longest": longest_day}

说实话,统计本身没有技术含量,但它对坚持写作的意义比想象中大得多。人是需要正反馈的动物,当你能直观看到"这个月写了 12 天,累计 8600 字",哪怕内容全是流水账,也会有"哦,我其实没断档"的踏实感。caveman 不做花哨的图表,一张纯文本统计表就够了——这也算是我对"功能克制"的又一次践行。

4. 四个被我跳过的"热门功能",以及拒绝它们的理由

4.1 标签系统:目录结构本身已经足够

几乎每个笔记类工具都把标签系统当作标配,但日记场景里我强烈怀疑它的必要性。你写"今天和同事因为项目进度不愉快",你要打几个标签?工作、冲突、情绪?打完三个标签,记录动作已经中断了一次,这会直接伤害写作的连续性。

caveman 的替代方案是让目录结构承担粗粒度的归档:entries/2025/天然按年份分桶,月度回望覆盖了按月统计的需求。那细粒度的主题检索怎么办?答案很简单:用grep。

grep -rn "项目进度" caveman/entries/

一行命令,全目录全文搜索,结果比大多数软件的标签检索还可靠。标签是一种需要刻意维护的元数据,而全文搜索是无状态的瞬时操作。在日记这种"写的时候不要被打断"的场景里,后者显然更合适。

4.2 云同步与端到端加密:别让安全功能成为门槛

有一些开发者朋友看到 caveman 接 git 同步之后,第一反应是"你怎么不做端到端加密"。这个问题我认真想过,最终还是决定不做。

核心矛盾在于:真正防弹的端到端加密,一定伴随密钥管理问题。你用什么保管私钥?如果私钥丢失,意味着所有历史日记都变成不可读的密文——对日记这种"越老越珍贵"的数据来说,这个风险比云端泄露高得多。

当然,完全不加密也不是最优解。我采用了一种折中方案:日记文件本身不加密,但存放目录放在系统级加密卷(macOS 的 FileVault、Linux 的 LUKS)之下。系统层面兜底,工具层面不额外作妖。安全功能的优先级不该高于"数据能不能长期存活"这个根本诉求。

4.3 热力图与连续打卡:警惕数据反噬写作动机

GitHub 的提交热力图带火了一种"连续打卡"文化,很多日记应用也跟进做了"连续写 N 天"的激励。我差点也动了这个念头,后来想通了:打卡统计会悄然改变写作的动机。

当你开始在意"连续天数"这个数字,你就会在某些实在没话说的日子,写出"今天没什么想记录的"这种为打卡而存在的句子。这些句子没有记录价值,只会制造一种虚假的高产幻觉。更糟的是,一旦哪天真忘了写,连续记录断掉,很多人的第一反应不是"无所谓,继续写",而是"断了就不想补了"——整条习惯链路随之崩塌。

caveman 的stats只展示累计趋势,不展示连续状态。这件事没有写在代码注释里,但它是我有意为之的设计取舍:工具能给的最小激励,就是诚实的诚实本身。

4.4 移动端 App:物理隔离反而保护了写作仪式感

关于手机端,我做了个看起来完全不"方便"的决定:不做 App,在手机上也不看日记,只管写。甚至我故意把同步仓库放在一个手机不太容易访问的位置。

这种"不方便"是有意的。夜深人静想写点什么的时候,打开电脑,进入终端的过程本身就是一种心理仪式——它把一个琐碎的记录动作从"刷手机时顺手记一笔"变成了"正经坐下来写几分钟"。手机端的随手可用会稀释这种仪式感,把日记降格成即时消息。对我个人来说,caveman 的价值恰恰在于它保留了那点"需要专门腾出时间"的门槛。

5. 踩坑记录:纯文本方案也有三道坎

5.1 中文文件名与 UTF-8 编码暗坑

因为核心内容都是中文,编码问题从一开始就绕不开。第一版代码里我犯过一个低级错误:在 Windows 环境测试时,把文件名按系统默认的 GBK 编码处理了一版,导致同一批日期文件在跨平台 git 操作时出现乱码,直接让同步仓库变得一塌糊涂。

教训其实非常基础:所有涉及文件读写的地方,显式声明encoding="utf-8",绝不依赖系统默认值。上面代码里每次write_text和read_text我都写过encoding参数,这词看起来啰嗦,但它能省掉一整类跨平台问题。

至于文件名本身,用纯数字加连字符的2025-01-15格式天然避开所有编码陷阱——这也是我建议其他人做类似工具时坚持日期命名的原因之一。

5.2 写入中断导致文件清空:原子写方案

另一个更隐蔽的坑出现在编辑器保存环节。最初我用最朴素的方式写文件:

content = editor_output() path.write_text(content, encoding="utf-8")

听起来没问题,但假如编辑器输出内容的过程中进程被中断(比如系统崩溃、不小心关了终端),文件可能处于被截断的状态,原本的日记内容直接丢了。尤其可怕的是,这种"丢失"往往要到下次打开文件时才发现。

修复方案是标准的原子写:

# 原子写:先写临时文件,再替换原文件 import os, tempfile def atomic_write(path: Path, content: str): fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp") try: with os.fdopen(fd, "w", encoding="utf-8") as f: f.write(content) os.replace(tmp_path, path) # 同一个文件系统内替换,原子操作 except Exception: os.unlink(tmp_path) # 出错时清理临时文件 raise

os.replace在同一个文件系统内的替换是原子性的,要么旧的完整保留,要么新的完整生效,不会出现半写状态。所有日记写入现在都走这条路径,一次崩溃都没再出现过。

5.3 编辑器与渲染层的边界:源文件永远是唯一权威

最后一个坑跟架构原则有关。早期我为了实现"实时预览",曾经尝试在编辑器之外自己维护一份格式化后的 HTML 副本,结果很快发现这会造成双份数据的不一致——有一次我在 HTML 副本上做了编辑,却忘记反写回 Markdown 源文件,直接丢了两天的日记内容。

这件事让我立下一条硬规则:caveman 里源文件永远唯一权威,渲染结果只是临时视图,绝不反写。编辑器只负责编辑.md源文件,预览功能每次使用都现场渲染,不缓存任何中间态。这条规则看着简单,但决定了整个项目后续每一个功能都往"如何不污染源数据"上想,而不是往"如何增加一套新数据"上想。

6. 实测两个月:极简日记到底改变了什么

6.1 数据说话:两个月的使用统计

从 11 月中旬到 1 月中旬,我几乎每天都在用 caveman 记录。实际的生成数据长这样:

月份记录天数累计字数平均单篇字数最长一篇
2025-11(后半)14 天82305881120
2025-1226 天154205931975
2026-01(前半)14 天7610544830

对比我此前用主流笔记 App 的三个月:App 里总共只有 11 篇日记,而且多数是"为打卡而写"的碎句。caveman 的单篇字数未必更高,但它让我真正养成了"每天睡前留十分钟"的节奏。说到底,工具能帮的最大忙,就是把记录动作的摩擦成本压到最低,不让我在"要不要记"这个决策上多犹豫一秒钟。

6.2 极简界面带来的心理变化:写给自己的字,不需要被别人评分

以前用笔记应用,每篇日记旁边都有字数、日期、标签、可能还有点赞数。这些数字堆积在一个界面上,形成一种隐形的评价压力——哪怕没有人在看,你也会觉得自己在"被评分"。

caveman 的界面里什么装饰都没有,只有一个终端光标。这种无评价感带来的实际改变是:我逐渐敢在日记里写一些不完整的、碎片化的、甚至逻辑不通的句子。反正没有热力图会为它们标红,没有标签系统等着给它们归类,它们就是纯粹写给未来的自己看的。写作的自由度,比功能丰富的 App 时代高了不少。

6.3 仍然想加的功能,以及加功能前的最后一道安检

两个月的使用让我摸清了真正的需求边界。现在最想加的不是标签,也不是图表,反而是一个更朴素的"导出打印"支持——把某个月的日记拼成一个干净的 PDF,方便年底装订成册。这个需求是把"档案感"延伸到物理世界,跟极简理念不冲突。

不过每次动手前我都会过一道安检:这个功能会不会增加"写下第一笔"的摩擦?如果会,直接砍掉。用这个标准去看,大部分花哨功能在第一轮就会被淘汰,剩下的都是真正为"记录"本身服务的东西。

写到这里我想起项目最早的那行描述:"caveman, a tiny markdown diary for people who just want to write." 两个多月下来,它确实做到了。如果你也想搭一个类似的工具,我的建议是从一行命令开始,从一月的 Markdown 开始,别急着规划数据库,也别在上面加任何未来你可能根本不会用的功能。先写起来,比什么都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 20:11:00

基于机器学习的Web日志异常检测工具:配置、实战与避坑指南

简介&#xff1a;这是一套面向安全运维与日志分析学习者的命令行Web日志审计工具&#xff0c;基于Python实现&#xff0c;将日志统计、终端可视化与机器学习恶意请求识别整合在一起&#xff0c;适合具备Python基础、希望上手日志审计与异常检测实战的开发者。资源包共63个文件&…

作者头像 李华
网站建设 2026/10/14 3:22:08

1982-2024年地级市逐月气温面板:ERA5再分析数据处理与实证研究全流程

简介&#xff1a;这份资源为1982至2024年地级市逐月平均气温数据集&#xff0c;面向气象气候研究、区域经济实证与深度学习建模等场景的科研人员和数据分析者&#xff0c;用于分析城市气候长期变化规律、支撑面板数据回归与时间序列建模。压缩包共13个文件&#xff0c;以xml表格…

作者头像 李华
网站建设 2026/10/24 0:18:24

识别钓鱼源码包:二手平台仿冒页面的伪装与防范

简介&#xff1a;这是一份以二手交易平台为蓝本、仿转转和闲鱼界面的完整站点源码&#xff0c;附带独立后台和个人二维码收款模块&#xff0c;适合Web前端开发者、PHP学习人员以及需要研究移动端商城交互的技术爱好者。压缩包共355个文件&#xff0c;体积约11.85MB&#xff0c;…

作者头像 李华
网站建设 2026/10/23 7:51:53

实测AI编程代理pi:从代码补全到自主执行的真实体验

最近AI编程助手圈子是真热闹&#xff0c;前脚Copilot、Cursor这些还在卷补全和对话&#xff0c;后脚就冒出来一批新的代码代理工具。我这一周重点试了试其中一个叫pi的coding agent——它跟普通AI插件最大的不同&#xff0c;是它真的自己去读项目、改文件、跑测试&#xff0c;不…

作者头像 李华
网站建设 2026/10/14 5:30:18

多模态融合与RAG驱动的健康辅助诊疗系统:从数据到推理的完整设计

简介&#xff1a;毕业设计资源围绕大语言模型与多模态人工智能技术&#xff0c;构建健康管理与辅助诊疗系统&#xff0c;面向计算机、医学信息工程等专业学生&#xff0c;提供从需求分析、系统设计到论文撰写与答辩展示的完整参考。压缩包共247个文件&#xff0c;约93.2MB&…

作者头像 李华