指南内含 1 场可玩狩猎

贪婪与懒惰量词:为什么 .* 会吃掉太多

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

Regex Hunter 标志

· 阅读约 4 分钟

/<.+?>/

抓住

  • <b>
  • </i>
  • <br>

放过

  • b
  • i>
  • <>

你写了 <.+> 来抓一个 HTML 标签,拿它去匹配 <b>bold</b>,结果它匹配了整个字符串:两个标签加上中间的文本。没有任何东西坏掉。量词只是做了它的本职工作:尽可能多地拿。

本文解释贪婪量词和懒惰量词实际做了什么,为什么两者的区别只体现在匹配到什么上,以及为什么最好的修复往往两者都不是。

01贪婪:先全拿走,再往回吐

量词 *、+、? 和 {n,m} 默认是贪婪的。它们尽可能多地匹配字符,只有在正则其余部分失败时才吐回字符。

匹配1/1 符合预期
/<.+>/
  • <b>bold</b> and <i>it</i>
在沙盒里试试
一个匹配,从第一个 < 到最后一个 >。.+ 一路跑到行尾,然后往回退,直到刚好找到一个 >。

引擎处理 <.+> 的过程是这样的:

  1. < 匹配第一个字符。
  2. .+ 吞掉直到行尾的所有内容。
  3. > 已经没有东西可匹配了,于是引擎回溯:.+ 每次吐回一个字符,直到 > 能匹配上。
  4. 往回退时找到的第一个 >,是这一行的最后一个。匹配成功。

02懒惰:尽可能少拿

在任何量词后面加一个 ?,它就变成懒惰的:尽可能少地匹配字符,只有在正则其余部分失败时才多拿。

匹配1/1 符合预期
/<.+?>/
  • <b>bold</b> and <i>it</i>
在沙盒里试试
四个独立的匹配,每个标签一个。懒惰的 .+? 在能停下的第一个 > 处就停下。

每个量词的懒惰版本:

贪婪 懒惰 含义
* *? 零个或多个
+ +? 一个或多个
? ?? 可选
{2,5} {2,5}? 2 到 5 个

带引号的字符串也一样。贪婪版本从第一个引号跑到最后一个引号,懒惰版本在下一个引号处停下:

匹配1/1 符合预期
/".*?"/
  • say "hi" and "bye"
在沙盒里试试
两个匹配,"hi" 和 "bye"。贪婪的 ".*" 会把 "hi" and "bye" 当成一个字符串匹配,连中间的引号也包括在内。

03往往比两者都好:否定字符类

懒惰量词仍然会拿 . 去试每个字符,包括你想停下的那个字符。否定字符类直接写出停止规则:“除了 > 以外的任何字符”。

匹配1/1 符合预期
/<[^>]+>/
  • <b>bold</b> and <i>it</i>
在沙盒里试试
和懒惰版本一样抓到四个标签,但规则写在字符类本身里。[^>] 永远不会越过 >。

<[^>]+> 和 "[^"]*" 通常是更好的选择:

  • 更清晰。正则描述的是标签是什么,而不是引擎该怎么去找它。
  • 更快。没有回溯:[^>]+ 按定义就会在第一个 > 处停下。
  • 能跨行。. 默认不匹配换行符,所以 <.+?> 会漏掉被拆成两行的标签。[^>] 也能匹配换行符。

04意外之处:懒惰不等于“最短匹配”

懒惰的意思是从匹配开始的位置算起尽可能短。它不会移动起点。引擎总是报告它能找到的最靠左的匹配:

匹配1/1 符合预期
你可能以为会得到最短的 "ab"。但匹配从第一个 a 开始,懒惰的 .*? 只能从那里一路伸展到 b。

如果你想要中间不含 a 的最短 a…b,那就再用字符类明确写出来:a[^a]*b。

05为什么在正则高尔夫里无关紧要

给打高尔夫的玩家一个有用的事实:贪婪还是懒惰,永远不会改变一个字符串是否匹配。a.*b 和 a.*?b 匹配的单词集合完全相同,区别只在于它们报告文本的哪一部分。狩猎只问“这个单词匹配吗?”,所以在量词后面加 ? 会多花一个字符,却什么也换不来。

真正对棋盘有帮助的是否定字符类,因为它会改变能匹配什么。在这块棋盘上,每个怪物都恰好有一个 a:

可玩狩猎标准杆 13

抓住恰好有一个 a 的单词。

目标 · 抓住 0/5

  • cat目标
  • bread目标
  • snake目标
  • plant目标
  • magic目标

无辜词 · 误抓 0

  • banana无辜
  • data无辜
  • salad无辜
  • plum无辜
  • tree无辜

输入正则——棋盘会实时更新。

在沙盒里试试
喜欢吗?每天都有新的狩猎。玩今天的狩猎

06速查

  • 贪婪(.*、.+):尽可能多地匹配,然后往回退。这是默认行为。
  • 懒惰(.*?、.+?):尽可能少地匹配,然后再延伸。加一个 ?。
  • 否定字符类([^>]*):匹配除停止字符以外的任何字符。当你知道该在哪里停下时,它通常是最好的工具。
  • 懒惰和贪婪匹配的是相同的字符串。区别在于它们捕获、提取或替换的内容。

07继续练习