贪婪与懒惰量词:为什么 .* 会吃掉太多
正则表达式的量词会尽可能多地抓取文本。本文讲清贪婪匹配和懒惰匹配到底如何工作,为什么 <.+> 会吞掉一整行 HTML,以及什么时候否定字符类比两者都好用。

· 阅读约 4 分钟
/<.+?>/
抓住
- <b>
- </i>
- <br>
放过
- b
- i>
- <>
你写了 <.+> 来抓一个 HTML 标签,拿它去匹配 <b>bold</b>,结果它匹配了整个字符串:两个标签加上中间的文本。没有任何东西坏掉。量词只是做了它的本职工作:尽可能多地拿。
本文解释贪婪量词和懒惰量词实际做了什么,为什么两者的区别只体现在匹配到什么上,以及为什么最好的修复往往两者都不是。
01贪婪:先全拿走,再往回吐
量词 *、+、? 和 {n,m} 默认是贪婪的。它们尽可能多地匹配字符,只有在正则其余部分失败时才吐回字符。
引擎处理 <.+> 的过程是这样的:
<匹配第一个字符。.+吞掉直到行尾的所有内容。>已经没有东西可匹配了,于是引擎回溯:.+每次吐回一个字符,直到>能匹配上。- 往回退时找到的第一个
>,是这一行的最后一个。匹配成功。
02懒惰:尽可能少拿
在任何量词后面加一个 ?,它就变成懒惰的:尽可能少地匹配字符,只有在正则其余部分失败时才多拿。
每个量词的懒惰版本:
| 贪婪 | 懒惰 | 含义 |
|---|---|---|
* |
*? |
零个或多个 |
+ |
+? |
一个或多个 |
? |
?? |
可选 |
{2,5} |
{2,5}? |
2 到 5 个 |
带引号的字符串也一样。贪婪版本从第一个引号跑到最后一个引号,懒惰版本在下一个引号处停下:
03往往比两者都好:否定字符类
懒惰量词仍然会拿 . 去试每个字符,包括你想停下的那个字符。否定字符类直接写出停止规则:“除了 > 以外的任何字符”。
<[^>]+> 和 "[^"]*" 通常是更好的选择:
- 更清晰。正则描述的是标签是什么,而不是引擎该怎么去找它。
- 更快。没有回溯:
[^>]+按定义就会在第一个>处停下。 - 能跨行。
.默认不匹配换行符,所以<.+?>会漏掉被拆成两行的标签。[^>]也能匹配换行符。
04意外之处:懒惰不等于“最短匹配”
懒惰的意思是从匹配开始的位置算起尽可能短。它不会移动起点。引擎总是报告它能找到的最靠左的匹配:
如果你想要中间不含 a 的最短 a…b,那就再用字符类明确写出来:a[^a]*b。
05为什么在正则高尔夫里无关紧要
给打高尔夫的玩家一个有用的事实:贪婪还是懒惰,永远不会改变一个字符串是否匹配。a.*b 和 a.*?b 匹配的单词集合完全相同,区别只在于它们报告文本的哪一部分。狩猎只问“这个单词匹配吗?”,所以在量词后面加 ? 会多花一个字符,却什么也换不来。
真正对棋盘有帮助的是否定字符类,因为它会改变能匹配什么。在这块棋盘上,每个怪物都恰好有一个 a:
抓住恰好有一个 a 的单词。
目标 · 抓住 0/5
- cat目标
- bread目标
- snake目标
- plant目标
- magic目标
无辜词 · 误抓 0
- banana无辜
- data无辜
- salad无辜
- plum无辜
- tree无辜
输入正则——棋盘会实时更新。