Chapter 05 / 10

贪婪、懒惰与回溯机制

理解正则引擎的工作原理,避免性能陷阱与灾难性回溯

贪婪量词(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+$       # 清晰、高效

常见触发场景

占有性量词(Possessive,部分引擎)

+ 变成占有性量词,匹配后绝不回溯(Java、PHP/PCRE 支持):

a++   # 贪婪匹配所有 a,然后绝不退还
\w++  # 同理

Python 的 re 模块不支持占有性量词;regex 第三方库支持。

原子组(Atomic Group,部分引擎)

(?>\d+)  # 原子组,匹配后不回溯,等价于占有性量词 \d++

贪婪 vs 懒惰的选择原则

场景推荐原因
匹配到下一个固定分隔符懒惰 .*?更精确,防止跨越分隔符
已知内容不含某字符否定类 [^"]*性能更好,无回溯
需要匹配尽可能多的内容贪婪 .*默认行为
# ❌ 用懒惰量词匹配引号内内容(有回溯)
".*?"

# ✅ 用否定字符类(无回溯,性能更好)
"[^"]*"

小结