无过
发布于 2026-10-01 / 0 阅读
0
0

正则表达式里最容易翻车的 5 个地方

正则写错从来不报错 —— 它只是安静地匹配到不该匹配的东西。下面这 5 个坑,共同点是「看起来对、跑起来错」。

坑 1:\b 在符号之间并不存在

这是最反直觉的一个。

\b 表示「词边界」,它的定义是:一侧是字类字符(\w:字母、数字、下划线),另一侧不是。关键在于 —— 两个符号之间不存在边界。

我解析 ls -l 风格的输出时想匹配权限字段,于是写了:

m = re.match(r'^([-dl][rwx-]{9})\b\s+(.+)$', line)
#                                ↑ 这里永远不会匹配成功

因为 -rw-r--r-- 的结尾是 -(非字类字符),紧跟着的又是空格(非字类字符)—— \b 要求的「一侧是字类字符」根本不成立。

解法:改用前瞻断言,把「后面是空白或行尾」明确写出来:

m = re.match(r'^([-dl][rwx-]{9})(?=\s|$)\s+(.+)$', line)

(?=\s|$) 是零宽断言:只要求「后面是空白或行尾」,不消耗任何字符。

教训:\b 表达的是「字类与非字类的交界」,不是「符号串结束了」。当你处理的是由符号组成的字段(权限、哈希、版本号、ID、路径),用 (?=...) 而不是 \b。

坑 2:字符类里的 - 是个位置敏感的字符

在 [...] 内部,- 只有放在首或尾才是字面量,放在中间表示范围:

r'[a-z]'     # a 到 z 的范围
r'[-az]'     # 字面量 -、a、z
r'[az-]'     # 同上:- 在末尾
r'[a\-z]'    # 显式转义,也是字面量
r'[a-z-]'    # 范围 a-z,外加字面量 -

真正危险的是 [\w-] 这种写法:它在 Python 里能跑,但意图模糊 —— 后人想加个字符时很容易改错位置,把一个字面量变成范围。

习惯写法:把 - 固定在末尾,或者显式转义。

坑 3:贪婪匹配的「吃过头」

.* 是贪婪的:它先一路吃到行尾,再往回退。用「两个分隔符之间的内容」时,它会匹配到最后一个分隔符:

s = 'a=1; b=2; c=3'
re.search(r'a=(.*);', s).group(1)    # '1; b=2'  ← 吃过头了

两种修法,推荐后一种:

re.search(r'a=(.*?);', s).group(1)    # 非贪婪:'1'
re.search(r'a=([^;]*);', s).group(1)  # 排除式:'1'

[^;]* 通常比 .*? 更好:它不需要回溯,在长文本上性能差异明显,而且意图明确 —— 「一直吃到遇到分隔符为止」。

经验法则:能用「排除式字符类」就別用「懒惰量词」。

坑 4:把用户输入直接拼进模式

pattern = r'^' + user_input + r'$'      # 危险

如果输入里含有 .、*、(、| 等元字符,轻则匹配错误,重则构造出灾难性回溯的模式(ReDoS)—— 一段几十字符的输入就能让匹配耗时爆炸。

解法:

pattern = r'^' + re.escape(user_input) + r'$'

更硬的规则:只要模式里带变量,就先假设它是不可信输入。能用 str.startswith / in / == 表达的,就不要用正则。

坑 5:\w、\d、. 的默认语义和直觉不一样

三个反直觉点:

写法直觉实际(Python 3 默认)
\w英文字母、数字、下划线包含中文等 Unicode 字母
\d0-9包含全角数字等其它数字字符
.任意字符不匹配换行符

后果举例:用 \w+ 提取「用户名」,会把中文一起吞进去;用 . 跨行匹配,永远失败。

解法:把语义显式写出来。

re.findall(r'\w+', text, re.ASCII)   # 退回 ASCII 语义
re.DOTALL                            # 让 . 也匹配换行
re.MULTILINE                         # 让 ^ $ 匹配每一行的首尾

习惯:用到 ^ $ 就明确是否要 re.MULTILINE;用到 . 就明确是否要 re.DOTALL。默认值经常不是你想要的。

附:调试正则的三个小技巧

  1. 小步验证。不要一次写完整个模式。先写「能匹配到那一行」,再逐段往两边加,每一步都验证。
  2. 把命中内容包起来看边界。findall 只给你结果,看不出边界对不对:
for m in re.finditer(pattern, text):
    print('<%s>' % m.group(0))

两侧的 <> 会暴露多余的空格和符号。

  1. 失败时先打印输入原文。模式返回 0 条结果时,先用 repr(text) 打出输入 —— 相当一部分「正则不工作」其实是输入里有多余空格、不可见字符或编码问题,和正则没关系。

小结

正则的难点不在语法,而在边界:\b 在哪里成立、贪婪匹配在哪里停下、\w 到底包含什么。

遇到诡异结果时,先回到一个问题:这个位置到底有没有我假设的那个边界?

把假设写成显式断言((?=...)、[^;]、re.ASCII),比记住更多语法管用。


评论