JavaScript中的贪婪表现不同?

时间:2014-01-04 12:56:59

标签: javascript regex regex-greedy

this question让我意识到量词的贪婪在某些正则表达式引擎中并不总是相同。从该问题中取出正则表达式并对其进行修改:

!\[(.*?)*\]

(我知道*在这里是多余的,但我发现以下内容是一个非常有趣的行为。)

如果我们尝试匹配:

![][][]

我希望第一个捕获组为空,因为(.*?)是懒惰的,并且会在它遇到的第一个]处停止。这确实发生在:

我查看了一些其他语言,例如rubyjavaC#但所有行为都像我预期的那样(即返回空捕获组)。

(regexplanet的golang风味显然也会获得非空捕获组)

似乎JavaScript的正则表达式引擎正在解释第二个*以将.*?从懒惰转换为贪婪。请注意,将第二个*转换为*?似乎可以使正则表达式按预期工作(完全删除量词,因为我知道在这种情况下它是多余的,但这不是重点)

正则表达式中使用了

*,但此行为与+?{m,n}类似,将其转换为懒惰版本会产生与*?

有谁知道真正发生了什么?

3 个答案:

答案 0 :(得分:10)

简短回答

行为在{strong> 15.10.2模式语义一节的ECMA-262 specs中定义,尤其是 15.10.2.5 ,其中讨论了生产 Term :: Atom Quantifier 。

稍微概括一下:设E为一个可以匹配空字符串的模式。如果存在输入字符串S,其中空字符串是E中的第一个匹配选项,则包含贪婪重复模式E的模式会受到影响。例如:

(a*?)*              against     aaaa
!\[(.*?)*\]         against     ![][][]
(.*?){1,4}          against     asdf
(|a)*               against     aaaa
(a|()|b){0,4}       against     aaabbb

Firefox和其他基于Webkit的浏览器似乎都严格遵循这些规范,而在没有受影响模式的续集的情况下IE不符合规范。

答案很长

以下引用了规范的相关部分。省略了某些部分规范([...])以保持讨论的相关性。我将通过缩小规范中的信息来解释,同时保持简单。

词汇

  
      
  • State 是一个有序对( endIndex 捕获),其中 endIndex 是一个整数和 capture NcapturingParens 值的内部数组。 States 用于表示正则表达式匹配算法中的部分匹配状态。 endIndex 是一个加上到目前为止由模式匹配的最后一个输入字符的索引,而捕获保存捕获括号的结果。 [...]。由于回溯,许多可能在匹配过程中随时使用。
  •   
  • MatchResult State 或特殊令牌失败,表示匹配失败。
  •   

这里应该没有混淆。 State 跟踪到目前为止已处理的位置(以及我们目前不感兴趣的捕获)。 MatchResult ,嗯,是匹配结果(呃!)。

  
      
  • Continuation 过程是一个内部闭包(即一个内部过程,其中一些参数已绑定到值),它接受一个 State 参数并返回 MatchResult < / em>结果。如果内部闭包引用了在创建闭包的函数中绑定的变量,则闭包使用这些变量在创建闭包时具有的值。 Continuation 尝试将模式的剩余部分(由闭包的已绑定参数指定)与输入String匹配,从其 State <给出的中间状态开始/ em>参数。如果匹配成功, Continuation 将返回它到达的最终 State ;如果匹配失败, Continuation 将返回失败
  •   
  • Matcher 过程是一个内部闭包,它接受两个参数 - State Continuation - 并返回 MatchResult 结果。 Matcher 尝试将模式的中间子模式(由闭包的已绑定参数指定)与输入String匹配,从其 State <给出的中间状态开始/ em>参数。 Continuation 参数应该是一个与模式的其余部分匹配的闭包。在匹配模式的子模式以获得新的状态之后,匹配器然后在新的状态上调用 Continuation 测试模式的其余部分是否也匹配。如果可以,匹配器将返回 Continuation 返回的 State ;如果没有, Matcher 可以在其选择点尝试不同的选择,反复调用 Continuation ,直到它成功或所有可能性都已用尽。
  •   

Matcher 包含与其代表的子模式匹配的代码,它将调用 Continuation 继续匹配。并且 Continuation 包含用于继续 Matcher 停止的匹配的代码。他们都接受 State 作为参数来知道从哪里开始匹配。

生产期限 :: Atom Quantifier

  

生产 Term :: Atom Quantifier 评估如下:

     
      
  1. 评估 Atom 以获取匹配器 m
  2.   
  3. 评估量词以获得三个结果:整数 min ,整数(或∞) max 和布尔贪婪
  4.   
  5. 如果 max 是有限且小于 min ,则抛出 SyntaxError 异常。
  6.   
  7. parenIndex 成为此生产扩展 Term 左侧的整个正则表达式中左侧捕获括号的数量。 [...]
  8.   
  9. parenCount 成为此制作 Atom 扩展中左侧捕获括号的数量。 [...]
  10.   
  11. 返回一个内部Matcher闭包,它带有两个参数,一个State x 和一个Continuation c ,并执行以下操作:      
        
    1. 调用RepeatMatcher( m,min,max,greedy,x,c,parenIndex,parenCount )并返回其结果。
    2.   
  12.   

请注意m是正在重复的 Atom 的匹配器,并且从更高级别的生产生成的代码传递Continuation c 规则。

  

抽象操作 RepeatMatcher 采用八个参数,一个匹配器 m ,一个整数 min ,一个整数(或∞) max ,布尔贪婪,状态 x ,延续 c ,整数 parenIndex ,以及整数 parenCount ,并执行以下操作:

     
      
  1. 如果 max 为零,则调用 c(x)并返回其结果。
  2.   
  3. 创建一个内部延续闭包 d ,它接受一个State参数 y 并执行以下操作:      
        
    1. 如果 min 为零且 y endIndex 等于 x &#39 ; s endIndex ,然后返回失败
    2.   
    3. 如果 min 为零,则让 min2 为零;否则让 min2 min - 1。
    4.   
    5. 如果 max 为∞,则让 max2 为∞;否则让 max2 max - 1。
    6.   
    7. 调用RepeatMatcher( m,min2,max2,greedy,y,c,parenIndex,parenCount )并返回其结果。
    8.   
  4.   
  5. cap 成为 x 捕获内部数组的全新副本。
  6.   
  7. 对于满足 parenIndex &lt; parenIndex 的每个整数 k k k parenIndex + parenCount ,将 cap [ k ]设置为 undefined
  8.   
  9. e 成为 x endIndex
  10.   
  11. xr 成为州( e,cap )。
  12.   
  13. 如果 min 不为零,则调用 m(xr,d)并返回其结果。
  14.   
  15. 如果贪心 false ,那么      
        
    1. 致电 c(x)并让 z 成为其结果。
    2.   
    3. 如果 z 不是失败,请返回 z
    4.   
    5. 致电 m(xr,d)并返回结果。
    6.   
  16.   
  17. 致电 m(xr,d),让 z 成为其结果。
  18.   
  19. 如果 z 不是失败,请返回 z
  20.   
  21. 致电 c(x)并返回结果。
  22.   

步骤2定义了一个Continuation d ,我们尝试匹配重复Atom的另一个实例,稍后在步骤7中调用( min &gt; 0 case),步骤8.3(懒惰的情况)和步骤9(贪婪的情况)通过匹配器m

步骤5和6创建当前状态的副本,用于回溯目的,并在步骤2中检测空字符串匹配。

此处的步骤描述了3个不同的案例:

  • 步骤7涵盖量词具有非零 min 值的情况。贪婪无论如何,我们需要至少匹配Atom的 min 实例,然后才允许我们调用Continuation c

  • 由于步骤7中的条件,此时 min 为0。步骤8处理量词是惰性的情况。步骤9,10,11处理量词贪婪的情况。请注意调用的相反顺序。

调用 m(xr,d)表示匹配Atom的一个实例,然后调用Continuation d 继续重复。

调用Continuation c(x)意味着退出重复并匹配下一步。注意Continuation c 如何传递给Continuation d 中的RepeatMatcher,以便它可以用于重复的所有后续迭代。

JavaScript RegExp

中的quirk说明

步骤2.1 是导致PCRE和JavaScript之间结果出现差异的罪魁祸首。

  
      
  1. 如果 min 为零且 y endIndex 等于 x &#39 ; s endIndex ,然后返回失败
  2.   

当量词最初为0 min *{0,n})或通过步骤7时,达到条件 min = 0,必须被调用,只要 min &gt; 0(原始量词为+{n,}{n,m})。

