【问题标题】:Perl vs Javascript regular expressionsPerl 与 Javascript 正则表达式
【发布时间】:2017-04-02 10:07:50
【问题描述】:

为什么下面的正则表达式在 Javascript 中捕获(通过捕获组)字符串“abc”,但在 PCRE 中没有捕获(尽管它仍然会匹配)?

(.*)*

【问题讨论】:

  • 您一定对 regex101.com 突出显示捕获组的方式感到困惑,对吧?
  • @WiktorStribiżew:不要以为就是这样。在the JavaScript versionthe PCRE version 之间比较捕获组的内容。前者显示“abc”,后者显示空。 (除非您指的是 regex101 中的错误。尽管结果不同,但解释似乎相同。)
  • 其实规范案例是(.*)*。将 kleene 星放在末端会阻止捕获组与 PCRE 一起工作。至少,根据 regex101 确实如此。我错过了这个特殊的正则表达式测试器的东西吗?
  • 另外,如果我将 * 更改为 +,即 (.+)+,它会按照我的预期工作。
  • 好的,ideone.com/ALeMqhjsfiddle.net/wvudw7yv 显示相同。必须是与 JS 和 PCRE 中的空匹配处理相关的差异。是的,将 * 更改为 + 会使模式匹配至少 1 个字符。这个问题与许多其他问题一致,其根本原因是使用 JS 正则表达式处理空匹配的方式。见Some regex pattern is breaking the javascript regex engine

标签: javascript regex perl pcre


【解决方案1】:

这就是 PCRE 中捕获组为空的原因:

  • 初始状态

    (.*)*     abc
     ^        ^
    
  • 首先将(.*)部分与abc匹配,将输入位置推进到最后。此时捕获组包含abc

    (.*)*     abc
        ^        ^
    
  • 现在,输入位置是之后 c 字符,剩下的输入是空字符串。 Kleene 星开始第二次尝试匹配(.*) 组:

    (.*)*     abc
     ^           ^
    
  • (.*) 组匹配abc 之后的空字符串。由于匹配,之前捕获的字符串将被覆盖

  • 由于输入位置没有前进,所以*在此处结束迭代,匹配成功。

JS 和 PCRE 之间的行为差​​异是由于指定正则表达式引擎的方式。 PCRE 的行为与 Perl 一致:

PCRE:

$ pcretest
PCRE version 8.39 2016-06-14

  re> /(.*)*/
data> abc
 0: abc
 1:

Perl:

$ perl -e '"abc" =~ /(.*)*/; print "<$&> <$1>\n";'
<abc> <>

让我们compare this with .NET,它具有相同的行为,但支持多个捕获

当捕获组第二次匹配时,.NET 会将捕获的值添加到捕获堆栈。 Perl 和 PCRE 会简单地覆盖它。


至于 JavaScript:

这是ECMA-262 §21.2.2.5.1 运行时语义:RepeatMatcher 抽象操作:

抽象操作RepeatMatcher有八个参数,一个匹配器m,一个整数min,一个整数(或∞)max,一个布尔值greedy,一个状态@987654343 @、Continuation c、整数parenIndex、整数parenCount,并执行以下步骤:

  1. 如果max 为零,则返回c(x)
  2. 创建一个内部继续闭包d,它接受一个状态参数y,并在评估时执行以下步骤:
    • 一个。如果min 为零并且yendIndex 等于xendIndex,则返回failure
    • 乙。如果min 为零,则令min2 为零;否则让min2min‑1
    • c。如果max 为∞,则令max2 为∞;否则让max2max‑1
    • d.调用RepeatMatcher(m, min2, max2, greedy, y, c, parenIndex, parenCount) 并返回结果。
  3. cap 成为x 的捕获列表的新副本。
  4. 对于每个满足parenIndex &lt; kk ≤ parenIndex+parenCount 的整数k,将cap[k] 设置为undefined
  5. e 成为x 的endIndex。
  6. xr 成为状态(e, cap)
  7. 如果min 不为零,则返回m(xr, d)
  8. 如果greedyfalse,那么
    • 一个。调用c(x) 并让z 成为它的结果。
    • 乙。如果z 不是failure,则返回z
    • c。调用m(xr, d) 并返回结果。
  9. 调用m(xr, d) 并让z 成为它的结果。
  10. 如果z 不是failure,则返回z
  11. 调用c(x) 并返回结果。

这基本上是在评估量词时应该发生的事情的定义。 RepeatMatcher 是处理内部操作m 匹配的操作。

您还需要了解 State 是什么(第 21.2.2.1 节,强调我的):

State 是一个有序对 (endIndex, captures),其中 endIndex 是一个整数,捕获的是一个 NcapturingParens 值的列表。状态用于表示正则表达式匹配算法中的部分匹配状态。 endIndex 是模式匹配的最后一个输入字符的索引的加一,而captures 保存捕获括号的结果。 capturesnth 元素要么是一个 List,表示由 nth 组捕获括号获得的值,要么是未定义的,如果尚未达到 nth 组捕获括号。由于回溯,许多 在匹配过程中,状态可能随时处于使用状态。

