开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情
量词乍一看非常简单,但实际上它们可能很棘手。
如果我们打算寻找比 /\d+/
更复杂的东西,就需要理解搜索的工作原理。
以接下来的任务为例。
有一个文本,我们需要用书名号:«...»
来代替所有的引号 "..."
。在许多国家,书名号是排版的首选。
例如:"Hello, world"
应该变成 «Hello, world»
。还有其他引用,例如 „Witam, świat!”
(波兰语)或 「你好,世界」
(中文),但对于我们的任务,让我们选择 «...»
吧。
首先要做的是定位带引号的字符串,然后替换它们。
像 /".+"/g
(一个引号,然后是一些内容,然后是另一个引号)这样的正则表达式看起来可能很合适,但事实并非如此!
让我们试一下:
let regexp = /".+"/g;
let str = 'a "witch" and her "broom" is one';
alert( str.match(regexp) ); // "witch" and her "broom"
……可以看出来它的运行结果与预期不同!
它没有找到匹配项 "witch"
和 "broom"
,而是找到:"witch" and her "broom"
。
这可被称为“贪婪是万恶之源”。
贪婪搜索
为了查找到一个匹配项,正则表达式引擎采用了以下算法:
-
对于字符串中的每一个位置
- 尝试匹配该位置的模式。
- 如果未匹配,则转到下一个位置。
这样简单的描述并不能说清楚这个正则表达式匹配失败的原因,所以让我们详细说明一下模式 ".+"
是如何进行搜索的。
-
该模式的第一个字符是一个引号
"
。正则表达式引擎尝试在源字符串
a "witch" and her "broom" is one
的位置 0 找到它,但那里有a
,所以匹配失败。然后继续前进:移至源字符串中的下一个位置,并尝试匹配模式中的第一个字符,再次失败,最终在第三个位置匹配到了引号:
-
找到引号后,引擎就尝试去匹配模式中的剩余字符。它尝试查看剩余的字符串是否符合
.+"
。在我们的用例中,模式中的下一个字符为
.
(一个点)。它表示匹配除了换行符之外的任意字符,所以将会匹配下一个字符'w'
: -
然后由于量词
.+
,点会重复。正则表达式引擎一个接一个字符地进行匹配。……什么时候会不匹配?点(.)能够匹配所有字符,所以只有在移至字符串末尾时才停止匹配:
-
现在引擎完成了对重复模式
.+
的搜索,并且试图寻找模式中的下一个字符。是引号"
。但是有一个问题:对字符串的遍历已经结束,没有更多字符了!正则表达式引擎知道它为
.+
匹配太多项了,所以开始 回溯。换句话说,它去掉了量词匹配项的最后一个字符:
现在它假设
.+
的匹配在字符串的倒数第一个字符前的位置结束,并尝试从该位置匹配模式的剩余部分。如果那里有引号,则搜索将结束,但最后一个字符是
'e'
,所以不匹配。 -
……所以引擎会将
.+
的重复次数减少一个字符:引号
'"'
与'n'
不匹配。 -
引擎不断进行回溯:它减少
'.'
的重复次数,直到模式的其余部分(在我们的用例中是'"'
)匹配到结果: -
匹配完成。
-
所以,第一次匹配项是
"witch" and her "broom"
。如果正则表达式具有修饰符g
,则搜索将从第一个匹配结束的地方继续。字符串is one
的剩余部分不再有引号,因此没有更多匹配项。
这可能不是我们所期望的,但这就是它的工作方式。
在贪婪模式下(默认情况),量词都会尽可能多地重复。
正则表达式引擎尝试用 .+
去匹配尽可能多的字符,然后在模式的其余部分不匹配时再将其逐一缩短。
对于这个任务,我们想要得是另一种结果。这也就是惰性量词模式的用途。