min = 0 时,量词是贪婪的,将调用Matcher m (在步骤9中),它会尝试匹配在第2步中设置的Atom实例并调用Continuation d 。继续 d 将递归调用RepeatMatcher,然后调用Matcher m (第9步)。

如果没有步骤2.1,如果匹配器 m 具有空字符串作为其在输入中向前推进的第一个可能选择,则迭代将(理论上)在没有提前的情况下永久地继续。鉴于JavaScript RegExp支持的功能有限(没有前向声明的反向引用或其他花哨功能),当前迭代匹配空字符串时,无需继续进行另一次迭代 - 无论如何,空字符串将再次匹配。步骤2.1是JavaScript处理重复空字符串的方法。

如步骤2.1中所设置的,当 min = 0时, JavaScript 正则表达式引擎将在Matcher m匹配空字符串时剪切(返回失败)。这个有效地拒绝&#34;多次有限重复的空字符串是一个空字符串&#34;

步骤2.1的另一个副作用是从 min = 0时开始,只要有一个匹配非m匹配非空字符串的实例,最后一次重复 Atom 保证不为空。

相比之下,似乎 PCRE (和其他引擎)接受&#34;空字符串重复有限多次是一个空字符串&#34;并继续与其余的模式。这解释了上面列出的案例中PCRE的行为。至于算法,PCRE在连续两次匹配空字符串后停止重复。

