rea:一个自制的命令行正则表达式调试与批处理工具

发布时间:2026/10/12 12:35:04
rea:一个自制的命令行正则表达式调试与批处理工具
rea这个名字看起来就像随手敲出来的三字母缩写。很多朋友第一次见到都会问这是什么对我来说它是 Regex Easy Assistant——一个被我在终端里用了大半年、完全自制的命令行正则表达式调试与批处理工具。如果你经常要面对日志分析、文本清洗、批量改名这类活儿一定知道正则表达式的双面性会了之后效率飞快写的时候却常常被反斜杠和括号组合折腾到怀疑人生。我过去习惯在网页工具里测好正则再把结果复制回终端执行中间还隔着数据能不能发到网页的顾虑。时间一长我决定自己做一个命令行工具把测试、提取、替换这三个最常用的动作统一起来这就是 rea 的由来。这篇就是完整的项目拆解和踩坑记录。它不涉及复杂架构就是一个 Python 脚本加上一组顺手的功能。适合那些想提高文本处理效率、又不想每次都被正则语法打断思路的开发者也适合完全没接触过命令行正则工具的初学者照着操作。1. 设计思路与方案选型1.1 正则表达式为什么总让人卡壳正则本身是描述文本模式的微型语言它需要同时记住元字符、分组、量词、断言以及不同语言实现之间的细微差异。这就像学做饭菜谱上写着盐少许、糖适量但实际操作时火候、顺序、食材含水量随便一个变量都会让结果不同。正则的问题更具体一个字符的转义漏了可能就从匹配成功变成匹配到整个文件。以我自己踩过的坑为例。曾经要匹配类似2025-04-13这样的日期第一版写的是\d{4}-\d{2}-\d{2}看着没问题但线上日志里偏偏有一段是2025-4-3没有补零。真要严格匹配日期又得加上对月份和天数的边界约束。我为了一个日期换了多种写法才找到平衡。这类场景出现多了你会发现问题不在于懂不懂正则语法而在于缺少一个能快速验证试错的闭环。再加上实际工作里不是单纯匹配一次就结束。今天要从访问日志里提取 IP 和状态码明天要把一批导出的文件名改成统一格式后天可能是把 CSV 里的邮箱脱敏。每个场景都值得用正则但每个场景的正则都要重新调试。时间就花在一次次的试错上。1.2 现成的正则工具为什么不够顺手我先把市面上的方案过了一遍各有各的问题。在线正则工具最常用但内部日志、客户数据不能随便粘贴到第三方网站哪怕只是复制粘贴也有泄露风险。而且在线工具通常只负责验证这一条不能直接处理本地文件每次都要手动把内容复制进去改完再复制出来非常麻烦。IDE 自带的查找替换功能适合在编辑器里操作但如果要处理的是几十个文件或者想把这个提取逻辑写进自动化脚本IDE 的图形界面就无能为力了。再看命令行里的 grep、sed、awk它们确实强大但语法不统一GNU 版本和 BSD 版本的行为有差异正则风格也跟 Python、JavaScript 不完全一样。我经常在 mac 和 Linux 服务器之间切换同样的正则在一个环境下能跑换另一个环境就报错。所以我的需求很明确要一个终端工具能读取文件和标准输入支持调试、提取、替换三个核心动作并且正则引擎要稳定统一。不需要花哨 UI不需要每次开浏览器最好还带一套常用规则库把多年积累下来的正则沉淀下来。1.3 技术选型为什么是 Python 加标准库选型时其实没有太多犹豫。Python 标准库里的 re 模块是文档完善、行为稳定的正则引擎我已经很熟悉。命令行部分用 argparse 就能做得规规矩矩整个工具可以写成单文件不依赖任何第三方库。这意味着在任何装有 Python3 的机器上直接拷过去就能用不用为运行环境费心。为什么不用 Go 或 Rust 重写这个问题我考虑过。Go 的正则是 RE2 风格最大优点是不会回溯爆炸但它也不支持反向引用和部分复杂断言我的替换场景依赖反向引用所以 Python 的 re 更合适。Rust 的 regex 库同样有类似限制。如果未来要做到极致性能、单二进制分发我可能会做一版 Go/Rust 版本但现阶段 Python 的开发效率和可维护性完全满足需求。整体结构也保持简单一个入口文件四个子命令test、grab、sub、list-rules一个规则库加载器外加一个负责从文件或标准输入读取数据的输入层。没有数据库没有配置文件轮询一切以随手能用为优先。2. 核心命令与细节解析2.1 rea test先验证再动手rea 的核心信条是先验证再批量处理。test 子命令就是干这个的。基本用法rea test --pattern ^(\d{4})-(\d{2})-(\d{2}) --string 2025-04-13 log message执行后会在终端直接输出match: True groups: [2025, 04, 13] span: (0, 10)这里能看到完整匹配是否成功、每个分组捕获的内容以及匹配结果在原字符串中的位置。如果没匹配上输出match: False这时候不要急着改正则先用肉眼检查输入内容有没有隐藏字符再考虑模式本身的问题。这里要强调一个终端使用习惯pattern 和 string 都要用单引号包起来。用双引号时shell 会把$、!、反斜杠等做一层解释正则里很多符号会受到干扰。比如pattern^[a-z]$其中的$会被 shell 当成变量引用结果传到 rea 手里的 pattern 已经变了。我早期因为这个浪费过不少时间后来规定自己一律单引号不再纠结。test 还支持几个常用 flag-i忽略大小写、-m开启多行模式、-s让.也能匹配换行符。这三个 flag 对应 Python re 模块的re.IGNORECASE、re.MULTILINE、re.DOTALL。需要注意-s不是 strict而是 单行模式这个名字经常让人误解它的作用是改变点号的行为不是把多行文本压成一行。2.2 rea grab一行一条提取结构化内容测试通过之后真正干活一般用 grab。它会把文件或标准输入里的每一行拿出来做匹配只输出我们需要的那部分。用它提取 IP 地址rea grab --pattern (?Pip\d{1,3}(?:\.\d{1,3}){3}) --file access.log --unique这里用命名分组(?Pip...)把 IP 单独拎出来。默认情况下grab 输出的是完整匹配内容如果只想输出某个分组可以使用--group ip。加了--unique会去重适用于统计有多少个不同 IP。配合--json可以输出 JSON 对象方便后面接 jq 或其他脚本处理。grab 最大的价值是批量。在线正则工具只能测一条grab 可以直接扫完整个文件输出所有匹配结果。我经常这样统计日志rea grab --pattern HTTP/1.1 (?Pstatus\d{3}) --group status --file access.log | sort | uniq -c | sort -rn这条命令把状态码出现次数从高到低排列比打开日志文件一个个数高效得多。分组编号从 1 开始0 代表完整匹配。如果用了命名分组直接用--group status这种可读性更好的写法后期维护也省心。2.3 rea sub带预览的替换替换是正则最危险的动作之一因为一旦匹配的边界没卡对就可能把不该改的数据一起改了。rea 的 sub 子命令为此设计了--dry-run模式rea sub --pattern (?Pyear\d{4})(?Pmonth\d{2})(?Pday\d{2}) --sub \gyear-\gmonth-\gday --file report.txt --dry-run这个例子把20250413这类日期格式改成2025-04-13\gyear是命名分组反向引用。--dry-run会在不改动文件的前提下把前几行替换前后的对比打印出来确认无误后再去掉--dry-run真正执行。这是我用下来最提升安全感的设计强烈建议任何批量替换都先跑一遍 dry-run。除了固定文本sub 还支持--callback参数把每个匹配的结果交给一段 Python 表达式处理。比如想把所有时间戳统一加一天这种逻辑用纯正则替换很难写但用 callback 就方便rea sub --pattern (\d{2})/(\d{2})/(\d{4}) --callback %s-%s-%s % (m.group(3), m.group(2), m.group(1)) --file dates.txt --dry-run不过 callback 是把双刃剑。它能做任意处理也就意味着能执行任意代码。我自己只在完全可信的本地数据上用它凡是从外部渠道拿到的规则一律不碰 callback。2.4 规则库让常用正则不用每次重写正则写得多了会发现很多模式其实是反复出现的。比如 IPv4 地址、URL、邮箱、日期、常见的日志行。rea 内置了一组预置规则通过rea list-rules可以查看用法是rea grab --rule ipv4 --file access.log --unique这样就不需要手敲那串又长又容易出错的 IP 正则。规则库存在~/.rea/rules.json里结构是 JSON 数组。一个典型条目长这样{ name: ipv4, description: 匹配 IPv4 地址, pattern: (?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3} }注意 JSON 里每个反斜杠都要写两遍手写容易错。所以 rea 也提供了rea add-rule命令用交互式输入的方式添加规则由工具处理转义问题。我最初设计规则库时图省事让用户直接改 JSON结果有段时间不停地被反斜杠问题困扰后来加了 add-rule 才好很多。3. 实操部署与三种高频场景3.1 五分钟部署一个可用的 rearea 就是单个 Python 文件不需要安装依赖。我在自己的机器上通常放在~/bin目录加一个软链接让 rea 命令全局可用mkdir -p ~/bin cp rea.py ~/bin/rea.py chmod x ~/bin/rea.py ln -s ~/bin/rea.py ~/bin/rea rea initrea init会建立~/.rea目录并生成默认的 rules.json 和 history.txt。如果用的是 Windows我建议在 Git Bash 或者新版终端程序的 Linux 环境里操作原因不是路径放不下而是文件编码和换行符会让正则的.、^$行为跟 Linux 不一致。这些坑后面会详细说。部署完成后第一时间测试核心流程rea test --pattern ^\w\w\.\w$ --string aliceexample.com能看到match: True说明环境基本正常。rea 本身还支持从标准输入读取数据所以它不是一个孤立的命令而是可以嵌到管道里的一个环节cat access.log | rea grab --rule status_code | sort | uniq -c3.2 场景一从访问日志里提取关键信息并统计假设拿到一份访问日志单行格式如下192.168.1.10 - - [13/Apr/2025:10:24:36 0800] GET /api/users?page2 HTTP/1.1 200 5387 - Mozilla/5.0我想提取 IP、时间、请求方法和路径、状态码。先写一个可读性较好的正则用命名分组把每个字段分开rea grab \ --pattern ^(?Pip(?:\d{1,3}\.){3}\d{1,3}) .*?\[(?Ptime[^\]])\] (?Pmethod\w) (?Ppath\S) HTTP/[0-9.] (?Pstatus\d{3}) \ --file access.log --json --limit 5输出{ip:192.168.1.10,time:13/Apr/2025:10:24:36 0800,method:GET,path:/api/users?page2,status:200}这里有两个地方值得解释。第一.*?是非贪婪匹配它会尽可能少地匹配字符直到后面跟着\[这样时间字段能正确落在第一个方括号上。第二\S用来匹配路径它不会吃掉后面的引号。HTTP/[0-9.]则匹配 HTTP 协议版本避免状态码被误并到请求行里。如果只是想看有哪些不同 IP 访问过用rea grab --rule ipv4 --file access.log --unique | wc -l也能实现。但从完整日志里提取上下文时自写正则更精准。3.3 场景二批量重命名文件先 dry-run 再执行重命名文件是我最开始做 rea 的主要动机之一。有一批测试数据文件名字是report_20250101.txt、report_20250201.txt这种想改成2025-01-01_report.txt。用 sub 把每个文件名处理一遍for f in report_*.txt; do new$(printf %s $f | rea sub --pattern ^report_(?Py\d{4})(?Pm\d{2})(?Pd\d{2})\.txt$ --sub \gy-\gm-\gd_report.txt --strip-newline) echo $f - $new mv $f $new done这里的--strip-newline让 rea 在输出替换结果时不追加换行方便 shell 变量赋值。一定要先在 echo 那一步人工看一遍映射关系再真正执行 mv。如果文件名里含有空格或特殊字符建议给两条命令都加上双引号就像上面这样。为什么不用现成的 rename 命令Linux 上 rename 的语法分发版本太多有的来自 util-linux有的来自 perl参数完全不同。我在这上面吃过亏在测试环境写好的 rename 规则换一台服务器直接报错。rea 的 sub 行为是统一的因为正则引擎和参数都是我控制的。3.4 场景三CSV 数据脱敏只保留邮箱前缀前三位处理测试数据时常需要给敏感字段脱敏。比如一份 CSV 里有邮箱想把用户部分打码只留前三个字符和***文本rea sub \ --pattern (?Puser\w{3})\w*(?Pdomain\w\.\w) \ --sub \guser***\gdomain \ --file users.csv --dry-run原始行1,aliceexample.com 2,bobsmithexample.com替换后1,ali***example.com 2,bob***example.com注意\w在 Python re 中默认匹配字母、数字和下划线对英文邮箱没问题。这个例子还展示了 rea 的一个特性它只改动匹配的部分没有匹配到的行原样保留。数据清洗时尤其有用因为很多 CSV 本身就有脏数据强行全行替换反而容易出错。如果只想输出发生过替换的行可以专门加--changed参数让结果更聚焦。4. 常见问题与排查技巧实录4.1 终端里匹配不到在线工具却正常最常见的原因不是正则写法而是 shell 对引号的处理。rea test --pattern ^[a-z]$ --string abc在双引号下$会被 shell 解析成变量引用实际传给 rea 的 pattern 变成了^[a-z]加一个空变量自然匹配不到。对策是统一使用单引号并且养成习惯在 rea 命令里看到任何元字符都默认用单引号包裹。第二个常见原因是换行符。如果文件是 Windows 格式CRLF每行末尾会多一个\r字符导致$匹配不到预期位置。rea 内部按splitlines()读取文件能自动处理\r\n但如果你把这段文本直接粘贴到终端里隐藏字符就还在。碰到诡异问题先怀疑不可见字符。rea 里我加了一个--show-escapes小开关可以查看输入中的转义字符排查时非常管用。第三个原因是没有开多行模式。Python 里的^默认只匹配整个字符串开头$只匹配整个字符串结尾不是每一行的开头结尾。如果想逐行匹配要加-m。还有一个容易混淆的点re.MULTILINE让^和$匹配行首尾但不会让.匹配换行后者要re.DOTALL对应 rea 的-s。两者作用不同不要记混。4.2 中文乱码和编码不一致问题我早期处理一批从 Windows 导出的 CSV 时文件在常见记事本工具里打开正常rea 读进来全是乱码。原因是 Python 默认按 UTF-8 读取而旧软件导出的是 GBK/GB18030 编码。解决方法是给 rea 加--encoding参数rea grab --pattern 姓名.* --file data.csv --encoding gbk如果不知道文件到底什么编码先试 UTF-8再试 GB18030。GB18030 是 GBK 的超集兼容性更好。后来我在 rea 里写了一个自动检测逻辑先用 UTF-8 读解码失败再退回 GB18030这个逻辑解决了大部分问题。不过自动检测也有误判的时候最保险的做法仍然是显式传入--encoding。Windows 环境下另一个坑是控制台输出编码。即使 Python 读对了 UTF-8在旧版命令行工具里打印中文还是会乱码。建议统一换成新版终端程序或者在终端里执行chcp 65001把代码页切成 UTF-8。这个问题跟 rea 本身无关但凡是处理中文文本的命令行工具都会遇到。4.3 贪婪匹配吞掉不该吞的内容正则默认是贪婪的.*会尽量匹配到字符串末尾。比如想从bhello/bbworld/b里提取加粗内容写b.*/b结果会匹配整个bhello/bbworld/b而不是两个分开的节点。这是我见过最多的一类问题特别是在处理 HTML 片段时。改成非贪婪rea grab --pattern b(.*?)/b --string bhello/bbworld/b就会分别得到hello和world。非贪婪量词*?让正则尽量少匹配配合后面的/b能停在第一个结束标签。但非贪婪也不是万能药如果结构本身嵌套正则解析还是会出错。遇到嵌套标签更好用的是专门的解析库而不是硬写正则。rea 的文档里也会提醒正则适合处理扁平结构HTML/XML 的嵌套结构别用正则硬刚。还有个衍生问题某些复杂正则会引起灾难性回溯比如(a)$遇到一长串a加一个b结尾回溯量会指数级增长程序看起来像卡死了。rea 没有真正的超时机制所以我的排查经验是先用head -c 5000截断输入判断是逻辑问题还是性能问题再决定怎么优化模式。4.4 大文件读取得注意内存和流式处理rea 的默认读取逻辑是一行一行读逐行匹配这样处理百万行日志时内存占用基本恒定。如果用户传入--whole选项会把整个文件读成一个字符串方便跨行匹配但内存会跟着文件大小线性增长一般不建议对超过几百 MB 的文件使用。实测下在我的笔记本上用默认模式处理 100 万行访问日志提取 IP 并去重大概需要两三秒具体时长与正则复杂度和机器配置有关。如果用了一条很复杂的正则速度会明显下降优化思路有两个一是简化正则避免过多回溯二是先做粗筛再做精筛比如先用简单模式把候选行过滤出来再用完整正则逐条匹配。rea grab --pattern ^192\.168\. --file access.log | rea grab --pattern (?Pstatus\d{3}) --group status管道组合的方式可以避免在一个正则里写太复杂的条件每个 rea 进程都按行流式读取内存压力很小。4.5 交互式 shell 的历史展开和别名干扰在 bash 里!加数字或字符会触发历史展开。如果正则里恰好有!比如匹配感叹号双引号包住时可能会被 shell 劫持。这个不太常见但遇到了会非常困惑。最简单的方法是单引号包裹因为单引号内的所有字符都不会被 shell 解释。还有别名问题。如果系统里已经有名为 rea 的别名指向别的东西执行rea时可能不是你想要的脚本。用type rea可以查看 rea 实际指向哪里。我自己的习惯是在部署后先跑一次rea --version确认输出的是 rea 的版本号避免被环境里的同名命令干扰。4.6 安全边界callback 不是玩具前面提到 sub 的--callback能执行 Python 表达式这既是它的卖点也是风险点。假如规则库文件是从网上复制来的里面藏了一句危险的导入和系统调用你在 rea 里引用规则时就会触发。所以我定了几条规矩不运行来源不明的规则文件callback 只在需要动态处理的临时命令里使用规则库要放进版本管理改动有迹可循。工具本身再方便也不要绕过安全底线。5. 进阶让 rea 变成自己的效率习惯5.1 把自己常用的正则沉淀成规则用一段时间后你会发现自己重复用到某几条正则。例如公司内部的日志格式、导出数据的日期格式、测试数据的 ID 规则。把这些沉淀进 rules.json 里之后每次使用只需要--rule。我比较推荐的入门方式是rea add-rule。它交互式地问你规则名字、描述、pattern然后自动处理转义并写进规则文件。加入后可以直接用rea add-rule rea grab --rule my_log --file app.log --json这里有一个心得规则文件的 pattern 最好写成语义明确、可读性好的版本哪怕略长一点也不要为了短而写得晦涩。因为规则是要反复使用的几个月后回头再看能一眼明白比节省几个字符重要得多。5.2 与管道、脚本和其他工具搭配rea 本身不生产数据也不消费数据它只处理数据流这决定了它的最佳用法是嵌在管道里。例如统计日志中访问次数最多的前 10 个 IPcat access.log | rea grab --rule ipv4 | sort | uniq -c | sort -rn | head -n 10如果 rea 输出的 JSON 格式想继续过滤可以接 jqcat access.log | rea grab --pattern ... --json | jq -r .status | sort | uniq -c这些用法没有引入新的概念只是利用 Unix 哲学让 rea 尽量做好自己那一件事。跟脚本配合时注意把 rea 的完整路径或者确保~/bin在 PATH 中。定时任务里的 PATH 经常不是登录 shell 的 PATH这是脚本里很容易忽略的坑。5.3 给 rea 配置别名和命令补全在 shell 里给 rea 配一个短别名可以显著减少输入成本。我使用的是alias rpython3 ~/bin/rea.py然后r test --pattern ...就可以快速工作。如果你也想在 bash/zsh 里获得子命令补全可以简单写一个补全函数把 test、grab、sub、list-rules、init、add-rule 这几个子命令列进去。补全的代码不长但能明显提升使用体验。这里没有放很长的补全脚本因为补全脚本和 shell 版本强相关照抄不一定能跑更重要的是理解原理补全本质上只是把命令的候选参数交给 shell 提前显示。5.4 能继续扩展的方向rea 目前还比较个人化但我已经在考虑几个很实用的扩展规则库支持远程共享方便团队内部统一常用正则增加--diff选项让替换结果以 diff 形式展示而不是简单打印增加--stats选项输出匹配数量、最长匹配、平均匹配长度等指标把规则库从 JSON 换成更宽容的格式减少手动转义。这些扩展未必都会落地但方向是明确的保持工具简洁同时让复用和协作变得容易。我在实际使用中最明显的感受不是省了多少次浏览器切换而是养成了先验证、再批量、最后检查的习惯。过去我经常写一个正则就直接扔到脚本里跑完才发现匹配结果不对返工成本特别高。有了 rea 之后我习惯先在终端里花几秒验证把分组输出亲眼看到再放进脚本。这个小习惯让处理文本的出错率降了不少。最后再分享一个小技巧任何工具都不要在一开始贪多求全。rea 最初只有 test 和 grab 两个命令sub、规则库、编码参数都是后来按需慢慢加上去的。如果你也想做一个类似的小工具建议先列出自己最常做的三个场景只解决这三个之后自然会知道该加什么。名字是不是叫 rea 不重要重要的是它有没有真正缩短你和文本之间的那条路径。