贪婪量词(Greedy)
默认情况下,量词是贪婪的:它们会尽可能多地匹配字符,然后在必要时回退(回溯)。
/<.+>/
<b>bold</b> text
贪婪的 .+ 尽可能多匹配,从第一个 < 一直到最后一个 >
这通常不是我们想要的——我们希望分别匹配 <b> 和 </b>。
懒惰量词(Lazy / Non-Greedy)
在量词后加 ?,让它变成懒惰的:尽可能少地匹配:
| 贪婪 | 懒惰(加 ?) |
|---|---|
| * | *? |
| + | +? |
| ? | ?? |
| {n,m} | {n,m}? |
/<.+?>/g
<b>bold</b> text
懒惰的 .+? 尽可能少匹配,遇到第一个 > 就停止
# 提取所有 HTML 标签 re.findall(r'<.+?>', '<b>bold</b> and <i>italic</i>') # → ['<b>', '</b>', '<i>', '</i>'] # 提取引号内的内容 re.findall(r'".*?"', '"hello" and "world"') # → ['"hello"', '"world"']
回溯机制(Backtracking)
理解引擎的工作方式是避免性能问题的关键。以 /a.*b/ 匹配字符串 "aXYZb" 为例:
① 引擎到达
.*,贪婪匹配,吃掉所有字符:aXYZb → 指针在末尾② 接下来需要匹配
b,但已经到末尾 → 失败③ 回溯:
.* 吐出最后一个字符 b → 指针在 b 前④ 尝试匹配
b → 成功!整体匹配 aXYZb灾难性回溯(Catastrophic Backtracking)
某些正则组合会导致指数级的回溯,使匹配时间极长甚至卡死:
DANGER这类问题被称为 ReDoS(Regular Expression Denial of Service)。一个精心构造的输入可以让正则引擎运行数秒甚至数分钟,可用于攻击服务器。
# 危险的嵌套量词 ^(a+)+$ # 匹配 "aaaaab" 会发生指数级回溯 (a|aa)+ # 同样危险 # 修复方案:明确语义 ^a+$ # 清晰、高效
常见触发场景
- 嵌套量词:
(a+)+、(\w*\s)+ - 有重叠的选择:
(a|ab)+ - 过于宽泛的
.*嵌套在复杂模式中
占有性量词(Possessive,部分引擎)
加 + 变成占有性量词,匹配后绝不回溯(Java、PHP/PCRE 支持):
a++ # 贪婪匹配所有 a,然后绝不退还 \w++ # 同理
Python 的 re 模块不支持占有性量词;regex 第三方库支持。
原子组(Atomic Group,部分引擎)
(?>\d+) # 原子组,匹配后不回溯,等价于占有性量词 \d++
贪婪 vs 懒惰的选择原则
| 场景 | 推荐 | 原因 |
|---|---|---|
| 匹配到下一个固定分隔符 | 懒惰 .*? | 更精确,防止跨越分隔符 |
| 已知内容不含某字符 | 否定类 [^"]* | 性能更好,无回溯 |
| 需要匹配尽可能多的内容 | 贪婪 .* | 默认行为 |
# ❌ 用懒惰量词匹配引号内内容(有回溯) ".*?" # ✅ 用否定字符类(无回溯,性能更好) "[^"]*"
小结
- 贪婪量词尽可能多匹配;懒惰量词(加
?)尽可能少匹配 - 正则引擎通过回溯来尝试不同的匹配路径
- 嵌套量词和有重叠的选择会导致灾难性回溯(ReDoS)
- 用否定字符类
[^x]*替代懒惰量词,性能更好 - 占有性量词(
++)和原子组(?>...)可以禁止回溯