贪婪量词和惰性量词

38 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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"

这可被称为“贪婪是万恶之源”。

贪婪搜索

为了查找到一个匹配项,正则表达式引擎采用了以下算法:

  • 对于字符串中的每一个位置

    • 尝试匹配该位置的模式。
    • 如果未匹配,则转到下一个位置。

这样简单的描述并不能说清楚这个正则表达式匹配失败的原因,所以让我们详细说明一下模式 ".+" 是如何进行搜索的。

  1. 该模式的第一个字符是一个引号 "

    正则表达式引擎尝试在源字符串 a "witch" and her "broom" is one 的位置 0 找到它,但那里有 a,所以匹配失败。

    然后继续前进:移至源字符串中的下一个位置,并尝试匹配模式中的第一个字符,再次失败,最终在第三个位置匹配到了引号:

  2. 找到引号后,引擎就尝试去匹配模式中的剩余字符。它尝试查看剩余的字符串是否符合 .+"

    在我们的用例中,模式中的下一个字符为 .(一个点)。它表示匹配除了换行符之外的任意字符,所以将会匹配下一个字符 'w'

  3. 然后由于量词 .+,点会重复。正则表达式引擎一个接一个字符地进行匹配。

    ……什么时候会不匹配?点(.)能够匹配所有字符,所以只有在移至字符串末尾时才停止匹配:

  4. 现在引擎完成了对重复模式 .+ 的搜索,并且试图寻找模式中的下一个字符。是引号 "。但是有一个问题:对字符串的遍历已经结束,没有更多字符了!

    正则表达式引擎知道它为 .+ 匹配太多项了,所以开始 回溯

    换句话说,它去掉了量词匹配项的最后一个字符:

    现在它假设 .+ 的匹配在字符串的倒数第一个字符前的位置结束,并尝试从该位置匹配模式的剩余部分。

    如果那里有引号,则搜索将结束,但最后一个字符是 'e',所以不匹配。

  5. ……所以引擎会将 .+ 的重复次数减少一个字符:

    引号 '"' 与 'n' 不匹配。

  6. 引擎不断进行回溯:它减少 '.' 的重复次数,直到模式的其余部分(在我们的用例中是 '"')匹配到结果:

  7. 匹配完成。

  8. 所以,第一次匹配项是 "witch" and her "broom"。如果正则表达式具有修饰符 g,则搜索将从第一个匹配结束的地方继续。字符串 is one 的剩余部分不再有引号,因此没有更多匹配项。

这可能不是我们所期望的,但这就是它的工作方式。

在贪婪模式下(默认情况),量词都会尽可能多地重复。

正则表达式引擎尝试用 .+ 去匹配尽可能多的字符,然后在模式的其余部分不匹配时再将其逐一缩短。

对于这个任务,我们想要得是另一种结果。这也就是惰性量词模式的用途。