目录

  • 目录是空的
首页 文章 2021 05 19

PCRE 重置捕获组编号

更新时间:

作者: Alex

重置捕获组编号是在研究 fast-router 时学到的概念,具体内容可以查看原文fast-router 利用了 PCRE 的正则表达式重置捕获组功能,将全部路由规则拼接成一个正则表达式, 从而避免了循环匹配,有效规避了 PHP 执行速度的劣势,它的匹配性能甚至超过了很多 C 扩展框架。以下是核心概念的总结:

多选分支 (alternative)

形如 /abc|def/ 的正则表达式中,有两个用竖杠分隔的子表达式 abcdef,正则引擎能够匹配其中任意一个表达式, 我们称这些子表达式为 “多选分支 (alternative)”。

非捕获组 (non-capturing group)

当我们需要对表达式分组,而又不想捕获组数据时,需要用到非捕获组(或称‘非捕获子模式’),用 (?: ) 表示。 如:/(?:Satur|Sun)day/, 此时,正则引擎只对 SaturSun 分组,但不捕获里面的数据,也不参与分组编号。当然,除了 (?: ) 以外还有几个子模式的语法,这里不一一叙述。

PCRE 的捕获组编号

在通常的捕获模式下,每个捕获括号都有自己的顺序编号,与多选分支无关,如 /abc(def)|ghi(jkl)mn/,这里的 (abc) 的编号为1,(jkl) 的编号为2。 当表达式与字符串 ghijklmn 匹配时,捕获结果为:

[
    0 => 'ghijklmn',
    1 => null,
    2 => 'jkl'
]
  • 编号 0 为 全匹配项 (full match)
  • 编号 1 对应第一个捕获括号,它没有匹配成功,所以为空
  • 编号 2 对应第二个捕获括号,匹配成功并捕获到数据 ‘jkl’。

捕获组编号重置 (duplicate subpattern numbers)

PCRE 还支持另一种非捕获子模式,用 (?| ) 表示。在这种子模式下,所有多选分支中的捕获编号都会被重置, 如:/(?|abc(def)|ghi(jkl)mn)/ 在与字符串 ghijklmn 匹配时,捕获结果为:

[
    0 => 'ghijklmn',
    1 => 'jkl'
]

正则引擎首先对子表达式 abc(def) 进行匹配,捕获编号为 1,此时匹配没有成功;随后又对 ghi(jkl)mn 进行匹配,由于在此模式下多选分支捕获编号重置,所以捕获编号仍然为 1,并且前一组没有匹配成功的分组被直接丢弃了。

捕获组编号重置功能对于那些 “只关心多选分支中匹配成功的部分” 的场景非常有用!它可以过滤捕获结果,只返回匹配成功的值。

PCRE 命名捕获陷阱

python 的 regex 包,和 PCRE 都实现了捕获组编号重置重置功能,但二者有细节上的差别。python 支持将相同编号的组命名为不同的名称,而 PCRE 不允许

python 的实现:

import regex

pattern = r"(?|(?P<foo>first)|(?P<bar>second))"
matchObj = regex.match(pattern, 'second', )

print(matchObj.groupdict())

输出结果为:

{'foo': None, 'bar': 'second'}

本来应该被重置的组被赋予了新的编号,并且保存到了捕获组中。可见在 python 的编号重置模式中,只有相同的组名才能对应相同的编号。

php 的实现

$pattern = '/(?|(?P<foo>first)|(?P<bar>second))/';

preg_match($pattern, 'second', $match);

输出结果为:

PHP Warning:  preg_match(): Compilation failed: different names for subpatterns of the same number are not allowed at offset 25

使用 PCRE 库的 php,在组号相同,命名不同的情况下会产生错误。这一点与 perl 也存在差异,perl支持这种语法,并且所有分组都会被赋予相同的值。

perl 的实现

use Data::Dumper;

if ('second' =~ /(?|(?<foo>first)|(?<bar>second))/) {
    print Dumper(\%+);
}

输出结果为:

$VAR1 = {
    'bar' => 'second',
    'foo' => 'second'
};
后一篇