这就是 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,并执行以下步骤:
- 如果
max 为零,则返回c(x)。
- 创建一个内部继续闭包
d,它接受一个状态参数y,并在评估时执行以下步骤:
- 一个。如果
min 为零并且y 的endIndex 等于x 的endIndex,则返回failure。
- 乙。如果
min 为零,则令min2 为零;否则让min2 为min‑1。
- c。如果
max 为∞,则令max2 为∞;否则让max2 为max‑1。
- d.调用
RepeatMatcher(m, min2, max2, greedy, y, c, parenIndex, parenCount) 并返回结果。
- 让
cap 成为x 的捕获列表的新副本。
- 对于每个满足
parenIndex < k 和k ≤ parenIndex+parenCount 的整数k,将cap[k] 设置为undefined。
- 让
e 成为x 的endIndex。
- 让
xr 成为状态(e, cap)。
- 如果
min 不为零,则返回m(xr, d)。
- 如果
greedy 是false,那么
- 一个。调用
c(x) 并让z 成为它的结果。
- 乙。如果
z 不是failure,则返回z。
- c。调用
m(xr, d) 并返回结果。
- 调用
m(xr, d) 并让z 成为它的结果。
- 如果
z 不是failure,则返回z。
- 调用
c(x) 并返回结果。
这基本上是在评估量词时应该发生的事情的定义。 RepeatMatcher 是处理内部操作m 匹配的操作。
您还需要了解 State 是什么(第 21.2.2.1 节,强调我的):
State 是一个有序对 (endIndex, captures),其中 endIndex 是一个整数,捕获的是一个 NcapturingParens 值的列表。状态用于表示正则表达式匹配算法中的部分匹配状态。 endIndex 是模式匹配的最后一个输入字符的索引的加一,而captures 保存捕获括号的结果。 captures 的 nth 元素要么是一个 List,表示由 nth 组捕获括号获得的值,要么是未定义的,如果尚未达到 nth 组捕获括号。由于回溯,许多
在匹配过程中,状态可能随时处于使用状态。
对于您的示例,RepeatMatcher 参数是:
-
m:Matcher 负责处理子模式(.*)
-
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])。
-
max 不为零
- 此时我们创建了延续闭包
d1,它将在第二遍中使用,因此我们稍后会回到这一遍。
- 复制一份
cap1的捕获列表
- 将
cap1 中的捕获重置为undefined,这是第一次通过的无操作
- 设
e1 = 0
- 让
xr1 = (e1, cap1)
-
min 为零,跳过这一步
-
greedy 为真,跳过这一步
- 让
z1 = m(xr, d1) - 这将评估子模式(.*)
现在让我们退后一步 - m 将匹配 (.*) 和 abc,然后调用我们的 d1 闭包,所以让我们展开那个闭包。
d1 被评估为状态 y1 =(3, ["abc"]):
-
min 是 0,但 y1 的 endIndex 是 3 而 x1 的 endIndex 是 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"])。
-
max 不为零
- 此时我们创建了延续闭包
d2
- 复制
cap2 的捕获列表["abc"]
- 将
cap2中的捕获重置为undefined,我们得到cap2 = [undefined]
- 让
e2 = 3
- 让
xr2 = (e2, cap2)
-
min 为零,跳过这一步
-
greedy 为真,跳过这一步
-
Let z2 = m(xr2, d2) - 这将评估子模式@ 987654478@
这一次m 将匹配abc 之后的空字符串,并用那个调用我们的d2 闭包。让我们评估一下d2 做了什么。它的参数是y2 = (3, [""])
min 仍为 0,但 y2 的 endIndex 为 3,x2 的 endIndex 也为 3 (请记住这次x 是上一次迭代的y 状态),闭包只是返回failure。
-
z2 是failure,跳过这一步
- 返回
c(x2),即本次迭代中的c((3, ["abc"]))。
c 只是在这里返回一个有效的 MatchResult,因为我们处于模式的末尾。这意味着d1 返回此结果,并且第一次迭代返回从第 10 步传递它。
基本上,如您所见,导致 JS 行为与 PCRE 不同的规范行如下:
一个。如果min 为零且y 的endIndex 等于x 的endIndex,则返回failure。
当结合:
- 调用
c(x) 并返回结果。
如果迭代失败,则返回之前捕获的值。