前阵子整理下载目录,我对着屏幕上密密麻麻的“新建文档(32).pdf”“图片_最终版3.png”“未命名222.txt”发了一会儿呆,然后决定写一个叫rea的小工具来彻底解决这件事。rea的全称是 Rename Everything Automatically,一个不依赖图形界面的批量文件重命名与归档工具,核心能力就是一条命令把一组乱七八糟的文件名变成你真正想要的格式,支持正则匹配、日期生成、序号填充、目录归档,以及干跑预览和回滚日志。
这篇文章适合谁看?如果你经常处理素材整理、代码工程清库、下载目录打扫,或者只是想给一批照片快速规范命名,rea的思路和代码实现都能直接抄作业。我会从需求拆解、设计思路、完整实操到踩坑记录一条龙讲清楚,尤其是那些在 macOS、Windows、Linux 上会微妙不同的细节,全部摊开来说。
1. 为什么做 rea:从“手工整理”到“批量处理”的思维转变
1.1 重新审视根本需求:你到底是“整理文件”,还是“整理规则”
我先花了半小时梳理自己真正的痛点。看起来是“文件太多,想要整理”,但仔细一想,手工整理之所以让人崩溃,不是因为文件本身多,而是因为每次整理都是重复劳动:用同一种标准给文件改名,同一个逻辑把文件挪进对应文件夹,同样对重复二字头疼不已。所以核心需求不是“操作文件”,而是“把操作规则固化下来,让规则自动执行”。
rea的定位由此确定:做一个接受规则、执行规则的 CLI 工具,不卷入图形界面的拖拽交互。为什么选 CLI?因为在批量场景里,图形界面反而低效。你要改一千个文件,在文件管理器里一个个右键重命名肯定输给一条rea命令。而且 CLI 天然适合脚本化,可以放进 cron 任务、CI 流程和自动化脚本,这是图形工具做不到的。
1.2 市面上的批量改名工具为什么不够用
我试用过几款主流批量改名软件,也经历过路径崩坏、正则支持残缺、重名覆盖无警告、以及改名后无法撤销的心惊胆战。它们的问题集中在三块:第一,正则引擎往往只支持简单通配符,想在文件名里提取年份再重新组合成新格式,操作起来非常费劲;第二,大部分工具没有“干跑预览”的习惯,你只能先执行再后悔;第三,运行平台绑定严重,Windows 端的工具在 macOS 上往往难以使用。
作为经常在不同系统间切换的人,我需要一个行为完全一致、基于文本配置、能进出任何脚本体系的工具。rea就是奔着这三个缺口去的:正则完整支持、默认 dry-run 预览、跨平台运行。工具不在乎你文件在哪里,它只负责把旧名变成新名,并且保证变更可追溯。
1.3 功能边界:先做减法,再做乘法
任何一个工具都有失控的风险。我在规划rea时,刻意圈定了几条功能边界:第一,它只处理文件和目录名,不移动文件内容;第二,它不做内容批量替换(那是另一个工具的事);第三,它不提供图形界面,只提供清晰的命令行交互。这样做的好处是把出错概率压到最低,把可测试性提到最高。
边界明确后,功能设计反而顺滑:一个核心解析器处理命名规则,一个执行器负责安全变更,一个日志模块记录全部操作。你给它一个“目标模板”,它返回一套“变更计划”,确认后执行。整个过程可以像考卷一样反复演练,这比任何花哨按钮都实在。
2. 核心设计:命名规则、安全机制与跨平台细节
2.1 命名规则模板的设计原则
rea的命名规则是我花最多心思的部分。它需要简单到一眼看懂,又要强大到能处理复杂场景。最终我设计了一套由“原文件匹配表达式”和“目标文件名模板”组成的规则体系。
原文件匹配表达式使用正则,用来捕获你要提取的信息。比如原文件是IMG_20240105_153022.jpg,你可以在表达式中定义四个分组:拍摄设备、日期、时间、扩展名。如果正则写得清晰,一组文件的所有关键信息都会进入捕获组。
目标文件名模板中,使用大括号加变量名的形式引用捕获组,例如:
{year}-{month}-{day}_{shot_type}_{sequence}.{ext}模板支持内置变量。{year}、{month}、{day}从捕获或日期解析中获得;{sequence}是自动递增序号;{ext}是原文件扩展名。模板里还允许自由插入分隔符,比如下划线、连字符、中括号。
设计这套模板时,我参考了常见日期格式化工具的思路,但刻意用了大括号而不是百分号或反斜杠,因为大括号在文件名中很少出现,不会和用户原有的命名习惯冲突。每个变量还可以带参数,比如{sequence:01}表示使用两位数字填充,不足位置补零。
2.2 安全第一:dry-run、备份与回滚日志
批量改名最可怕的不是改错,而是改错之后没有回头路。一个不留神把2023_report.doc改成2023_report_1.doc,结果又把另一个文件改成2023_report_1.doc,那就是直接覆盖。为了防止这类事故,我在rea里实现了一个强制安全的执行流程。
第一步是 dry-run,也就是“干跑”。执行任何变更前,工具只读取文件列表、计算新文件名、输出计划表,但不写盘。用户看到计划后,确认无误再执行。这个习惯一开始我也有点嫌麻烦,实操下来发现,干跑多花的时间远低于出错之后恢复的时间。
第二步是写入变更日志。每次实际执行,rea会按时间戳生成一份 JSON 日志文件,里面记录每一项操作前后的完整路径。日志默认存放在用户指定的日志目录,且日志本身不会被批量改名规则触及,避免日志文件也被卷进变更。
第三步是备份机制。默认情况下,rea不会执行不可逆操作。它推荐用户使用--backup参数,执行前把所有待变更文件复制到一个临时隐藏目录。虽然备份会占用额外磁盘空间,但在批量处理重要资料时,这个成本绝对值得。
2.3 跨平台要处理的三个细节
跨平台是这个工具最隐蔽的挑战。我第一版写完在 macOS 上跑得很溜,拿到 Windows 上就翻车了,问题出在三个细节上。
第一个是路径分隔符。Windows 使用反斜杠,类 Unix 使用正斜杠。rea内部统一用 Node.js 的path模块处理路径,避免手写字符串拼接;在输出日志和预览表时再转换成各平台习惯的展示格式。
第二个是文件名大小写。Windows 文件系统默认大小写不敏感,但 Linux 敏感。你要把一个文件从Readme.txt改成README.txt,在 Linux 上很容易,在 Windows 上如果只改大小写,系统会认为源和目标完全相同,直接报错或静默失败。解决方案是两步改名:先把目标改成临时临时文件名,再把临时名改成最终目标名。
第三个是文件被占用。Windows 下文件一旦被某个进程打开,rename 操作就会抛EPERM或EBUSY。程序不能盲目失败退出,而应该识别这类场景并明确提示占用者,甚至等待重试。类 Unix 系统上文件被打开也能重命名,但为了行为一致,rea的默认策略仍是检测后警告。
3. 实操实录:从零搭建 rea 的过程
3.1 项目初始化与环境选型
我选择 Node.js 来实现rea,原因有三:跨平台支持最好;文件系统 API 成熟;包管理生态完善,后续想加语法高亮、表格输出等都有现成方案。初始化时我只依赖一个辅助库用于命令行参数解析,核心逻辑全部手写,保持代码透明可审查。
先建立项目目录,用npm init初始化,然后创建一个cli.js入口:
mkdir rea cd rea npm init -y在入口文件顶部定义命令参数解析,支持以下几个核心参数:
--match 必填,用于匹配原文件名的正则表达式 --template 必填,目标文件名模板 --base 可选,操作的基础目录,默认当前目录 --ext 可选,限定参与处理的扩展名列表 --dry-run 执行干跑预览 --backup 执行前备份 --log-dir 日志目录参数解析没有采用复杂方案,就是逐项读取process.argv,然后组装成配置对象。参数名刻意设计得短而明确,方便在 cron 任务里一行写完。
3.2 核心代码实现:解析、匹配、生成、执行
整个rea的单次运行可以拆成四个阶段:扫描文件、匹配旧名、计算新名、提交变更。
扫描阶段使用递归方式读取目录,但不读取符号链接指向的目录,避免循环。每一级的目录可以被当作“只读目录”过滤,也可以被当作“待改名对象”,这取决于匹配规则是否命中。我在设计时有意让目录名也可以参与改名,这样整理项目树时会非常方便。
匹配阶段用用户提供的正则表达式逐一测试文件名,例如:
const regex = new RegExp(matchPattern, 'i'); const parts = regex.exec(filename); if (!parts) return null;注意这里我用'i'作为默认选项?不,实际运行时我会让用户决定是否忽略大小写,默认不忽略,否则大小写敏感的场景会被误伤。但为了更安全,我额外允许用户在--match参数中以/i标记结尾来显式不区分大小写。
计算新名阶段将捕获组映射到模板变量,处理内置函数。核心逻辑大约如下:
function renderTemplate(template, matches, index) { return template.replace(/\{(\w+)(?::([^}]+))?\}/g, (raw, name, opt) => { if (!builtins[name]) return raw; return builtins[name](matches, index, opt); }); }内置变量里的{sequence}比较特殊,它的值依赖文件排序。所以我先扫描全部文件,再按某种策略排序,最后才对每个文件渲染模板,确保序号按预期递增。
提交变更阶段,每一条记录先写入日志,再执行 rename。顺序是先写日志后改名,这样万一程序中途崩溃,日志里至少记录了已经执行的所有操作,可以用来恢复。
3.3 案例一:把相机照片改成“日期_位置_序号”格式
假设我有一个photos目录,里面全是相机导出的文件,命名类似IMG_20240105_153022.jpg、IMG_20240105_153738.jpg。我想把文件名改成“位置描述_日期_序号”,但日期格式要从20240105拆成2024-01-05。
匹配表达式可以写成:
^IMG_(\d{4})(\d{2})(\d{2})_(\d{6})\.([a-zA-Z0-9]+)$目标模板:
vacation_{year}-{month}-{day}_{sequence:02}.{ext}在执行前,先用--dry-run看预览:
node cli.js --base ./photos --match "^IMG_(\d{4})(\d{2})(\d{2})_(\d{6})\.([a-zA-Z0-9]+)$" \ --template "vacation_{year}-{month}-{day}_{sequence:02}.{ext}" --dry-run输出会显示一个变更计划表,例如:
IMG_20240105_153022.jpg -> vacation_2024-01-05_01.jpg IMG_20240105_153738.jpg -> vacation_2024-01-05_02.jpg注意这个案例里文件名顺序并不等于时间顺序,如果照片是连拍的,序号意义不大;更好的做法是按拍摄时间排序再编号。我在扫描阶段支持--sort-by mtime,这样序号能反映文件的修改时间先后。对这个案例,我会先按 mtime 排序,再渲染模板。
确认干跑结果无误后,去掉--dry-run并加上--backup执行:
node cli.js --base ./photos \ --match "^IMG_(\d{4})(\d{2})(\d{2})_(\d{6})\.([a-zA-Z0-9]+)$" \ --template "vacation_{year}-{month}-{day}_{sequence:02}.{ext}" \ --sort-by mtime --backup执行完成后目录里会出现一个隐藏备份目录,记录这次操作涉及的全部原文件。如果后续发现新的需求,可以恢复原状重新处理。
3.4 案例二:代码工程目录的统一归档
第二个案例来自项目工程。一个模拟项目X的docs目录里有各种命名混乱的文档,例如final report.txt、Draft_03.txt、notes_2023-12-30.txt,我想统一成“项目缩写_类型_日期_版本”的格式。
对不同规则文件,一个正则很难全覆盖。rea的设计里支持多个规则依次尝试,命中即生效。我先为两类文件分别定义规则:
第一类,文档文件,匹配表达式:
^(final|draft|notes)[_\s-]+(.+)\.txt$模板:
prj_{doc_type}_{date:today}_{sequence:02}.txt第二类,图片素材文件,匹配表达式:
^(scan|photo)_(\d{8})\.(.+)$模板:
prj_image_{year}-{month}-{day}_{sequence:02}.{ext}{date:today}是我内置的另一个特殊变量,允许在无法从原文件提取日期时,使用当天日期填充。这个变量在批量归档当天收到的文件时非常有用。
实际执行时,rea会先加载一个规则文件而不是命令行参数,这样保持命令短小,也便于把规则版本化,纳入代码仓库管理。
执行后我还会生成一份manifest.json,记录每个文件从哪个路径变成了哪个路径,方便后期追溯。我的建议是:不要只把这个日志放在本地,请同步到一个私有仓库里;文件变更的时间点和规则版本都能对应上,排错时帮助很大。
4. 常见问题与排查技巧实录
4.1 正则匹配正常但捕获组总是为空
这个问题第一次出现时,我以为是正则写错了。反复调试后发现,问题出在可选分组上。比如我的正则写成^IMG[_-]?(\d{4})?(\d{2})?,当文件名没有分隔线时,捕获组的顺序会整体错位,{year}捕获到空值。
解决原则很简单:正则里的每个捕获组都要固定存在,如果可能缺失,要用占位符而非让组本身可选项,或者在渲染模板前对捕获组做一次校验,缺字段就报错并给出文件名。宁可报错,也不要生成一堆空字段文件。
4.2 改名后文件名冲突导致覆盖
批量改名的经典事故就是目标名撞车。比如所有文件都改成report_{sequence}.pdf,但排序顺序不稳定,序号分配就会和预期不一致。更严重的情况是,某些文件根本不应该参与改名,但因为模板生成的新名恰好与另一个文件重名,导致冲突。
rea在处理冲突时,默认不覆盖已存在同名文件,而是把整个冲突项标记为错误并在预览中高亮。我测试过是否要自动追加后缀,最后放弃了:工具自动加后缀虽然不覆盖文件,但会让用户失去命名控制权,宁可提示、重跑、重新制定规则,也不能静默地把事情“变得差不多”。
4.3 Linux 下中文文件名乱码
在类 Unix 系统上,文件名本质上是字节序列,不一定是合法的 UTF-8。如果你从 Windows 拷贝一批旧文件过来,文件名可能带着非 UTF-8 编码,Node.js 默认按 UTF-8 解析就会变成乱码。
我在处理时添加了检测逻辑:如果文件名解码成 UTF-8 时出现 replacement 字符,就保留原始字节不做解析,并在日志里警告。rea的默认策略是:宁可跳过,也不进行破坏性修改。后来的实践也证明这个策略是对的,因为你无法从乱码文件名中可靠地推断原始内容,还是让用户手工处理这些异常文件更稳妥。
4.4 Windows 下只改大小写失败
前面提到过,Windows 对大小写不敏感。实际测试中,把readme.txt改成README.txt,如果直接执行fs.rename,绝大多数情况下什么都不发生,甚至不会报错。因为系统认为这是同一个文件。
解决方案是要么故意先改到一个中间名,再改到最终名;要么使用 Windows 底层特有方式处理。rea的实现就是在检测到“目标与原文件名只有大小写差异”时,自动插入一次临时改名。这个临时名规则是在原文件名后加随机短串,比如readme.txt.tmp_8f3a,然后再改成最终目标。
4.5 日志滚不回来怎么办
执行一次大变更之后,最怕两件事:一是担心结果不对,二是想恢复原样。我的日志设计里包含旧路径和新路径,理论上可以反向把新路径恢复成旧路径。但有个前提:你不能再执行任何新的批量改名,否则会乱。
实际操作中,我建议使用 git 管理文件目录的元数据?很不现实,因为文件太普通了。更实用的恢复方式是利用 backup 目录。执行前备份的文件放在原目录结构外,恢复时直接整体覆盖回来。如果没加备份参数,那就只能逐条用日志恢复,这时日志的完整性非常重要。所以我的自我要求是:每次执行必须看到日志成功写入后,才允许工具继续;日志写入失败,改名流程立刻中止。
5. 让 rea 变得更顺手:几个扩展方向
5.1 接入定时任务实现目录自动归档
定时任务是我用得最多的扩展场景。下载目录每隔三天就会重新堆积,我写了一条 cron 任务,每周日凌晨用rea把下载目录里超过七天的文件归档到对应月份目录。命令长一点没关系,因为全部参数写在规则文件里,cron 只调用一条简单命令。
这个过程里验证过的关键心得是:定时任务环境下一定要保留 dry-run 的阶段日志,不能只输出“成功”。否则哪天规则写错了,图片被错误命名,你根本意识不到。我会把每次执行产生的 preview 计划输出重定向到日志文件,保留一个月。
5.2 与文件标签系统结合
单纯的改名解决不了一件事:语义检索。文件名能承载的信息有限,更好的方案是把命名生成的元数据写入一个索引表。我基于rea的 manifest.json 做了一个简单的本地索引,通过文件名直接反查出文件来源和归属项目。这样即使文件挪了位置,索引依然准确。
其实工具本身要保持克制,不去强制生成某种数据库,因为文件的组织方式带着强烈的个人习惯。能输出稳定数据格式的底层工具,比什么都管的大一统工具好维护得多。
5.3 把常用命名规则沉淀成模板库
我用了一阵子后发现,很多命名规则高度重复。比如“日期+描述+序号”在照片处理、日志归档、项目文档里反复出现。于是我把常用规则整理成模板库,用rea --from-template photo直接生成对应参数,再按需微调。
模板库的格式就是一份 JSON 文件,其中包含正则、模板、排序方式和冲突策略。这样做的好处是团队内部可以共享规范,新人拿到一条命令就能批量整理文件,不用理解正则细节。
6. 我对 rea 的真实使用体会
我现在几乎每周都会用到rea,但最常使用的场景反而没那么“高级”:给老婆的旅行照片改名、给孩子的作业电子版归档、给横跨三年的项目资料统一前缀。工具只有进入日常才不会被遗忘,这也是我始终坚持默认干跑、坚持完整日志的原因。批量改名本质上是高风险低容错的操作,工具设计得再聪明,都不能替代一双事后能回看过去的眼睛。
最后再分享一个小技巧:复杂场景不要试图写一个万能规则,先用--dry-run处理一个子目录,校验结果显示在预览列表里的每一条记录,再用规则文件固化版本。宁可多花三分钟检查预览,也绝对不要直接在正式目录上试错。这大概就是我踩过最多次的坑,也是rea在一次次操作里真正沉淀下来、值得你抄走的核心经验。