☰
rea:一个自制的命令行正则表达式调试与批处理工具
2026/10/12 12:34:46 网站建设 项目流程

"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 '(?P<ip>\d{1,3}(?:\.\d{1,3}){3})' --file access.log --unique

这里用命名分组(?P<ip>...)把 IP 单独拎出来。默认情况下,grab 输出的是完整匹配内容;如果只想输出某个分组,可以使用--group ip。加了--unique会去重,适用于统计有多少个不同 IP。配合--json可以输出 JSON 对象,方便后面接 jq 或其他脚本处理。

grab 最大的价值是批量。在线正则工具只能测一条,grab 可以直接扫完整个文件,输出所有匹配结果。我经常这样统计日志:

rea grab --pattern 'HTTP/1.1" (?P<status>\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 '(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})' --sub '\g<year>-\g<month>-\g<day>' --file report.txt --dry-run

这个例子把20250413这类日期格式改成2025-04-13,\g<year>是命名分组反向引用。--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 五分钟部署一个可用的 rea

rea 就是单个 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 init

rea init会建立~/.rea目录并生成默认的 rules.json 和 history.txt。如果用的是 Windows,我建议在 Git Bash 或者新版终端程序的 Linux 环境里操作,原因不是路径放不下,而是文件编码和换行符会让正则的.、^$行为跟 Linux 不一致。这些坑后面会详细说。

部署完成后,第一时间测试核心流程:

rea test --pattern '^\w+@\w+\.\w+$' --string 'alice@example.com'

能看到match: True,说明环境基本正常。rea 本身还支持从标准输入读取数据,所以它不是一个孤立的命令,而是可以嵌到管道里的一个环节:

cat access.log | rea grab --rule status_code | sort | uniq -c

3.2 场景一:从访问日志里提取关键信息并统计

假设拿到一份访问日志,单行格式如下:

192.168.1.10 - - [13/Apr/2025:10:24:36 +0800] "GET /api/users?page=2 HTTP/1.1" 200 5387 "-" "Mozilla/5.0"

我想提取 IP、时间、请求方法和路径、状态码。先写一个可读性较好的正则,用命名分组把每个字段分开:

rea grab \ --pattern '^(?P<ip>(?:\d{1,3}\.){3}\d{1,3}) .*?\[(?P<time>[^\]]+)\] "(?P<method>\w+) (?P<path>\S+) HTTP/[0-9.]+" (?P<status>\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?page=2","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_(?P<y>\d{4})(?P<m>\d{2})(?P<d>\d{2})\.txt$' --sub '\g<y>-\g<m>-\g<d>_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 '(?P<user>\w{3})\w*@(?P<domain>\w+\.\w+)' \ --sub '\g<user>***@\g<domain>' \ --file users.csv --dry-run

原始行:

1,alice@example.com 2,bobsmith@example.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 贪婪匹配:吞掉不该吞的内容

正则默认是贪婪的,.*会尽量匹配到字符串末尾。比如想从<b>hello</b><b>world</b>里提取加粗内容,写<b>.*</b>,结果会匹配整个<b>hello</b><b>world</b>,而不是两个分开的节点。这是我见过最多的一类问题,特别是在处理 HTML 片段时。

改成非贪婪:

rea grab --pattern '<b>(.*?)</b>' --string '<b>hello</b><b>world</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 '"(?P<status>\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 个 IP:

cat access.log | rea grab --rule ipv4 | sort | uniq -c | sort -rn | head -n 10

如果 rea 输出的 JSON 格式想继续过滤,可以接 jq:

cat 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 r='python3 ~/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 不重要,重要的是它有没有真正缩短你和文本之间的那条路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询