程序员圈子里有个词叫 caveman,直译过来是穴居人。不过放在代码世界里,它通常被用来描述一种极简到近乎原始的开发方式:不引依赖、不搞架构、不考虑长期维护,打开编辑器写几十行代码,功能跑通就算完事。我入行前几年特别瞧不上这种路子,觉得不工程化就是耍流氓。后来自己折腾个人项目、写脚本工具,才发现 caveman 式开发其实被严重低估了——它恰恰是解决“个人小工具”问题最合适的心态和手段。
这篇文章想聊的就是“caveman”在真实开发与个人项目中到底意味着什么,它适合什么场景,该在什么环节停手别过度设计,以及我踩过的坑和总结出的实操套路。如果你也经常囤积一堆“写给别人看的代码”最后烂尾,或者一开始做个人项目就把脚手架搭得比功能还重,这篇文章应该能帮你找回调性。
1. 重新认识 caveman:别把它当成“代码质量的反义词”
很多人第一次听到 caveman 编程,脑子里浮现的是没注释、没测试、变量名叫 abc 的烂摊子。我不否认存在这种极端情况,但“洞穴式开发”的本意,并不是让人把代码写烂,而是让人把注意力全部集中在“这个工具对使用者有没有用”上。
1.1 洞穴式开发的典型画像与适用场景
caveman 式开发最典型的画像,是这样一个过程:某天你发现有个重复操作特别烦人,于是产生一个念头——写个脚本干掉它。你没有打开需求文档,没有建 Git 仓库,没有搭测试框架,而是直接写。几十行后脚本能跑了,你开心地用起来,之后再也没有回头管它。
这种开发方式的前提,是“使用者只有你自己”或者“使用者只有三五个人”。它适用的场景有很明确的边界,我列几个我实际经历过的:
- 一次性数据处理:比如把下个月的账单 CSV 合并成表格,用 shell 或 Python 跑一遍就完。
- 私有环境里的自动化:给自己电脑写开机清理、文件归档、笔记转换的小工具。
- 原型验证:一个想法还没想清楚,先粗暴实现看效果,说白了就是“搞个能点的东西试试”。
- 外挂式工作流:在既有系统外面加一个小钩子,不需要侵入原系统,独立运行即可。
在这些场景里,你写给“永恒软件”的代码质量约束,什么单元测试、类型标注、对外开放 API,确实都用不上。用了反而是负担,花费的时间和精力,远远超过工具本身带来的便利。
1.2 为什么今天还要“原始开发”?它与工程化并不冲突
让我先解释一个容易误解的点:caveman 不是工程化的敌人,而是工程化的前置阶段。真正的项目,通常也是先以 caveman 方式验证核心逻辑,然后不断补测试、补抽象、补文档,最终长成正经工程。反过来才是灾难——一开始就铺开多层架构,结果连核心需求都没跑通。
我自己的体会是,工程化思维应该用于“项目长大了之后”,而不是“项目还没出生时”。你面对一个可能只活三天的小工具,脑子里想的应该是“最快多久能跑通”,而不是“将来如果有人接手怎么办”。将来不会有人接手,那个“将来的人”就是我自己,而我自己很清楚当时的实现逻辑。
所以说,caveman 是一种认知工具:它帮你判断当前投入的边际收益。为十行代码写一百行测试,不是细心,是自我消耗。真正的高手,是知道什么时候该穴居、什么时候该盖楼的人。
2. 从设计一块“石头”开始:我的 caveman 工具选型过程
聊完理念,接下来进入实操。很多人第一次尝试 caveman 式开发,最容易卡在第一步:“我该怎么开始?要不要先学框架?”答案是不用。那到底该怎么选型,我拿自己做的一个小工具举例,完整走一遍。
2.1 工具选型逻辑:命令行脚本为什么是首选的“洞穴”
先说背景。我平时有大量纯文本笔记散落在 Markdown 文件里,包括会议记录、代码片段、临时想法。时间一长,查找和汇总特别麻烦。我想做一个“把所有笔记里的待办事项揪出来”的小工具,输入一行命令,输出一份今天该做的事清单。
选型时我没有碰任何 Web 前端、GUI 或者数据库,直接选了 Python 标准库写命令行工具。理由很简单,从“我到底要解决什么问题”出发反推:
- 核心功能是文本解析和过滤,Python 的
re和pathlib标准库就够了。 - 使用场景是我的个人电脑,环境里已经有 Python 3,不需要额外部署。
- 交互方式是终端输入一条命令,看到结果,不做复杂交互。
这个选型过程看起来很“自然”,但其实背后有一个关键判断:选择的复杂度,必须低于要解决的问题。如果我为了解决“找出待办事项”去搭一个 Flask 网页,等于用一个更复杂的系统去包住一个小问题,这就是典型的过度开发。
2.2 第一个版本:允许粗糙,但一定要能跑
我当时给自己定的原则是:第一个版本,代码丑一点没关系,功能一定要完整地跑通。完整跑通意味着从输入到输出是一条通畅的链路,而不是只写完解析部分。
于是第一版cave_todo.py长这样:
#!/usr/bin/env python3 # caveman style: no dependencies, no classes, just get the job done import os import re import glob NOTES_DIR = os.path.expanduser("~/notes") TODO_PATTERN = re.compile(r"^- \[ \] (.+)$", re.MULTILINE) def main(): matches = [] for path in glob.glob(os.path.join(NOTES_DIR, "**", "*.md"), recursive=True): text = open(path, encoding="utf-8").read() for line in TODO_PATTERN.findall(text): matches.append((path, line)) for path, item in matches: print(f"{os.path.basename(path)}: {item}") if __name__ == "__main__": main()这段代码放到正经工程评审里肯定不及格:把路径写死了,没处理文件读取异常,没有结果排序,一次打印也没有格式化。但我用它的那两周,每一天都真真切切省了时间。它不需要优雅,只需要在需要的时候,准确告诉我今天有什么事情没做完。
这个阶段最关键的一点,是首次把整个链路走通。一旦你看到了“输入命令,得到结果”的完整流程,后面的优化就有了方向和动力。
3. 实操核心:用“石头与火”实现一个真正可用的个人工具
理论说了一堆,很多人还是想看一个完整能跑的成品。我就把上面那个cave_todo.py继续打磨,升级成一个更完整的 caveman 风格个人工具。这里不搞虚无的教程,直接把每一段代码背后的思路和参数选择都拆开讲清楚。
3.1 核心实现逻辑:从“能用”到“好用的三个转变”
第一版能跑了,但说实话还不太好用。我在真实使用中发现三个痛点:
第一,每天打开终端手动敲python3 ~/scripts/cave_todo.py太啰嗦,必须做成一个全局命令。 第二,输出结果没有按项目和紧急程度排序,一堆待办堆在一起,还是看不出先做哪个。 第三,只有“查看待办”还不够,最好能直接向笔记文件里添加新待办,形成闭环。
基于这三个痛点,我把脚本升级成一款“个人待办管家”。升级过程中依然不依赖第三方库,只用标准库,保持 caveman 的原始气质。
先看主程序框架:
#!/usr/bin/env python3 # caveman_todo.py — a deliberately simple personal todo manager import sys import os import re import glob from datetime import datetime from collections import defaultdict NOTES_DIR = os.path.expanduser("~/notes") TODO_PATTERN = re.compile(r"^- \[ \] (.+)$", re.MULTILINE) def find_todos(): result = defaultdict(list) for path in glob.glob(os.path.join(NOTES_DIR, "**", "*.md"), recursive=True): text = read_safe(path) if not text: continue for line in TODO_PATTERN.findall(text): result[os.path.basename(path)].append(line) return result def read_safe(path): try: with open(path, encoding="utf-8") as f: return f.read() except (PermissionError, OSError): return "" def add_todo(text, tag="inbox"): today = datetime.now().strftime("%Y-%m-%d") inbox_path = os.path.join(NOTES_DIR, f"inbox-{today}.md") with open(inbox_path, "a", encoding="utf-8") as f: f.write(f"- [ ] [{tag}] {text}\n") return inbox_path def main(): args = sys.argv[1:] if args and args[0] == "add": if len(args) < 2: print("usage: caveman add <todo text> [tag]") sys.exit(1) tag = args[2] if len(args) > 2 else "inbox" dest = add_todo(" ".join(args[1:2]), tag) print(f"added to {dest}") return todos = find_todos() total = sum(len(v) for v in todos.values()) print(f"=== TODO ({total}) ===") for file_name in sorted(todos): for item in todos[file_name]: print(f" [{file_name}] {item}") if __name__ == "__main__": main()这里每个设计点都有实际考虑。比如用defaultdict(list)按笔记文件名分组,是因为我实际使用中更想知道“哪个文件里攒了最多待办”,而不是只看全局平铺。read_safe函数处理权限异常,是因为我的笔记目录里有几个文件被我用 root 权限改过,普通用户打开会报错,这在个人机环境里很常见。add子命令写进当天专属的inbox-日期.md,是为了形成“收集箱”习惯,待办先落地,回头再统一整理。
3.2 参数选择为什么这么定:路径、编码与命令入口的取舍
上面代码里几个看起来不起眼的参数选择,实际是踩过坑才定的,值得单独说一下。
关于路径,NOTES_DIR = os.path.expanduser("~/notes")这个写法比写死/home/username/notes安全得多,因为~会随着系统当前用户自动展开。我一开始写死路径,后来换了电脑用户名才发现麻烦,从此都改成了expanduser。
关于编码,所有文件读写都明确指定encoding="utf-8"。有段时间我的笔记里夹杂了从 Windows 拷贝过来的 GBK 编码文件,Python 默认编码读取直接抛 UnicodeDecodeError。指定 utf-8 之后再配合read_safe的吞异常处理,整个扫描过程稳了很多。
关于命令入口,我没有直接用 Python 包管理工具安装,而是采用最朴素的方式:把脚本放在~/bin/目录,并在~/.bashrc里加了一行export PATH="$HOME/bin:$PATH",然后把脚本本身加chmod +x。这种方式人人可操作,没有任何学习曲线,而且完全符合“个人工具”的定位:给我自己用,不需要考虑给别人安装部署。
有了这些改造以后,我的日常使用频率明显上升。本来我对“搞个工具也要修改半天”是有心理预期的,但用下来最直接的感受是:一个工具只要能减少哪怕一次重复劳动,它就值回投入了。
4. 把“石器”升级成武器库:从单脚本到迷你工具集的扩展路径
用顺手之后,人的欲望就会膨胀。我发现自己需要一个能处理多种任务的“caveman 工具箱”,而不是一堆散落在不同目录里的脚本。这个阶段我做了几件事,让个人工具真正形成一个“小而顺”的工作流。
4.1 单体脚本还是多命令派发?我选择了“命令式集合”
鉴于已经有caveman add和默认查询待办的雏形,我自然而然想到把所有个人小工具都收拢到一个程序里,变成类似caveman todo、caveman note、caveman stat这样的子命令集合。
这个决策的依据很简单:命令太多太散的时候,人的记忆成本会急剧上升。如果每个小功能都单独一个脚本名,今天要用的时候还得先回忆脚本名是什么,那工具本身的效率就被抵消了。统一入口以后,caveman这个根命令就变成我个人的“命令行总控”,随时可以查看有哪些子命令。
子命令派发用最朴素的 if-elif 结构实现:
if args[0] == "todo": run_todo(args[1:]) elif args[0] == "note": run_note(args[1:]) elif args[0] == "stat": run_stat(args[1:]) elif args[0] == "dig": run_dig(args[1:]) else: show_help()不用插件机制,不用反射动态加载函数,代码是长一点,但逻辑一目了然。这种直接的好处是:三个月后回来改代码,不需要先回忆设计模式,看到if ... elif ...就懂了一切。
4.2 一个扩展实例:给工具加上“无侵入的定时能力”
如果只停留在手动执行,caveman 工具只能算“省力”,还谈不上“自动”。我接下来做的扩展,是给整个工具集挂上定时触发机制。这一步反过来让我更理解了“不引依赖”的正确性。
我写了一个极简的调度片段,不引入 APScheduler,不引入 Celery,而是直接用系统自带的cron。我只需要保证脚本能够以非交互方式运行,然后写一行 crontab 即可。
# crontab -e # 每天 09:00 执行一次待办汇总,并把结果追加到日志 0 9 * * * /usr/bin/python3 /home/yourname/bin/caveman.py todo >> /home/yourname/logs/todo-daily.log 2>&1这个方案的理解成本几乎为零,而且稳定性极高。很多人一听到“定时任务”就想着上调度框架,殊不知对个人场景,系统 crontab 早就够了。我把这个经验写在这里,核心想表达的是:caveman 不是排斥外部工具,而是排斥“为了工具而工具”的引入。crontab 是系统自带的,可以直接当石器时代的火把来用。
4.3 配置化管理:用 YAML 还是直接用文件路径?
随着子命令变多,脚本里开始出现一堆需要改动的路径和参数。我面临一个选择:引入 YAML 配置文件,还是继续硬编码。
考虑到 YAML 解析需要第三方库 PyYAML,而“caveman 工具集”的定位是“零第三方依赖”,我最后用了 Python 标准库的configparser,或者干脆就是独立的环境变量。具体实现是读取一个~/.cavemanrc文本文件,里面用最简单的键=值格式保存配置。
# ~/.cavemanrc NOTES_DIR=~/notes LOG_DIR=~/logs DEFAULT_TAG=inbox读取端用十几行代码解析,不引依赖,配置又在明面上,改起来很直观。这个做法的另一个好处是:哪天我想把工具分享给朋友,直接把.cavemanrc和我这单个 Python 文件打包发过去就能用,不需要朋友先装一堆东西。
5. 常见问题与排查技巧实录:caveman 式开发最容易翻车的几个地方
再简单的工具,用久了也会出问题。这一节把我自己踩过和帮朋友排查过的典型问题整理出来,基本都是常规文档不会写的内容。
5.1 路径硬编码与用户切换引起的“玄学失败”
这类问题排在第一位,因为它最隐蔽。我在脚本里一开始写的是/home/yusen/notes,后来系统升级时用户名从yusen改成了yusen_new,结果整个工具直接瘫掉。如果是面向多用户环境,这是一个标准的权限与路径问题,但个人工具常被忽视。
排查方法也简单:在脚本开头加两行打印,把解析后的路径打出来。
print("notes dir:", os.path.expanduser(NOTES_DIR))如果你发现os.path.expanduser("~/notes")返回的还是旧路径,多半是环境变量 HOME 没切过来。个人机最常见的是在 sudo 模式下运行,sudo 会把 HOME 重置为/root,于是脚本找的其实是/root/notes。这个坑我起码帮三个人排查过,每次都是哭笑不得。解决办法要么是脚本里主动取pwd.getpwuid(os.getuid()).pw_dir,要么就是用绝对路径明确指定。
5.2 文本编码的脏数据陷阱
caveman 工具的最大特点是不管数据来源,所以在解析文本时最容易碰到编码混乱。我的笔记文件夹里既有从网页复制来的内容,也有从 PDF 抽取的文本,甚至还有已损坏的二进制片段混在里面。
如果不加保护,读取到半个 UTF-8 字符,Python 就会抛异常,整个脚本瞬间挂掉。这里我给个非常实用的兜底方案,比单纯的 try-except 更温和:
def smart_read(path): for enc in ["utf-8", "gbk", "latin-1"]: try: with open(path, encoding=enc) as f: return f.read() except UnicodeDecodeError: continue return ""这个函数的思路是“梯队降级”:先按 UTF-8 读,读不了换 GBK,再不行就 latin-1(latin-1 几乎不会失败,因为每个字节都有对应字符)。这种方式保证脚本不崩,即使某些内容显示成乱码,也不影响整体功能的可用性。
5.3 输出过长导致“刷屏式失控”
还有一类问题出现在输出阶段。个人笔记量大了以后,caveman todo可能一次列出几百条待办事项,直接刷屏刷到根本看不过来。我之前没有做数量限制,还傻乎乎地滚动半天。
后来加了一个很简单的收敛逻辑:默认只输出前 20 条,并提示还剩余多少条。别小看这种“简陋截断”,它直接改变了工具的使用体验。代码加在main的最末段:
MAX_DISPLAY = 20 count = 0 for file_name in sorted(todos): for item in todos[file_name]: if count >= MAX_DISPLAY: print(f"... and {total - count} more") sys.exit(0) print(f" [{file_name}] {item}") count += 1这个“截断显示”的思想,比任何花哨的分页库更符合工具应用的直觉,本质上就是懒人哲学——我只想看今天最相关的 20 件事,剩下的我自然会用 grep 去精确搜索。
5.4 缺少时间维度:待办永远不过期
最后一个坑比较案例化,但也很有代表性:我的待办工具只采集 Markdown 里的- [ ]行,不管这些行是什么时候写入的。结果就是半年前的待办和今天的待办混在一起,优先级完全没有体现。
解决思路是在 Markdown 待办里引入一个可选的日期前缀:
- [ ] [2025-03-01] 给博客配图 - [ ] [2025-06-10] 清理硬盘缓存然后在脚本里增加一个--due参数做过滤。只筛选日期在今天之前的待办项,相当于给自己做了一个“过期清单”。这个功能不复杂,但一旦用上,你就能直观看到有多少任务是积压的,比任何项目管理软件的报表都算得明白。
5.5 关于崩溃恢复与数据安全的两条忠告
最后讲两条忠告。第一条,无论工具多“caveman”,凡是涉及写入操作,都要先备份或者写入新文件而不是覆盖旧文件。我的add_todo一直用open(..., "a")追加模式,天然安全。但如果哪天你写一个“自动整理 Markdown”工具,务必先复制一份再操作——别问我怎么知道的。
第二条,不要因为工具简单就不做日志。我在caveman里加了--debug参数,会在关键阶段打印当前处理的文件路径和匹配到的行数。这个操作的背后逻辑是:个人项目里,给自己留一个事后能查痕迹的入口,比任何注释都更管用。
6. 写在最后:别怕做“洞穴人”,怕的是假装自己是建筑师
我一直记得第一次用自己写的 caveman 小工具解决问题时的感觉,那是一种“我能掌控工具,而不是让工具掌控我”的踏实感。后来我再写正经项目,也总会刻意保留一部分“洞穴思维”:先让核心逻辑用最原始的方式跑通,再去谈抽象、扩展、性能。
如果你看完这篇文章也想动手,我给的最直接的路径是:找一个让你烦了两周以上的重复操作,打开记事本或代码编辑器,用你最快的语言写一个能跑的最小版本,然后立刻替换原来的手工流程。不要担心代码丑、不要想着开源、不要发到社交平台,就自己用,观察它是否真的省时间。
用上三个月,再回来讨论“怎么让它更优雅”。到那时候你会发现,那个曾经被你嫌弃的 caveman 版本,其实已经默默地帮你扛过了最容易放弃的阶段。这就是“穴居人开发”真正的价值——它让你先拥有一双能走路的脚,再去思考怎么搭翅膀。