答案 1 :(得分:4)

我做了一些测试,发现在Firefox和Chrome中,如果一个组有一个贪婪的量词,并且直接或间接包含一个或多个惰性量词,其中零作为最小迭代次数,那么对于贪心量词的迭代已经满足最小迭代次数,如果惰性量词匹配零次迭代,如果组将找到零长度匹配,则可以匹配一次或多次迭代的最左边的惰性量词将匹配一次迭代。

E.g。 (.{0,2}?){5,8}匹配“abcdefghijklmnopqrstuvwxyz”中的“abc”,因为.{0,2}?匹配{5,8}的迭代6,7和8的一次迭代。

如果在具有无法匹配的贪心量词的组之后存在令牌,则延迟量词会扩展其迭代次数。永远不会尝试使用零迭代的置换。但贪婪的量词仍然可以回溯到最小迭代次数。

在相同的主题字符串上,(.{0,3}?){5,6}[ad]匹配“abcd”而(.{0,3}?){5,6}a匹配“a”。

如果组中有任何其他内容找到匹配项,那么延迟量词就像在其他正则表达式引擎中一样。

当且仅当在具有贪婪量词的组之后存在不可选的令牌时,在Internet Explorer中也会发生同样的情况。如果组之后没有令牌或者它们都是可选的,那么IE就像大多数其他正则表达式引擎一样。

Firefox和Chrome中的行为解释似乎是JavaScript标准中第15.10.2.5节中两个步骤的组合。使用RepeatMatcher的步骤2.1使正则表达式引擎失败已经匹配所需迭代的最小数量的量词的零长度迭代,而不是仅仅停止继续迭代。在评估惰性量词本身之前,步骤9评估惰性量词之后的任何内容。在这些例子中,包括继续重复贪婪量词。当这个贪婪量词在步骤2.1中失败时,懒惰量词被迫重复至少一次。

因此,为了回答这是否是一个错误,我想说它是JavaScript规范中的一个错误。这种行为没有任何好处,使得JavaScript正则表达式与所有其他(流行的)回溯正则表达式引擎不同。我不认为未来的JavaScript规范会改变这一点。

Opera表现不同。 (.{0,2}?){5,8}匹配“abcd”而(.{0,2}?){6,8}匹配“abcde”。除了贪婪量词的第一次迭代之外,Opera似乎强制延迟量词匹配至少一次迭代,然后在贪婪量词找到所需的最小迭代次数时停止迭代。

我建议不要使用一切都是可选的组或替代品。确保每个替代方案和每个组都匹配。如果该组需要是可选的,请使用?使整个组可选,而不是使组内的所有内容都可选。这将减少正则表达式引擎必须尝试的排列数。

答案 2 :(得分:2)

这并没有回答为什么 grediness在Javascript中表现不同,但它表明这不是一个错误,并且经过测试表现如此。 我将以google的v8引擎为例。我在他们的测试中发现了一个类似的例子。

<强> /test/mjsunit/third_party/regexp-pcre.js:

line 1085: res[1006] = /([a]*?)*/;
line 4822: assertToStringEquals("aaaa,a", res[1006].exec("aaaa "), 3176);

https://code.google.com/p/v8/source/browse/trunk/test/mjsunit/third_party/regexp-pcre.js#1085

此代码适用于Javascript http://regex101.com/r/nD0uG8但它没有与PCRE php和python相同的输出。

<强>更新 关于你的问题:

  

似乎JavaScript的正则表达式引擎正在解释第二个*来转换。*?从懒惰到贪婪

https://code.google.com/p/v8/source/browse/trunk/src/parser.cc#5157

RegExpQuantifier::QuantifierType quantifier_type = RegExpQuantifier::GREEDY;
if (current() == '?') {
    quantifier_type = RegExpQuantifier::NON_GREEDY;
    Advance();
} else if (FLAG_regexp_possessive_quantifier && current() == '+') {
    // FLAG_regexp_possessive_quantifier is a debug-only flag.
    quantifier_type = RegExpQuantifier::POSSESSIVE;
    Advance();
}