对于您的示例,RepeatMatcher 参数是:

  • mMatcher 负责处理子模式(.*)
  • min: 0(Kleene 星量词的最小匹配数)
  • max: ∞(Kleene 星量词的最大匹配数)
  • greedy: true(使用的 Kleene 星量词是贪婪的)
  • x: (0, [undefined])(参见上面的状态定义)
  • c:延续,此时它将是:在折叠父规则后,始终将其 State 参数作为成功的 MatchResult 返回的延续
  • parenIndex: 0(根据 §21.2.2.5,这是 出现在此左侧的整个正则表达式中的左捕获括号数 生产)
  • parenCount: 1(相同的规范段落,这是在这个产品的 Atom 扩展中左捕获括号的数量 - 我不会在这里粘贴完整的规范,但这基本上意味着 m定义一个捕获组)

规范中的正则表达式匹配算法是根据continuation-passing style 定义的。基本上,这意味着c 操作意味着接下来应该发生什么

让我们展开这个算法。

第一次迭代

在第一次通过时,x1 状态为 (0, [undefined])

  1. max 不为零
  2. 此时我们创建了延续闭包d1,它将在第二遍中使用,因此我们稍后会回到这一遍。
  3. 复制一份cap1的捕获列表
  4. cap1 中的捕获重置为undefined,这是第一次通过的无操作
  5. e1 = 0
  6. xr1 = (e1, cap1)
  7. min 为零,跳过这一步
  8. greedy 为真,跳过这一步
  9. z1 = m(xr, d1) - 这将评估子模式(.*)

现在让我们退后一步 - m 将匹配 (.*)abc,然后调用我们的 d1 闭包,所以让我们展开那个闭包。

d1 被评估为状态 y1 =(3, ["abc"]):

  • min 是 0,但 y1endIndex 是 3 而 x1endIndex 是 0,所以不要返回failure
  • min2 = min = 0 因为min = 0
  • max2 = max = ∞,因为max = ∞
  • 调用RepeatMatcher(m, min2, max2, greedy, y, c, parenIndex, parenCount) 并返回结果。即:RepeatMatcher(m, 0, ∞, false, y1, c, 0, 1)

第二次迭代

所以,现在我们要进行第二次迭代,x2 = y1 = (3, ["abc"])

  1. max 不为零
  2. 此时我们创建了延续闭包d2
  3. 复制cap2 的捕获列表["abc"]
  4. cap2中的捕获重置为undefined,我们得到cap2 = [undefined]
  5. e2 = 3
  6. xr2 = (e2, cap2)
  7. min 为零,跳过这一步
  8. greedy 为真,跳过这一步
  9. Let z2 = m(xr2, d2) - 这将评估子模式@ 987654478@

    这一次m 将匹配abc 之后的空字符串,并用那个调用我们的d2 闭包。让我们评估一下d2 做了什么。它的参数是y2 = (3, [""])

    min 仍为 0,但 y2endIndex 为 3,x2endIndex 也为 3 (请记住这次x 是上一次迭代的y 状态),闭包只是返回failure

  10. z2failure,跳过这一步
  11. 返回c(x2),即本次迭代中的c((3, ["abc"]))

c 只是在这里返回一个有效的 MatchResult,因为我们处于模式的末尾。这意味着d1 返回此结果,并且第一次迭代返回从第 10 步传递它。

基本上,如您所见,导致 JS 行为与 PCRE 不同的规范行如下:

一个。如果min 为零且yendIndex 等于xendIndex,则返回failure

当结合:

  1. 调用c(x) 并返回结果。

如果迭代失败,则返回之前捕获的值。

【讨论】:

  • 啊哈!这就说得通了。但是,我不确定我是否理解 JS 的行为。大概,当它的主题为空时,外部 * 是 not 第二次匹配?当然,如果我在 JS 中使用 (.*){2}force 进行第二次匹配,则行为与 Perl 匹配。
  • @TimAngus 我必须深入研究 JS 规范才能找到答案,但我怀疑 JS 会在到达字符串末尾时立即停止。我没有将其包含在答案中,因为我对此不是 100% 确定,我会尝试找出并稍后添加。
  • 对于 Perl,它不会给出无限的空字符串序列,因为 Perl 会丢弃与前一个匹配具有相同起始位置和相同长度的匹配。
  • @ikegami hmmm 我不认为 "same length" 部分很重要,规则应该是:将输入位置推进到最后一场比赛的末尾,并且如果最后一个匹配项是空字符串,则添加一个字符(以避免在获得空匹配项时出现无限循环 - 这是 JS regex API 的一个缺点)。
  • @Lucas Trzesniewski,你错了。如果最后一个匹配是空字符串,Perl 不会添加一个字符。例如,perl -E'say "\@$-[0]: $&amp;" while "abc" =~ /.*?/g' 演示了在相同位置同时匹配空字符串和非空字符串。正如我上面所说,当匹配与前一个匹配位于相同位置和相同长度时,Perl 会添加一个。
猜你喜欢
  • 2017-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-24
  • 1970-01-01
  • 2012-07-22
相关资源
最近更新 更多