1. 为什么“随手写的小脚本”值得被认真沉淀
我见过太多人,电脑里散落着一堆test.py、tmp.sh、新建文本文档(3).bat,每次要用的时候翻半天找不到,找到了又忘了当时是怎么跑通的。更常见的情况是:某个脚本三个月前写得行云流水,今天再打开,连自己都要愣两分钟才想起来那个正则表达式到底在匹配什么。这不是记性问题,是沉淀方式的问题。
“日常小脚本沉淀”这件事,听起来朴素得不像个项目,但它解决的恰恰是每个动手干活的人都会遇到的真实痛点——重复劳动没有被复用,临时方案没有被固化,个人经验没有被检索。一个合格的小脚本沉淀体系,能让你把“这次先这么凑合”变成“下次直接拿来用”,把散落的碎片变成可检索、可复用、可迭代的个人工具箱。它适合所有需要跟命令行、文件、数据、重复操作打交道的人,不管你是刚学会写第一行代码的新手,还是已经攒了几年经验的老手,这套思路都能直接套用。
我自己的做法是:不追求“大而全的脚本库”,而是追求“小而准的沉淀习惯”。一个脚本从临时写到正式入库,中间要过几道筛子,每道筛子都有它存在的理由。下面把我踩过的坑、总结的规则、以及实际在用的目录结构和模板,一条条摊开讲。
2. 什么样的脚本才配进“沉淀库”
2.1 三条准入标准:跑过三次、参数化过、有明确输入输出
不是所有随手写的代码都值得沉淀。我给自己定的第一条硬规矩是:同一个脚本,如果只是为某一次特定任务写的,用完就扔,不进库。判断标准很直接——这个脚本我有没有在三个不同场合、用三组不同数据跑过?如果答案是“没有”,那它大概率还是一次性用品,强行沉淀只会让库变成垃圾场。
第二条是参数化。一个脚本如果里面写死了路径、写死了文件名、写死了日期,那它本质上是个“快照”,不是“工具”。进库之前,所有可变的部分必须抽成参数或配置文件。举个最常见的例子:批量重命名文件的小脚本,如果里面写死了os.chdir("/Users/xxx/Downloads/2024照片"),那换台机器就废了。正确的做法是把目录作为第一个位置参数传进去,把重命名规则作为第二个参数,这样它才具备复用价值。
第三条是输入输出明确。一个合格的沉淀脚本,应该做到“看一眼就知道喂什么、吐什么”。我习惯在每个脚本头部用注释写清楚三件事:输入是什么格式、输出是什么格式、依赖哪些外部命令或库。这三行注释比任何文档都管用,因为它是跟着脚本走的,不会出现“文档在A处、脚本在B处、两边对不上”的尴尬。
提示:如果你拿不准一个脚本该不该沉淀,先把它扔进
drafts/目录放两周。两周内如果又用到了,说明它有复用价值,正式整理入库;两周内没碰过,大概率可以删了。这个“冷静期”机制帮我过滤掉了至少一半的伪需求。
2.2 沉淀粒度:一个脚本只干一件事
我早期犯过的最大错误,是喜欢写“万能脚本”——一个tool.py里面塞了文件整理、文本替换、图片压缩、日志分析四个功能,靠一个--mode参数切换。结果就是:每次改其中一个功能,都怕碰坏另外三个;每次想复用其中一个功能,都得把整个大文件拖过去。这种“瑞士军刀式”的写法,在个人沉淀场景里是灾难。
后来我改成一个脚本只解决一个具体问题,粒度细到什么程度呢?比如“把目录下所有.jpeg后缀改成.jpg”和“把目录下所有图片按拍摄日期重命名”是两个独立脚本,不合并。这样做的好处非常实际:找的时候按关键词一搜就中,改的时候只动一个文件,复用时直接拷走不用裁剪。代价是文件数量变多,但配合下面要讲的命名规范,检索成本几乎为零。
2.3 什么不该沉淀:环境强依赖和一次性探索
有两类东西我明确不往库里放。第一类是强环境依赖的临时脚本,比如某次为了排查一个特定服务的问题,写了个依赖某个内部接口、某个特定版本库的脚本,换个环境根本跑不起来。这种脚本沉淀下来只会误导未来的自己。第二类是探索性代码,比如“试试这个库能不能用”的验证片段,这类代码的价值在于验证过程本身,验证完了结论记下来就行,代码没必要留。
判断方法很简单:问自己一句“如果换一台干净的机器,这个脚本还能不能跑”。如果答案是“得先装一堆东西、改一堆配置”,那它就不适合进沉淀库,适合进的是它的结论——比如“用某某库处理某某格式比原生方法快三倍,注意某某参数要设成某某值”,这条结论记在笔记里,比留一堆跑不起来的代码有用得多。
3. 目录结构与命名:让三个月后的自己一秒钟找到
3.1 按“动作”分类,不按“语言”分类
很多人习惯按语言分目录:python/、shell/、js/,我一开始也这么干,后来发现完全不好用。原因很实在:我要找的是“批量改文件名”这个功能,我不关心它当初是用 Python 还是 Shell 写的。按语言分类,等于强迫自己先回忆实现语言,再去找功能,多绕了一道弯。
我现在用的是按动作分类,顶层目录就几个大类:
scripts/ ├── file-ops/ # 文件与目录操作 ├── text-ops/ # 文本处理与转换 ├──>#!/usr/bin/env python3 """一句话描述这个脚本干什么。 用法: python script_name.py <输入路径> [选项] 示例: python script_name.py ./photos --dry-run """ import argparse import sys from pathlib import Path def parse_args(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input_path", type=Path, help="输入目录或文件路径") parser.add_argument("--dry-run", action="store_true", help="只打印将要执行的操作,不实际修改") return parser.parse_args() def main(): args = parse_args() if not args.input_path.exists(): print(f"路径不存在: {args.input_path}", file=sys.stderr) return 1 # 核心逻辑写在这里 return 0 if __name__ == "__main__": sys.exit(main())这个骨架里有两个设计是我反复验证后保留下来的。第一个是--dry-run开关,几乎所有涉及文件修改的脚本我都强制加这个参数。原因很实际:批量操作最怕的就是“跑完才发现规则写错了”,而文件删除和重命名往往不可逆。有了 dry-run,先跑一遍看输出,确认无误再去掉开关正式执行,这个习惯帮我避免过至少两次批量误操作。
第二个是main()返回退出码,而不是直接sys.exit(0)散落在各处。这样脚本被其他脚本调用时,能通过退出码判断成功与否,组合起来用很方便。比如我可以写一个run-all.sh依次调用几个脚本,任何一个返回非零就中止,靠的就是这个约定。
4.2 Shell 脚本的防御性开头
Shell 脚本的模板更短,但开头几行是雷打不动的:
#!/usr/bin/env bash set -euo pipefail usage() { echo "用法: $0 <目录> [--dry-run]" >&2 exit 1 } [ $# -lt 1 ] && usageset -euo pipefail这行是 Shell 脚本的保命符,拆开说:-e让任何命令失败时立即退出(而不是默默继续往下跑),-u让引用未定义变量时报错(而不是当成空字符串),-o pipefail让管道中任何一环失败都算整体失败。没有这三个开关的 Shell 脚本,出问题时往往已经跑了一半,留下半成品状态,清理起来非常头疼。
usage()函数和参数个数检查也是标配。我见过太多脚本,参数没传就往下跑,结果把当前目录当成目标目录处理了——这种事故在批量操作场景里是灾难性的。多写三行检查,省下的是事后恢复的时间。
4.3 模板里不放业务逻辑
这一点值得单独强调:模板只放骨架,不放任何具体业务代码。我早期图省事,在模板里预置了“遍历目录下所有文件”的循环,结果后来每个脚本都带着这段循环,哪怕有些脚本根本不需要遍历目录,删起来还容易删漏。模板的职责是统一“外壳”——参数解析、错误处理、退出码、dry-run 开关,业务逻辑一律从零开始写。这样每个脚本都是干净的,不会带着用不上的包袱。
5. 让脚本“可被发现”的检索层设计
5.1 一个总索引文件,自动生成不手写
脚本多了之后,光靠目录和文件名还不够,需要一个总览。我的做法是在scripts/根目录放一个INDEX.md,里面是一张表:脚本名、功能一句话、所属分类、最后修改日期。关键点是——这张表不手写,用脚本生成。
生成索引的脚本本身也是沉淀库的一员,逻辑很简单:遍历各分类目录,读每个脚本同名 md 文件的第一行作为功能描述,读文件修改时间,拼成 Markdown 表格输出。每次新增或修改脚本后跑一下,索引自动更新。手写索引的问题在于“一定会忘记更新”,而自动生成的索引永远和实际文件同步,可信度完全不一样。
5.2 用文件内容搜索代替记忆
真正高频的场景不是“打开索引浏览”,而是“我记得有个脚本能干某事,但想不起叫啥”。这时候靠的是全文搜索。我的习惯是在脚本库根目录用编辑器的全局搜索,搜关键词。为了让搜索命中率高,每个脚本的 docstring 和同名 md 里都会刻意写全同义词——比如处理图片尺寸的脚本,描述里会同时出现“缩放、resize、尺寸、宽高、等比”,这样不管我搜哪个词都能命中。
这个技巧听起来琐碎,但实际效果很明显。人的记忆是模糊的,三个月后你可能只记得“那个把图片变小的脚本”,记不住它叫resize-images-max-width。描述里多写几个同义词,等于给未来的自己多留几条检索路径。
5.3 给高频脚本建快捷入口
有几个脚本我几乎每天都在用,每次敲完整路径太累。解决办法是在 shell 的配置文件里加别名,指向脚本库里的具体文件。比如:
alias imgresize='python3 ~/scripts/media-ops/resize-images-max-width.py' alias dedupe='python3 ~/scripts/text-ops/dedupe-lines-keep-order.py'别名只给真正高频的脚本加,控制在十个以内。加太多别名反而记不住,而且会和系统自带命令冲突。判断标准是:过去一周里,这个脚本我手动调用超过五次了吗?超过就加别名,没超过就继续用完整路径。
6. 版本管理与“脚本腐烂”的防治
6.1 整个库一个 Git 仓库,提交信息写清楚“为什么改”
脚本库整体用一个 Git 仓库管理,不搞每个脚本一个仓库——个人沉淀场景没那个必要,反而增加操作负担。提交频率也不用高,我一般是攒几个改动一起提交,但提交信息必须写清楚改了什么、为什么改。
反面例子是我早期的提交信息:update、fix bug、改了一下。这种信息三个月后看等于没写。现在的习惯是至少写一句“改了什么”,能写两句就加上“为什么”:
resize-images: 增加 --max-height 参数 之前只能按宽度缩放,处理竖图时宽度够了但高度超标, 加一个高度上限参数,两个都设时取更严格的约束。这种提交信息在git log里翻的时候,本身就是一份变更历史文档,比事后补的 changelog 可靠得多。
6.2 定期“除草”:删掉不再用的脚本
沉淀库最大的敌人不是不够多,而是太多没用的。我每个季度会花半小时过一遍索引,把过去三个月一次都没调用过的脚本挑出来,问自己三个问题:还用得上吗?还能跑通吗?还有参考价值吗?三个都是“否”就直接删,Git 历史里还留着,真需要能找回来。
这个“除草”动作很重要,因为不删的话,索引会越来越长,搜索命中会越来越杂,最后整个库变得和没整理一样。我现在的库稳定在四十个左右的脚本,进进出出,始终保持“每个都是活的”这个状态。数量不是目标,可用率才是。
6.3 依赖也要沉淀:一个 requirements 文件管住环境
Python 脚本的依赖我统一记在库根目录的requirements.txt里,不搞每个脚本一个。原因是个人脚本库的依赖重叠度很高,分开管反而容易出现版本冲突。新增脚本引入新依赖时,往这个文件里加一行,同时在那条依赖后面用注释标一下是哪个脚本用的:
requests # net-ops/fetch-page-title.py pillow # media-ops/resize-images-max-width.py这样换机器时一条pip install -r requirements.txt搞定,而且能一眼看出某个依赖是不是还有脚本在用,清理依赖时不会误删。
7. 实操中踩过的坑与对应解法
7.1 坑一:脚本里写死绝对路径,换机器全废
这是我最开始沉淀时踩得最狠的坑。早期脚本里到处是/Users/我的用户名/xxx这种路径,在自己机器上跑得好好的,换台机器或者换个用户目录就全线崩溃。后来定的规矩是:脚本内部绝不出现绝对路径,所有路径要么来自命令行参数,要么来自相对于脚本位置的推导。
相对推导的写法在 Python 里是Path(__file__).resolve().parent,在 Shell 里是SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"。这两个写法能拿到脚本自身所在目录,然后基于它拼出配置文件的路径,脚本整体挪到哪都能跑。这个改动看起来小,但它把脚本从“绑定某台机器”变成了“跟着库走”,是沉淀体系能成立的前提。
7.2 坑二:默认行为太“猛”,一跑就改文件
第二个坑是脚本默认就执行实际修改,没有缓冲。有次我写了个批量改后缀的脚本,参数传错了一个目录,一跑下去几百个文件后缀全变了,恢复花了不少时间。从那以后,所有涉及写操作的脚本,默认行为改成 dry-run,要实际执行必须显式加--apply或去掉--dry-run。
这个改动违反直觉——大多数人觉得“默认就该干活”。但实际用下来,多敲一个参数的成本,远小于误操作后恢复的成本。而且 dry-run 的输出本身有价值:它会列出“将要做什么”,你扫一眼就能发现规则对不对,相当于执行前的一次免费预演。
7.3 坑三:错误处理缺失,出错时不知道错在哪
早期脚本出错时经常只给一行Traceback或者干脆静默失败,排查起来很痛苦。后来统一的做法是:每个可能失败的操作都包一层,失败时打印“在做什么的时候失败了、失败的对象是什么”。比如处理一批文件时,单个文件失败不应该让整个脚本崩掉,而是记录下来继续处理下一个,最后汇总报告哪些失败了。
Python 里的写法是try/except包住单个文件的处理,把异常信息收集到一个列表里,循环结束后统一打印。Shell 里则是用|| echo "处理 $f 失败"这种形式记录。这个改动让脚本从“要么全成功要么全失败”变成了“尽力而为、事后报告”,在批量场景里实用得多——毕竟几百个文件里有一两个格式特殊处理不了,不应该拖累其余的。
7.4 坑四:脚本之间互相调用,改一个崩一片
当脚本多起来之后,自然会出现“脚本 A 调用脚本 B”的情况。我踩过的坑是:B 改了参数格式,A 没跟着改,结果 A 静默出错。解法是脚本之间的调用只通过命令行接口,不通过 import。也就是说,A 调用 B 时是subprocess.run(["python3", "b.py", ...]),而不是import b。
这样做看起来“笨”,但边界清晰:B 的内部实现随便改,只要命令行接口不变,A 就不受影响。而 import 方式会把两个脚本的内部实现耦合在一起,改一个牵动一片。个人脚本库规模不大,subprocess 那点启动开销完全可以接受,换来的是解耦,很划算。
7.5 坑五:忘了脚本当初为什么这么写
最后一个坑最隐蔽:脚本能跑,但里面有个奇怪的判断或者魔法数字,三个月后自己看不懂了。解法是对任何“不显然”的写法,当场写一行注释说明原因。比如“这里加 1 是因为某某接口的页码从 1 开始”“这个阈值设成 0.8 是实测下来误判最少的值”。
判断“显不显然”的标准很简单:写的时候如果自己犹豫了一下“为什么要这么写”,那就值得注释。注释不是给代码做翻译(i += 1 # i 加一这种毫无意义),而是记录决策理由。代码说“是什么”,注释说“为什么”,后者才是三个月后真正需要的信息。
8. 从“沉淀”到“复用”的最后一步
脚本躺在库里不等于被用起来。我观察自己的使用数据,发现一个规律:能被想起来用的脚本,往往是那些在“需要它的场景”里被提醒过的。所以除了建库,我还会做一件事——在常用工作流的入口处留“钩子”。
具体做法是:在项目模板、常用命令的别名注释、甚至待办清单的固定条目里,写上“这类任务先看看 scripts/xxx 分类下有没有现成的”。比如每次新建一个需要处理图片的项目,项目说明模板里就有一行“图片处理优先查 media-ops”。这种提醒看起来多余,但实际效果是:它把“去库里翻”变成了流程的一部分,而不是靠临时想起来。
另一个提高复用率的做法是让脚本的输出格式统一。我所有脚本的输出都遵循同一个约定:正常结果打到标准输出,错误和警告打到标准错误,退出码 0 表示成功、非零表示失败。这个约定让脚本可以互相串联——A 的输出直接管道给 B,B 失败时整个链条停下。统一约定带来的组合能力,比单个脚本的功能更重要,因为它让“小脚本”能拼出“大流程”。
说到底,日常小脚本沉淀这件事,技术含量不高,难的是持续和克制——持续地写、持续地整理,克制地只留真正有用的、克制地不把库搞成大杂烩。我这套体系跑了两年多,库里的脚本换了一茬又一茬,但“随手写的东西三个月后还能一秒钟找到并用起来”这个目标,算是稳住了。你要是刚开始,别想着一次建全,先从“每个脚本配一个同名 md”和“文件名动词开头”这两条做起,跑顺了再往上加规则,比一上来就搭大框架靠谱得多。