做开发这几年,我最烦的不是业务逻辑有多绕,而是那些“不做不行、做了又纯粹浪费生命”的重复操作。整理一堆同类型文件、批量改格式、从几十份日志里捞同一段信息,这些活你说它难吧,真不难,但每天来一遍,时间就这么被一口口吃掉了。所以我花了一个周末,用脚本写了套小工具,代号就叫 REA,英文全称是 Repetitive Execution Automator,说白了就是“重复执行自动化器”。这篇文章不是讲什么高深架构,而是把我的设计思路、踩过的坑、最后沉淀下来的用法,原原本本分享出来,给同样被琐碎任务缠住的人一个可直接抄作业的方案。
这工具的适用范围很明确:批量文件整理、内容抽取、格式转换、目录规整这一大类事情。适合谁看?一是长期跟文件、日志、数据集打交道的开发或运维朋友;二是没有系统学过脚本但想提升效率的内容工作者。文章里我会从设计取舍讲到配置语法,再给完整可运行的代码和落地案例,最后补一份常见的报错排查手册,保证你照着动手也能跑起来。
1. 整体设计与思路拆解
1.1 为什么不做成网页服务,而是命令行工具
最开始我确实犹豫过,要不要套个网页界面,毕竟看着“高级”,点按钮总比敲命令门槛低。但仔细一琢磨,这个方向是错的。REA 要处理的是本地文件,弄成网页服务意味着要常驻一个进程、要考虑端口占用、要处理上传下载的延迟,这些额外开销对一个“替我省时间”的工具来说完全是负担。
命令行工具的好处非常实在:一条命令就能跑,不占后台资源,能直接在终端里看到输出,方便排查问题。更重要的是它可以被其他工具调用,比如挂到定时任务里,或者作为 CI/CD 流程的一环。它不需要“界面”,需要的是“接口”。这个定位清晰之后,后续所有设计都围绕“轻量、可组合、可离线使用”来展开。
1.2 三条核心设计原则:单一职责、幂等执行、可追溯
REA 从第一天起就定了三条规矩,后面所有功能都是在这三条上长出来的。
第一条,单一职责。一个规则只做一类事情,比如“整理图片”就只负责按扩展名和日期归档,“抽取日志”就只负责把匹配到的行写到结果文件。别想着做一个万能工具,规则之间一旦互相耦合,出了问题你根本不知道是哪里改坏了。
第二条,幂等执行。同一个规则,跑一遍和跑十遍,最终结果必须一致。这就意味着不能“无脑复制”,得先判断目标是否已经存在,存在就跳过或者覆盖前先比较。这条非常重要,否则定时任务每天跑一次,目录里会堆满重名或者被反复处理过的文件。.
第三条,可追溯。每次执行都写日志,记录哪个规则处理了哪个文件、做了什么动作、花了多少时间。不然哪天发现文件被动过,你连是谁干的都不知道。
1.3 技术选型:只用 Python 标准库
技术栈我刻意保持得很朴素。Python 3.8 以上版本,只用标准库里的argparse、json、re、pathlib、shutil、logging,不装任何第三方依赖。理由很现实:装依赖这事看似简单,换台机器就成灾难现场,虚拟环境、版本冲突、网络受限,哪一个都能让你折腾半小时。标准库虽然没那么花哨,但稳定、随处可用,对于文件批处理这种 I/O 密集任务完全够用。
做个简单对比:用第三方框架也许能让代码量再少一点,但换来的是环境管理成本和升级风险。为了省二十行代码引入一个依赖,很不划算。REA 的核心逻辑其实就几百行,标准库完全扛得住。
2. 核心命令设计与配置语法
2.1 命令总览:init、run、audit、diff
REA 一共就四个子命令,设计原则是“常用功能一眼看懂,不常用的功能也别藏着”。
rea init:在当前目录生成一份默认配置文件rea.config.json,还会顺带创建一个rules/目录放规则文件,以及一个logs/目录存运行日志。rea run:按配置文件里的规则依次执行。默认只跑一遍,适合手动触发。rea audit:只检查规则匹配情况,不实际执行动作,相当于一次“彩排”。我调试规则的时候用得特别多。rea diff:对比执行前后文件的变化,输出一个简单的变更清单,方便你确认预期。
这四个命令覆盖了“初始化、执行、预览、复查”的完整闭环。可能有人觉得audit和diff冗余,但实际用下来,这是我把误操作概率降为零的关键设计。
2.2 配置文件到底长什么样
配置文件是整套工具的灵魂。我用的格式是 JSON,原因有两个:一是 Python 标准库直接支持,解析零成本;二是 JSON 足够严格,少个逗号就会报错,不容易“半对半错”地执行。错误早暴露,远比执行到一半才发现配置有问题要好。
默认配置的结构大概是这样的:
{ "work_dir": ".", "log_level": "INFO", "rules": [ { "name": "archive_images", "trigger": { "pattern": "*.png,jpg,jpeg,gif", "location": "./downloads/images" }, "action": { "type": "move_with_date", "target": "./archive/images" } } ] }每个规则由三部分构成:name是规则名,日志里用来追踪执行记录;trigger定义“匹配什么”,通过文件扩展名加目录位置圈定范围;action定义“做什么”,目前支持移动、复制、重命名、内容抽取四类动作。规则的表达刻意做得浅显,尽量不引入抽象概念,让非程序员也能看懂配置文件在干什么。
2.3 为什么用 JSON 而不是 YAML
有些朋友一上来就问:现在主流都用 YAML,你干嘛用 JSON?说实话,我写第一版的时候确实试过 YAML,但踩了几个小坑之后果断放弃了。
第一,YAML 对缩进极其敏感,一个 Tab 和空格混用,解析出来的内容直接错位,而且报错信息经常指向错误的行号。第二,标准库没有 YAML 解析器,要么装第三方包,要么自己写个阉割版解析器,两条路都是麻烦。第三,JSON 的基础语法大多数人都有直觉,{}、[]、,就这几个符号,不容易写错。
REA 的目标用户包括不常写代码的人,降低配置的书写门槛比“跟随技术潮流”重要得多。
2.4 变量替换:让规则摆脱硬编码
规则里支持{{变量名}}的占位符,执行时从环境变量或者配置文件里的variables字段取值。举个例子,归档路径写成./archive/{{project_name}},那么跑不同项目的时候,只要在命令行传一次参数,规则文件本身不用改。
这个设计是从“规则复用”的角度出发的。你有十个项目,每个项目的目录结构都类似,没必要复制十份规则文件,共用一份,把项目名当作变量传进去就行。稍微习惯之后,你会发现做工具最值钱的设计往往是这些小细节。
3. 实操过程与核心环节实现
3.1 搭出项目骨架
动手写代码之前,先把目录结构定下来。REA 是这样组织的:
rea/ ├── rea.py # 入口脚本 ├── core/ │ ├── __init__.py │ ├── config.py # 配置读取和校验 │ ├── matcher.py # 文件匹配逻辑 │ ├── engine.py # 动作执行引擎 │ └── logger.py # 日志封装 ├── rules/ # 自定义规则目录 ├── logs/ # 运行日志目录 └── tests/ # 测试脚本(非必需,但建议有)入口文件保持精简,只负责解析参数和调度核心模块。把逻辑拆到core/目录的好处是,将来想加图形界面或者 WEB 接口,只需要复用核心模块,不用大改代码。这也是“接口与实现分离”思想在项目结构上的落地方案。
3.2 入口脚本的参数解析
我用argparse做命令行解析,先定义三个层级:子命令、参数、全局选项。代码如下:
import argparse def build_parser(): parser = argparse.ArgumentParser(description="REA - Repetitive Execution Automator") subparsers = parser.add_subparsers(dest="command", required=True) init_parser = subparsers.add_parser("init", help="generate config file") init_parser.add_argument("--force", action="store_true", help="overwrite existing config") run_parser = subparsers.add_parser("run", help="execute rules") run_parser.add_argument("--rule", type=str, help="run only a specific rule") run_parser.add_argument("--var", action="append", default=[], help="pass variable as key=value") audit_parser = subparsers.add_parser("audit", help="dry run without changes") audit_parser.add_argument("--rule", type=str) diff_parser = subparsers.add_parser("diff", help="show changes from last run") return parser这里有一个容易被忽略的细节:自定义变量通过--var以key=value形式传入,可以重复使用多次。这是为了支持“一次命令、多个变量”的场景,项目名、日期、分类名都可以在一条命令里传进去。
3.3 规则引擎核心逻辑
引擎是整个 REA 的心脏。每执行一条规则,它要做四步:定位、匹配、执行、记录。下面这段代码是移动文件规则的核心实现:
from pathlib import Path import re, shutil from datetime import datetime def move_with_date(src_dir: Path, pattern: str, target_dir: Path): target_dir.mkdir(parents=True, exist_ok=True) matched = [] for ext in pattern.replace(" ", "").split(","): for f in src_dir.glob(f"*.{ext.strip()}"): if not f.is_file(): continue date_part = datetime.fromtimestamp(f.stat().st_mtime).strftime("%Y-%m") dest_subdir = target_dir / date_part dest_subdir.mkdir(exist_ok=True) dest = dest_subdir / f.name if dest.exists(): continue # 幂等性保护 shutil.move(str(f), str(dest)) matched.append((f.name, str(dest))) return matched这段代码里有两个关键点。第一个,if dest.exists(): continue是幂等性的实现核心——目标文件已经在,就跳过,不会重复移动,也不会覆盖已有文件。第二个,文件按月份归类,把一堆散乱的截图变成“年-月”组织的目录,后续查找和管理都方便得多。
3.4 落地案例:把散乱的开发素材统一归档
光说不练没意义,我拿一个有代表性的场景来演示:整理下载目录里的开发素材。假设~/Downloads下长期堆积着截图、设计稿、日志、安装包,全部混在一起,手动分类起码十五分钟。现在写一条 REA 规则来解决:
{ "name": "organize_downloads", "trigger": { "pattern": "png,jpg,jpeg,gif,svg,log,txt,zip", "location": "~/Downloads" }, "action": { "type": "classify", "target": "./organized" } }执行过程如下:先用rea audit跑一遍彩排,屏幕上会打印“哪些文件将被归到哪个目录”。确认无问题后,再用rea run正式执行。日志里会记录每个文件的来源和目标位置。全部跑完,下载目录瞬间清爽,每个文件都按类型和时间归好了类。整个过程手动敲三条命令,用时不到三秒(不加文件移动本身的耗时)。
3.5 执行效果与时间对比
我实测了一个包含 200 多个文件的下载目录,规则分类总计用时约 4.8 秒,log 文件里能看到每个动作的时间戳。如果手动操作,一个文件拖拽加确认,平均五秒,200 个文件就是大概二十分钟。这还不是最费时的地方,真正的成本是人在这二十分钟里处于完全机械、毫无创造力的状态。
REA 处理完之后目录结构长这样:
organized/ ├── images/ │ ├── 2025-01/ │ └── 2025-02/ ├── logs/ │ ├── 2025-01/ │ └── 2025-02/ └── archives/ └── 2025-01/这种“执行一次、受益整月”的工具,是你值得投入一个周末去打磨的。
4. 常见问题与排查技巧实录
4.1 五个高频报错与对策速查表
开发和使用 REA 过程中,我遇到了一堆稀奇古怪的问题,挑最有代表性的五个列出来:
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 规则匹配不到任何文件 | pattern里扩展名带了空格,比如"png, jpg" | 去掉空格,或者代码里做.replace(" ", "")处理 |
| 执行时报“目标目录不存在” | 只写了target,没提前建目录 | 代码里用mkdir(parents=True, exist_ok=True)自动创建 |
| 文件总是被重复处理一遍 | 缺少幂等性判断,目标文件已存在还继续移动 | 加if dest.exists(): continue |
| 日志不输出或输出位置不对 | log_level配置成ERROR,把 INFO 过滤掉了 | 改成INFO,或在命令里加--verbose |
| 非 ASCII 文件名乱码 | 没指定 UTF-8 编码 | 文件读写强制encoding="utf-8" |
这五个问题里,前两个是“粗心型”,后三个是“设计缺陷型”,全都是在真实操作里踩出来的,不是纸上谈兵。
4.2 一个从现象到根源的排查实例
有个使用场景让我印象很深:规则在audit模式完全正常,但一到run就只有前几个文件动了,后面的全部卡住。我当时第一反应是“文件有问题”,逐个检查了权限,没问题;查路径,也没问题。最后翻日志,发现异常堆栈显示某个文件名的编码超出了默认值范围。
根源在于当时环境里的默认编码不是 UTF-8,中文字符文件名在shutil.move时抛了 UnicodeEncodeError。这个报错被日志系统吞掉之后,程序继续执行,但动作已经中断。解决方式是全局声明# -*- coding: utf-8 -*-,同时所有涉及路径的字符串显式编码。敲定这个修复之后,REA 后面的执行才算真正稳定下来。
这个案例值得单独说,是因为它展示了一个核心经验:日志日志日志。没有日志,这种问题只能靠肉眼慢慢排查,有了日志,五分钟定位。
4.3 几条操作红线
使用这类工具,有些红线必须刻在脑子里:
- 正式执行之前,永远先跑
audit或diff,确保你完全知道将要发生什么。 - 大批量操作前,先准备一个小目录做试点,规则确认无误再放开跑。
- 移动文件时不要跨磁盘操作,避免意外中断导致文件残留。
- 规则文件是“代码”,改动之后必须先测试再使用,不要指望它每次都对。
这些红线看起来啰嗦,但它们的作用是让你永远有后悔药吃。做自动化工具,最怕的不是 bug,而是“你根本不知道 bug 做了什么”。
5. 扩展方向与使用建议
5.1 给 REA 加上缓存与去重
基础版本跑顺之后,我给自己加了两个进阶能力:缓存和去重。缓存的逻辑是每次执行前先读取上次的结果快照,如果文件没变化,直接跳过。去重的逻辑则是利用文件哈希,识别内容相同但文件名不同的文件,统一归档到一个名为duplicates/的目录里。
这两个扩展让 REA 不再只是“分类工具”,而慢慢变成一个轻量级的数据治理工具。尤其在图片、安装包这类容易重复下载的文件类型上,能帮你清理出不少磁盘空间。实现也不复杂,核心就是记录哈希表到 JSON 文件,执行时多一步比对。
5.2 把 REA 接进定时任务与通知
命令行工具的另一个优势就是方便挂进定时任务。我写了一个很简短的脚本,每天凌晨三点跑一次 REA,把前一天下载的素材自动归类。任务跑完之后调一个 PUT 请求把日志摘要推送到我自己的消息队列服务(完全在内网环境,不涉及任何外部平台)。
这个方案的好处是“无人值守”。早上一到工位,目录已经是整理好状态,不需要你记得去执行任何事。定时任务的框架很多,你用cron可以,用系统自带的任务计划也行,REA 只需要作为一条命令被调用,对它来说,上游是谁都无所谓。
5.3 适合团队的用法:规则库共享与版本管理
再进一步,REA 完全可以放进团队协作流程。因为配置和规则文件都是纯文本,天然适合版本化管理。把规则库单独拆成一个仓库,团队里每位成员各自执行pull后就能共享最新的归档策略。如果有人新增了规则,评审后合并,全组同步。
具体做法是:根目录下建一个rules/目录,每个规则文件都带有author和version字段,这样追溯变更时能清楚地知道是谁在什么时候改了什么逻辑。团队里如果有人提了改进方案,直接改规则文件,跑一遍rea diff对比前后变化,比开会说明高效得多。
5.4 后续可以预见的迭代方向
需求是长出来的,趁这篇文章最后聊两句它可能迭代的方向。我自己在考虑给 REA 加一个“文件内容模板”功能,比如批量在目录下的所有 txt 文件头部插入相同标题;还想支持规则间的依赖关系,例如先抽取日志,再对抽取结果进行统计。
技术实现上都不难,但每次加功能之前都要回到最初的三条设计原则,问问自己:是否保持了单一职责?是否还幂等?是否可追溯?答案有一个是否定的,这个需求就应该被砍掉或者换一种方式实现。
回到最初写 REA 的那个周末,我其实只是想节省每天二十分钟的手动整理时间。结果工具写完之后,省下来的不只是时间,还有“脑子里那根一直惦记着杂事”的弦。现在看到下载目录里的文件自动进入该去的地方,心里几乎没有波澜,反而觉得这是理所当然的。
最后再分享一个小技巧:写规则的时候,先用一个只有两三个测试文件的临时目录把动作跑通,再把规则指向真实目录。这么做虽然多了一步操作,但能避免绝大多数的误操作风险。REA 里我把这种模式叫做“小步快跑”,这大概也是所有自动化工具最值得养成的好习惯。