正则表达式进阶:分组、捕获、反向引用、零宽断言
好,我们接着往下走。基础的正则符号能匹配简单模式,但真要对付敏感数据——比如身份证号、银行卡号、手机号——光靠[0-9]{18}可不够。你想想看,一个18位数字串可能是身份证,也可能只是订单编号。怎么区分?
这就得靠分组、捕获、反向引用这些进阶武器了。我刚开始用正则时也觉得分组没啥了不起,直到有一次在日志里匹配嵌套括号……嗯,那天的调试经历让我记住了所有坑。
分组——把模式装进口袋
分组就是用小括号 () 把一段模式包起来。作用有两个:一是改变优先级,二是把匹配结果存下来。
// 匹配重复的单词(如"the the")
(\w+)\s+\1
这里的 \1 就是反向引用——它会引用第一个分组匹配到的具体内容。说白了,就是“刚才抓到啥,现在再抓一遍一样的”。
(?P<name>...) 能让代码可读性翻倍。我在处理JSON日志时特别喜欢用命名分组,回溯起来一目了然。
捕获与非捕获
默认的分组都是捕获分组——正则引擎会记住它们匹配的文本。但有时候你只是需要括号改变优先级,并不想存结果。那就用非捕获分组 (?:...)。
// 非捕获分组:只用于逻辑分组,不占用捕获编号
(?:0\d{2,3})?[1-9]\d{6,7}
上面匹配电话号码时,区号部分用了非捕获,省去了不必要的内存开销。我见过有人写大型正则时疯狂用捕获分组,结果反向引用编号乱成一团……
| 分组类型 | 语法 | 是否会捕获 | 用途 |
|---|---|---|---|
| 捕获分组 | (...) | 是 | 需要引用或提取 |
| 非捕获分组 | (?:...) | 否 | 仅逻辑分组 |
| 命名捕获分组 | (?P<name>...) | 是(按名引用) | 增强可读性 |
反向引用——让模式自我引用
反向引用能在同一正则里引用前面捕获的内容。比如匹配重复的敏感词、标签对、连续相同的分隔符等场景。
// 匹配 HTML 标签对(简化版)
<(\w+)>.*?</\1>
这里 \1 就是第一个分组捕获的标签名,确保开闭标签一致。我曾经用这个模式解析过一批爬虫数据,避免了手动校验错配的问题。
零宽断言——看路不看地
零宽断言是正则里最“聪明”的特性。它不消耗字符,只检查当前位置的前后是否符合条件。说白了,就是“我只看一眼,但不往前走”。
- 正向先行断言
(?=...):后面必须是…… - 正向后行断言
(?<=...):前面必须是…… - 负向先行断言
(?!...):后面不能是…… - 负向后行断言
(?<!...):前面不能是……
举个例子:我们要匹配不是跟在“敏感”后面的“数据”。用负向后行断言就特别干净:
(?<!敏感)数据
它只匹配前面没有“敏感”二字的“数据”。我在做内容审核时,用这个过滤掉了大量误报。
实战:提取银行卡号中的发卡行
银行卡号前6位是发卡行识别码(BIN)。我们可以用分组 + 零宽断言精准提取:
// 匹配以 62 开头、后跟 3 位数字(银联标准卡)
(?<=^)62\d{3}(?=\d{12,15}$)
这里用 (?<=^) 确保行首,(?=\d{12,15}$) 确保后面还有足够位数。分组只捕获 BIN 部分,方便后续查表匹配发卡行。
知识体系总览
下面这张图梳理了本章所有概念的关系,方便你对照记忆:
总结几个避坑经验
- 捕获分组数量过多时:我曾经在一条规则里用了十几个分组,结果每次修改都要数
\1到\9,那叫一个痛苦。后来强制自己用命名分组,或者用非捕获分组减少编号。 - 零宽断言的位置陷阱:断言只是条件检查,不是匹配字符。如果你用
(?<=abc)def,它匹配的是“def”本身,而不是“abcdef”。我见过有人写(?<=abc)def以为能匹配整串,结果替换时只替换了“def”,前面没动。 - 反向引用与性能:反向引用会让正则引擎回溯,在超长文本中可能导致灾难性回溯。如果可能,优先用更简单的模式或预编译。
好了,这一章的核心就是这些。分组给你拆分结构的能力,反向引用让你能表达重复模式,零宽断言则帮你做精确定位。把这些东西组合起来,敏感数据识别就从“瞎蒙”变成了“精准打击”。