用Python开发rea:正则表达式批量文本处理命令行工具
发布时间:2026/10/21 4:03:15 锦皓数字建站

如果你跟我一样每天要面对大量日志、导出表格和配置文件一定会有这样的瞬间明明只是把某一段重复出现的文字改成另一段却要打开几十个文件手动操作。后来我花了一个晚上写了代号 rea 的命令行小工具全称 Regular Expression Assistant说白了就是“正则表达式辅助处理工具”专门用来做批量文本提取、替换和文件改名。这篇文章把我从需求梳理、技术选型、核心实现到踩坑复盘的全过程都记录下来适合那些想自己写脚本解决重复劳动、又不想一开始就上重型框架的人。rea 解决的痛点非常具体日志里有大量临时调试信息需要清理导出的数据表里某些字段需要脱敏一批照片要按日期规则重命名几十个配置文件里同一个参数要换值。这些活儿用文本编辑器一个个处理不仅慢而且容易漏写一次性脚本下次需求稍微变一点又得重写。rea 要做的就是把“正则匹配 文件批量操作”这两件事封装成稳定、可复用、先说清楚再动手的命令行工具。1. rea 项目的定位与设计思路1.1 为什么需要这样一个命令行小工具我先说一个自己的真实场景。项目上线前要排查日志某个功能模块每次运行都会往日志里写一串带版本号和时间戳的调试信息一晚上累积了几百兆日志。我需要把这些调试信息统一替换成“已脱敏”的占位内容同时不能碰业务正常输出。一开始我用编辑器打开大日志结果不仅卡而且只改了一个文件几十个分片日志根本没覆盖到。后来又尝试直接敲一段 Python 处理但每次都要临时写文件遍历、编码判断、备份逻辑反复折腾。类似的情况出现第三次的时候我就确定自己需要一个小工具而不是再去复制粘贴脚本。我把需求收敛成三条第一能按正则规则定位文本第二能批量替换并支持预览第三能按正则规则批量修改文件名。这三条几乎覆盖了我日常 80% 的“机械式文本劳动”。工具名取 rea中文可以叫“正则助手”项目代号简短命令行里敲起来也省事。rea 的定位不是替代专业编辑器也不是替代完整的数据处理框架。它更像是你手边的一把螺丝刀规则简单动作直接坏不了事。很多人一听到写脚本就觉得自己搞不定但 rea 的核心逻辑其实只有“读取文件—匹配正则—改写内容”这三步难度并不高真正麻烦的是把这三步做得足够稳妥不误伤文件、不破坏编码、不丢数据。1.2 rea 的核心功能拆解我把 rea 设计成三个子命令对应三类高频操作rea match用正则表达式在文件内容里查找并输出匹配到的行或捕获组常用于“先看看数据长什么样”。rea replace用正则表达式做批量替换支持先预览、再写入也可以开启备份目录。rea rename用正则表达式改写文件名适用于照片、文档、日志分片等文件的批量改名。这三个命令有一个共同点它们都不修改原始文件结构只对“匹配到的部分”做操作。设计上刻意没把“删除文件”“移动文件”“执行任意命令”这类危险能力放进去避免工具越用越复杂也避免误操作时造成不可逆结果。删除和移动是操作系统该管的事rea 只负责文本和文件名层面的精确修改。举个例子如果你有一批report_20240601.csv、report_20240602.csv这样的文件想把日期格式改成2024-06-01用rea rename加上一条正则规则就能完成。如果你有一堆日志里夹杂着调试用的临时 Session ID想在保留业务日志的同时把它替换成[REDACTED]用rea replace配合预览模式就能安全处理。如果只是想知道某个错误码到底在哪些文件里出现过多少次rea match会直接告诉你答案。1.3 设计目标先预览再执行永远留后路我在做 rea 的第一天就定了一个铁律批量修改类操作必须支持预览模式。无论是替换内容还是重命名文件默认先打印出“改前”和“改后”的结果用户确认无误后再真正落盘。这个设计是从一次失误里学到的早期我用临时脚本批量替换配置正则写得太宽松把不该替换的前缀也替换了结果一天的工作量被一次误操作毁掉。从那以后凡是涉及批量改写我都坚持“先预览再执行能备份就备份”。rea 的执行过程因此被拆成两段。第一段是“收集信息”遍历指定目录下的所有文件逐个读取内容并应用正则规则但只把结果打印出来。第二段才是“落盘”在没有任何--preview参数的情况下才真正修改文件。对于 replace 命令我还加了--backup选项可以指定一个备份目录在执行前先把原文件复制过去。多一个备份目录代价只是多占用一点磁盘空间但遇到极端情况时可以救回整批数据。2. 技术选型与原理拆解2.1 为什么是 Python而不是 Shell 或 Node做工具选型时我第一反应是 Shell 脚本毕竟 sed、awk 在 Linux 下处理文本很方便。但很快否掉了一是 sed 的正则语法和常见正则存在差异比如默认不支持\d需要写成[0-9]二是跨平台问题严重在 Windows 的命令行里和类 Unix 环境下行为不一致。我希望能有一个在 Windows、macOS、Linux 都能跑的工具而不是写完只能在服务器上用的脚本。Node.js 也可以写命令行工具但如果只是为了文本处理引入node环境让人觉得有点重。Python 的好处是绝大多数开发机和很多普通电脑都已经装了解释器正则模块re是标准库不需要安装额外依赖pathlib和shutil可以很简洁地处理文件遍历与备份处理几千个中小型文件完全没问题。如果你只需要在 Linux 服务器上临时处理一次用 sed 没问题。但如果你和我一样希望积累一个能长期使用、可以放到任何机器上的小工具Python 是更稳的选择。rea 的核心代码只有一个文件没有任何第三方依赖拿到任何一台装了 Python 3 的机器上就能运行。这对“工具的生命周期”很重要依赖越少越不容易坏。2.2 rea 的运行机制三步走rea 的运行机制可以概括成三步收集文件、读取内容、应用规则。听起来很简单但每一步都有细节要处理。收集文件这一步我用的是Path.rglob()它能在指定目录下递归查找所有符合条件的文件。比如*.log会匹配所有以.log结尾的文件*.txt匹配普通文本文件。如果目录里有子目录也会一并处理。这个设计让 rea 特别适合清理多级目录下的日志分片不用手动写一长串路径列表。读取内容这一步最大的坑是文件编码。同一个目录下的日志有些可能是 UTF-8有些可能是 GBK还有些 Windows 工具会生成带 BOM 的 UTF-8。我用一个常见的“编码试探”写法按顺序尝试 UTF-8、UTF-8 with BOM、GBK 解码一旦成功就采用这个编码。这个策略不是完美的但对付绝大多数中文场景已经够用。后面我会单独讲这种做法的代价。应用规则这一步依赖 Python 正则引擎。正则的匹配结果可以是一个匹配对象也可以是多个匹配对象。rea match用re.finditer()遍历所有匹配位置rea replace用re.sub()做替换rea rename则是对文件名做同样的re.sub()只不过操作对象从文件内容变成了文件名字符串。整个机制统一而简单这也是 rea 能保持体积小的原因。2.3 规则文件怎么设计命令行参数适合临时处理但如果处理规则很复杂或者要每周跑一次同样的清理任务把规则写进命令行就会变得很痛苦。尤其是正则表达式里有大量反斜杠和特殊字符在 Shell 里还要再转义一遍很容易出错。所以 rea 增加了一个run子命令用来读取一个 JSON 格式的规则文件。规则文件的格式如下{ action: replace, path: logs, glob: *.log, pattern: (?\\d{3})\\d{4}(?\\d{4}), replacement: ****, preview: true, backup: backup }action 字段指定执行哪条命令path 和 glob 指定目录和文件范围pattern 是正则规则replacement 是替换内容。preview 字段默认为 true可以避免一执行就落盘。backup 字段指定备份目录为空则不备份。用 JSON 而不是 YAML主要原因是不想引入额外依赖Python 标准库自带json模块。另一个原因是 JSON 更严格字段名拼写错误会直接报错不会像宽松格式那样默默生效然后跑出奇怪结果。规则文件的好处是让“清理日志”这类任务变成可版本管理的配置你可以把 JSON 文件提交到版本仓库里团队成员共用规则变更也有历史记录。3. 实操过程与核心环节实现3.1 搭出最小可用版本我先把 rea 的核心代码完整放出来这个版本可以直接保存为rea.py运行。为了阅读方便我砍掉了并发处理、彩色输出和更复杂的错误分类保留了最核心的 match、replace、rename、run 四个命令。import argparse import json import re import shutil from pathlib import Path def iter_files(path: str, glob: str): root Path(path) if glob *: files root.rglob(*) else: files root.rglob(glob) return (f for f in files if f.is_file()) def read_file_smart(file: Path): for enc in (utf-8, utf-8-sig, gbk): try: return file.read_text(encodingenc), enc except UnicodeDecodeError: continue return None def cmd_match(args): for f in iter_files(args.path, args.glob): res read_file_smart(f) if not res: continue content, _ res for m in re.finditer(args.pattern, content): print(f{f}: {m.group(args.group)}) def cmd_replace(args): for f in iter_files(args.path, args.glob): res read_file_smart(f) if not res: print(f跳过无法识别的编码: {f}) continue content, enc res new_content re.sub(args.pattern, args.replacement, content) if new_content content: continue if args.preview: print(f[预览] {f}) print(f - {content[:100]}) print(f {new_content[:100]}) continue if args.backup: backup_dir Path(args.backup) backup_dir.mkdir(exist_okTrue, parentsTrue) shutil.copy2(f, backup_dir / f.name) f.write_text(new_content, encodingutf-8-sig if enc utf-8-sig else enc) print(f已更新: {f}) def cmd_rename(args): for f in iter_files(args.path, args.glob): new_name re.sub(args.pattern, args.replacement, f.name) if new_name f.name or new_name : continue dest f.with_name(new_name) if args.preview: print(f[预览] {f} - {dest}) continue if dest.exists(): print(f目标已存在跳过: {dest}) continue f.rename(dest) print(f重命名: {f} - {dest}) def cmd_run(args): rules json.loads(Path(args.rule).read_text(encodingutf-8)) action rules.pop(action) cmd { match: cmd_match, replace: cmd_replace, rename: cmd_rename, }[action] cmd(argparse.Namespace(**rules)) def main(): parser argparse.ArgumentParser(progrea) sub parser.add_subparsers(destcommand, requiredTrue) for name in (match, replace, rename): p sub.add_parser(name) p.add_argument(--path, default.) p.add_argument(--glob, default*) p.add_argument(--pattern, requiredTrue) if name match: p.add_argument(--group, typeint, default0) else: p.add_argument(--replacement, requiredTrue) p.add_argument(--preview, actionstore_true) if name replace: p.add_argument(--backup, default) p_run sub.add_parser(run) p_run.add_argument(--rule, requiredTrue) args parser.parse_args() if args.command run: cmd_run(args) elif args.command match: cmd_match(args) elif args.command replace: cmd_replace(args) elif args.command rename: cmd_rename(args) if __name__ __main__: main()这段代码不复杂但每一步都有实际用途。iter_files负责文件遍历read_file_smart负责编码试探cmd 系列函数分别对应三个动作。argparse是 Python 标准库里的命令行解析工具比手动解析sys.argv靠谱得多参数缺失时会给出清晰错误提示。我用“最小可用版”这个词是想强调先跑通再优化。当时我写完这个版本后立刻拿真实日志试了试。第一轮跑下来发现最大的问题不是正则写不对而是某些文件编码识别不了还有文件名重复时会被跳过。这些问题都不是设计层面的硬伤而是在真实文件系统里最常见的物理约束。3.2 match 命令先解决“找出问题”rea match的典型用法是在不确定规则是否可靠时先只做查找不做替换。比如你想知道SessionIdabc123这种模式的字符串在哪些文件里出现可以先执行python rea.py match --path logs --glob *.log --pattern SessionId([a-zA-Z0-9]) --group 1这条命令会把每个文件里匹配到的 Session ID 打印出来。--group 1表示只输出捕获组 1 的内容也就是括号里匹配到的 ID。如果不加--group默认输出整个匹配文本包括SessionId前缀。这一步的价值在于“验证正则”因为正则这东西写在编辑器里看着对跑到真实数据上往往发现有边界情况。我在实际使用中会先跑match把输出结果用head或编辑器分页看一眼确认匹配到的内容符合预期再继续执行replace。这是一个成本极低的检查动作却能过滤掉大部分误操作。特别是面对脏数据时正则匹配往往会暴露出一些你没想到的格式有的是全角括号有的是多余空格有的是换行符被吞掉。只有先看匹配结果才谈得上安全替换。3.3 replace 命令批量替换加保险rea replace是在 match 基础上增加写入能力。它先对每个文件做re.sub得到新内容然后判断新内容和原始内容是否一致。如果一致说明这个文件里没有可匹配的文本直接跳过如果不一致再进入预览或写入流程。写回文件时我保留了原文件的编码策略。如果原文件是用utf-8-sig解码的写回时也用utf-8-sig避免 BOM 丢失。这个细节在 Windows 环境里非常关键很多 Windows 工具会依赖 BOM 来判断文件类型如果替换完把 BOM 弄丢了后续其他脚本就可能解析失败。使用示例python rea.py replace --path logs --glob *.log --pattern DebugTempInfo.*?end --replacement [REMOVED] --preview先加--preview跑一遍rea 会把每个受影响文件的“改前片段”和“改后片段”打印出来。确认无误后去掉--preview再跑一次。如果担心意外再加上--backup backup所有被修改的原始文件会先复制到backup目录文件名不变方便事后对照。3.4 rename 命令文件批量改名rea rename处理的是文件名而不是文件内容。它先对f.name执行re.sub得到新文件名然后判断是否存在同名目标文件。如果目标文件已经存在就跳过避免覆盖。这一步看起来简单但在真实文件系统里几乎必然遇到两个文件改成同一个名字后一个会覆盖前一个或者直接报错。所以我选择“跳过并提示”而不是“强制覆盖”。批量改名的典型场景是照片。例如一批照片叫IMG_20240601_123456.jpg希望整理成2024-06-01_123456.jpg可以用python rea.py rename --path photos --glob *.jpg --pattern IMG_(\d{4})(\d{2})(\d{2})_(\d{6}) --replacement \1-\2-\3_\4 --preview注意这里 replacement 里的\1、\2是正则捕获组引用。在 JSON 规则文件里这些反斜杠还要额外处理所以我通常更推荐把复杂的改名规则放到 JSON 里管理命令行只处理临时的简单规则。3.5 实战日志脱敏某天我需要把一批日志里的手机号脱敏。手机号是连续 11 位数字如果直接匹配 11 位数字可能会把业务数字误伤。更好的做法是只替换中间 4 位保留前 3 位和后 4 位。正则规则写成(?\d{3})\d{4}(?\d{4})它的含义是匹配一个长度为 4 的数字串但这个数字串前面必须有 3 位数字后面必须有 4 位数字。这样整个匹配结果刚好是手机号的中间 4 位replacement 用****前 3 位和后 4 位保持不变。我把这条规则写进rules.json{ action: replace, path: logs, glob: *.log, pattern: (?\\d{3})\\d{4}(?\\d{4}), replacement: ****, preview: true, backup: backup }执行python rea.py run --rule rules.json先以 preview 模式查看效果。确认日志里的手机号都变成类似138****1234的格式后再把preview改成false正式执行。整个过程不到一分钟比人工打开文件要快得多。3.6 实战批量重命名相机照片另一个例子是把一批导出文件改成统一命名格式。同事把文件命名为data 20240601 001.csv、data 20240601 002.csv但后续数据处理脚本只认data_20240601_001.csv空格必须替换成下划线。用 rea 只需要一条规则{ action: rename, path: downloads/data, glob: *.csv, pattern: \\s, replacement: _, preview: true }执行预览后会看到所有文件名从带空格的版本变成带下划线的版本。确认无误后去掉 preview 执行。这类改名需求如果手动在文件管理器里操作几十个文件还能忍受几百个文件就纯粹是浪费时间而用 rea 处理规则本身还可以留着下次复用。4. 常见问题与排查技巧实录4.1 中文乱码让人头大rea 的编码试探策略是依次尝试 UTF-8、UTF-8 with BOM、GBK。这个顺序在实际使用中表现不错因为 UTF-8 是目前的事实标准而 GBK 通常出现在老的 Windows 中文软件导出的文件里。但有一个坑某些文件既能被 GBK 解码又能被 UTF-8 解码但解码出来的内容可能是乱码。解决这类问题的办法是先对一个文件执行一次 match 命令打印出实际读取到的内容片段肉眼看看到底是不是目标文本。如果发现匹配不到尝试指定其他编码。我在正式版 rea 里增加了一个--encoding参数可以覆盖默认的试探顺序。比如在旧项目里明确知道所有文件都是 GBK就直接指定gbk既快又准。4.2 正则贪婪匹配吃掉了不该吃的内容这是最常见的正则问题。比如你想匹配DebugInfo...这里的内容.../DebugInfo这样的片段如果写成DebugInfo.*/DebugInfo.*默认是贪婪的会一直匹配到同一天日志里最后一个/DebugInfo为止。如果这一行日志里有两个 DebugInfo 块贪婪匹配会把你不想删除的内容也吞掉。解决办法是使用非贪婪写法DebugInfo.*?/DebugInfo或者更严格地用字符集排除符号DebugInfo[^]*/DebugInfo。我平时会优先用非贪婪写法然后立刻跑 match 验证匹配结果的数量。比如 grep 数一下输出行数和预期数量对不上就说明正则边界有问题。只要匹配这一步先准确后面替换就很少出错。4.3 命令行转义与规则文件在命令行里写正则最痛苦的是反斜杠转义。比如\d在 Shell 里有时候会被吞掉在 Windows 命令行里又有另一套转义规则。一种规避方式是使用单引号包住整个 pattern像--pattern这种写法在很多 Shell 下会把内容原样传给程序。但如果你在 PowerShell 里单引号的语义又不一样。我的建议是临时的、短小的规则可以直接用命令行参数复杂或需要复用的规则一律写进 JSON 文件。JSON 里虽然有反斜杠转义但它至少是确定性的不用猜 Shell 怎么解释。写完规则后执行 preview只有预览结果正确才算规则真正可靠。4.4 文件多、文件大怎么提速rea 默认是单线程逐文件处理处理几千个小型文件没什么问题但如果面对几十 GB 的大日志单线程逐行读入会有点吃力。性能瓶颈主要在磁盘 IO 和正则执行上。最简单的提速方法是按目录或文件类型拆分任务把一个大任务拆成几个子任务并行跑。我在后续版本里加了一个--workers参数用 Python 的concurrent.futures做多进程处理但这里不展开因为核心逻辑仍然是单线程的并行只是把文件列表分给多个进程分别执行。如果文件特别大建议先做抽样验证。比如先用--glob *.log配合match只处理一个文件确认规则没问题后再在原目录的子目录上跑正式任务。rea 的 preview 模式会逐文件打印改前改后片段如果文件太多建议把输出重定向到临时文件再检查免得终端刷屏。4.5 权限和只读文件有一次我在一台服务器上跑 replace发现某些配置文件修改失败原因是文件属性是只读的。rea 的代码里没有强制绕过只读属性失败时 Python 会抛异常任务中断。为了提升容错我后来加了一个逻辑写文件前先判断os.access(file, os.W_OK)如果不可写就跳过并打印提示。本地使用时权限问题不多。但在企业级环境里日志文件可能被日志服务占用Windows 下还会遇到文件锁。遇到这种情况处理思路不是让 rea 强行写而是先调整文件权限或停掉占用进程再重新执行。工具不是越强越好能安全地停下来比强行完成更重要。5. 进阶扩展与个人收获5.1 把 rea 接进定时任务rea 的命令行接口很适合放进定时任务。如果你是 Linux可以写一个 cron 表达式让它在每天凌晨清理昨天的临时日志如果你是 Windows可以用计划任务调用python rea.py run --rule cleanup.json。规则文件里的preview要改成false并且最好带上backup目录这样即使规则有漏洞备份也能兜底。我自己的习惯是新规则第一次运行必须手动执行 preview连续跑一周没问题后才写进定时任务。即使进了定时任务我也不会删除备份目录里的旧备份而是让备份目录按日期轮转保留最近七天的版本。定时任务最怕的不是处理失败而是处理成功但处理错了所以每次执行后我会看一眼统计输出确认没有异常数量的文件被修改。5.2 用 git 追踪每一次批量修改如果你的项目本身在用 git批量替换前可以先确认工作区是干净的再执行 rea。替换完成后用git diff查看具体改动。这一步比 preview 更精确因为 diff 能显示完整上下文而不仅仅是片段。对于不用 git 的目录我建议在执行 replace 前先做一次快照备份哪怕只是把目录压缩成一个 zip 文件。我实际踩过的一个坑是规则里本该只替换测试环境的域名结果因为配置文件里有一段相同的文字把生产环境配置也改了。当时幸好执行前做了备份回滚只花了几分钟。从那以后凡是批量替换我都会在预览之外再多看一眼 diff这已经成了肌肉记忆。rea 本身不解决业务理解问题但它把“操作前检查”的成本降到了极低。5.3 三个让我印象很深的教训第一正则边界怎么强调都不过分。匹配规则宁可写得严格一点漏掉几条也不要写宽了误伤数据。漏了可以再跑一次误伤了就要回滚。第二预览不是万能的。预览显示的片段有限特别长的行会截断到前 100 个字符如果替换位置在行尾可能看不到。所以我后面把预览改成了“显示第一次出现差异的上下文”比简单截断前 100 个字符可靠得多。第三工具的长期价值不在功能多而在“可复用”。我最早写 rea 的时候只想着解决眼前一次日志清理后来把规则外置成 JSON、加上备份、加上 run 命令它才真正变成一个能反复使用、能共享给同事的小工具。很多时候一个项目做完以后最有价值的并不是代码本身而是你在设计过程中对重复工作流程的理解。回到开头那个问题批量文本处理真的值得自己写工具吗我的答案是如果你已经重复处理过三次相同类型的文件就值得动手。rea 这个名字我用了很久它提醒我再小的工具只要把“安全”和“复用”想清楚就能在实际工作中省下大量时间。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。