正则匹配不上?一份调试清单
看起来没问题的正则却匹配不上,原因通常是十几种情况之一:隐藏字符、丢失的反斜杠、没转义的点号、缺少锚点或修饰符。下面按排查顺序逐一列出,并附可以动手试的示例。

· 阅读约 3 分钟
/1\.5/
抓住
- 1.5
- v1.5.2
放过
- 125
- 1x5
正则看起来没错却匹配不上时,模式本身很少有什么有趣的错误。通常是十几种寻常原因之一:输入里有你看不见的字符、字符串中的反斜杠丢了、特殊字符没转义、缺了锚点或修饰符,或者代码运行正则的方式和你的测试工具不同。下面是一份按排查顺序排列的清单,附实时示例。
示例使用 JavaScript 的引擎;Python 或其他语言行为不同的地方,对应条目会注明。
01先来一个 60 秒排查法
- 原样打印输入。 在 Python 中,
print(repr(text))会把隐藏字符显示为转义形式,比如\xa0(不换行空格)、\u200b(零宽空格)或\r。在 JavaScript 中,[...text].map(c => c.codePointAt(0).toString(16))会列出每个字符的码点,a0或200b一眼就能看出来。 - 缩小模式。 从末尾开始一段段删掉,直到能匹配,再一段段加回去。加回哪段就出错,bug 就在哪段。
- 写两组测试行: 几行必须匹配的,几行必须不匹配的。把两组都粘贴到沙盒,它的“结构解析”面板会用通俗中文解释模式的每个部分。
02输入和你想的不一样
从网页、PDF 和聊天软件复制来的文本,常常含有看着像普通字符、其实不是的字符:用不换行空格代替了空格,单词中间夹着零宽空格,用弯引号代替了直引号。下面两行看起来一模一样。第二行冒号后面是不换行空格,所以用普通空格的模式匹配不到它:
在 JavaScript 中,\s 两种空格都能匹配,所以当来源不可信时,就用它来解决:
零宽空格更麻烦:hello 打印出来是“hello”,却有六个字符,^hello$ 匹配不上它。弯引号 “hi” 匹配不上 "hi"。打印输入(上面的第 1 步)能把这些全部暴露出来。
03反斜杠没能保留下来
写在普通字符串里的模式,在正则引擎看到它之前就丢了反斜杠:
- JavaScript:
new RegExp("\d+")变成了模式d+,它匹配ddd而不是42。应写成new RegExp("\\d+"),或使用字面量:/\d+/。 - Python: 始终使用原始字符串
r"\bcat\b"。没有r的话,"\b"是退格符,所以"\bcat\b"永远匹配不到a cat,而且不会报任何错。 - Java: 源代码里每个反斜杠都要写两次:
"\\d+"、"\\."。
如果模式在测试工具里能用、在代码里却不行,首先怀疑这一点。
04特殊字符没转义
. * + ? ^ $ | ( ) [ ] { } \ 都有特殊含义。没转义的点号是经典案例,因为模式仍然“能用”,只是用过头了:1.5 会匹配 125。
价格里的 $、URL 里的 ?、电话号码里的 + 也一样:在前面加反斜杠,或者把字符放进 [.]、[+] 这样的字符类。
05只匹配了一部分,或者完全没匹配
正则搜索会在文本中任意位置查找匹配。\d{3} 会“验证通过” 12345abc,因为其中某处出现了三个数字。做校验时要锚定两端:^\d{3}$。
Python 还有个坑:re.match 只锚定开头,所以 re.match(r"\d{3}", "12345") 会成功。要匹配“整个字符串”,请用 re.fullmatch。
反过来的问题:除非设置 m 修饰符,否则 ^ 和 $ 表示整段文本的开头和结尾,所以在加上 m 之前,^ERROR 在日志第 5 行什么也找不到。还有,Windows 换行符(\r\n)的文件会让 Python 栽跟头:使用 re.M 时,它的 $ 不会在 \r 之前匹配,所以 ^abc$ 在 abc\r\n 上会失败。JavaScript 的 $ 则会在那里匹配。先去掉 \r,或者允许它出现:^abc\r?$。
更多内容见正则锚点。
06缺少修饰符
i: 没有它,hello匹配不上Hello。s: 没有它,.不匹配换行符,所以a.b匹配不到分处两行的a和b。g: 没有它,JavaScript 的replace只替换第一个匹配:"a-b-c".replace(/-/, "+")得到a+b-c。u: 没有它,一个 emoji 算作两个字符,所以^.$匹配不上😀。
07g 修饰符会记住上次停在哪里(JavaScript)
带 g 修饰符的正则会在多次调用之间保留 lastIndex。用 test() 重复调用它,结果会交替变化:
const re = /cat/g;
re.test("cat"); // true
re.test("cat"); // false: the search started at index 3
re.test("cat"); // true again
用 test() 时去掉 g,或者在函数内部创建正则。在没跑到末尾就结束的循环里使用 exec(),也是同样的问题。
08匹配得太多了
.* 是贪婪的:在 <b>x</b><b>y</b> 中,模式 <b>.*</b> 会从第一个 <b> 一直吃到最后一个 </b>。改用 .*?,或者更好的做法是用不会越过结尾的字符类,比如 [^<]*。贪婪与懒惰量词有详细讲解。
09字符类的意思和你以为的不同
[a-Z]会报错(“range out of order”):大写字母排在小写字母前面。应写成[a-zA-Z]。- 中间的连字符会构成范围。
[\w-.]在不加u修饰符时碰巧能用,加上就会报错。把连字符放在开头或末尾:[\w.-]。 ^只有在最开头才表示取反。[^abc]是“不是 a、b 或 c”;[a^bc]是“a、^、b 或 c”。
10你的测试工具用的是另一个引擎
在 PCRE 测试工具里写好的模式,放到 JavaScript、Python 或 Go 里可能会失败:命名分组写法不同,后行断言的限制不同,而 Go 的引擎根本不支持环视。表格见正则引擎对比。请在你的代码实际使用的引擎中测试;沙盒使用的是 JavaScript 的引擎。
11不是失败,而是卡住了
如果正则处理某些输入要花好几秒,那就不是匹配不上的问题了:像 (a+)+ 这样的嵌套量词会让回溯型引擎尝试数量巨大的路径。灾难性回溯介绍了如何发现并修复它。
12防止问题再次出现
把测试行和模式放在一起:写一段注释,列出两三行必须匹配的和两三行必须不匹配的,或者写一个单元测试。沙盒可以用一个链接同时分享模式和你的测试行,代码评审时很方便。如果你想让这些调试反应变成本能,每日狩猎就是专门练这个的五分钟:要抓住的行、要放过的行,还有一个要不断修改、直到两边都对的模式。