正向后行断言与匹配重置(\K)正则表达式特性的区别

9
我刚刚了解到 Ruby 正则表达式中的 似乎未记录的 \K 行为(感谢anubhava提供的此答案)。这个特性(可能被命名为保持?)也存在于 PHP、Perl和Python正则表达式。它在其他地方被描述为“从匹配要返回的内容中删除迄今为止匹配的内容”。
"abc".match(/ab\Kc/)     # matches "c"

这个行为是否与下面使用的正向先行断言标记完全相同?

"abc".match(/(?<=ab)c/)  # matches "c"

如果没有,这两者有哪些不同之处?


一个区别是,回顾后面的表达式不会消耗字符。请参见此使用回顾后面表达式的示例与此使用\K的示例 - bobble bubble
2个回答

10

通过使用String#scan方法,更容易看到\K(?<=...)之间的区别。

回顾环视是零宽度断言,不会消耗字符,并且从当前位置向后测试:

> "abcdefg".scan(/(?<=.)./)
=> ["b", "c", "d", "e", "f", "g"]

"保持"特性\K(这不是一个锚点)定义了模式中的一个位置,该位置将左侧模式已匹配到的所有内容从匹配结果中删除。但在\K之前匹配的所有字符都已被消耗,它们只是不出现在结果中:

> "abcdefg".scan(/.\K./)
=> ["b", "d", "f"]

行为与没有使用 \K 相同:

> "abcdefg".scan(/../)
=> ["ab", "cd", "ef"]

除了会从结果中删除 \K 前面的字符之外,它几乎和正则表达式中的其他断言一样。

\K 的一个有趣用途是模拟可变长度的向后查找,这在 Ruby(同样适用于 PHP 和 Perl) 中不允许,或者避免创建单独的捕获组。例如,可以使用 \K 来实现 (?<=a.*)f.

> "abcdefg".match(/a.*\Kf./)
=> #<MatchData "fg">

另一种方法是编写/a.*(f.)/,但\K可以避免创建捕获组的需要。

请注意,\K特性也存在于Python的regex模块中,即使此模块允许可变长度的向后查找。


2
我不熟悉Ruby,但在Perl中\K(?<=...)更快。通常使用\K来避免使用捕获组,所以你建议相反有点奇怪。 - ikegami

2

回答了这个问题,并包含一个比喻后,我被要求在这个规范页面上发布答案。

在优化给定的正则表达式时,我努力通过简单的层次结构来优化模式。精确性,效率,可读性,简洁性。当较高级别的条件不完美/不最优时,通常忽略较低级别的标准。


请允许我简化一下所提出问题中已经被简化的任务。我们不需要匹配c,而是可以使用PHP进行一个或多个零宽度匹配,并比较/a\K//(?<=a)/两者返回相同的结果。

  1. 在比较这个简单演示的两种模式设计时,准确性是相同的。

  2. 效率是确定胜负的关键。

    $string = 'abcbacbacbabcbabcbabcccbbaaabacbabbcacbabca';
    
    $regex1 = '/a\K/'; // 14 次匹配,42 步 https://regex101.com/r/j4Jrrj/1
    
    $regex2 = '/(?<=a)/'; // 14 次匹配,167 步 https://regex101.com/r/JuZJDE/1
    
这是为什么:使用$regex2时,正则表达式引擎每走一步(遍历字符串中的每个字符),都必须回头检查可能的a。相反,$regex1快速地遍历字符(只查看最近的字符),如果遇到a,则匹配成功,不需要查找其他字符。一般来说,回顾(和捕获组)可能会导致效率下降--值得运行一些测试,以查看如何通过微小的调整来提高模式效率。

常见的情况是在不消耗任何字符的情况下拆分字符串。 使用preg_split()oneTwoThreeFourFiveSix在小写字母和大写字母之间进行拆分,可以执行以下操作:

  1. "向两边看" - /(?<=[a-z])(?=[A-Z])/将在22个字符的字符串上需要70步(因为它需要在每个位置向一侧或两侧查看)。
  2. "匹配任何小写字母,释放,前瞻" - /[a-z]\K(?=[A-Z])/将采取78个步骤(因为有大量小写字母,然后采取2个后续动作)。
  3. "匹配连续小写字母,释放,前瞻" - /[a-z]+\K(?=[A-Z])/将采取41个步骤(因为它仅对最后一个小写字母采取其他操作)。

当然:preg_match()可以使用/(?:^|[A-Z])[a-z]*/在38个步骤内遍历字符串,但这可能是在实际项目中讨论精度的不同方面。


当我们停止让正则表达式引擎在每个字符处停止时,比赛可以加强。

   $string = 'abcbacbacbabcbabcbabcccbbaaabacbabbcacbabca';

   $regex1 = '/ab\Kc/'; // 5 matches, 45 steps https://regex101.com/r/j4Jrrj/2

   $regex2 = '/(?<=ab)c/'; // 5 matches, 44 steps https://regex101.com/r/JuZJDE/2

$regex1 遍历字符串直到找到一个 a,然后是一个 b,最后是一个 c
$regex2 遍历字符串直到找到一个 c,然后向后查找一个 b,最后是一个 a

示例字符串包含 a 14x、c 24x、ab 7x 和 bc 6x。字符的存在和顺序对步骤数有直接影响。 记录一下,/abc/ 只需要 38 步,所以每匹配一个 ab\K 就要花费 7 步,但 /ab(c)/ 花费更多 -- 50 步。


为了展示一个优于 \K 的正向预查,搜索在 Price: $5666;Weight: 7145;Height: 420;Width: 411; 中出现在数字后面的分号。 因为数字比分号更频繁地出现,/(?<=\d);/ 需要20步,而 /\d\K;/ 需要46步。

结论

要求正则表达式引擎在字符串中的每个位置上执行操作是一项低效的任务——即使您更喜欢可读性。

除此之外,通常是字符的频率和顺序决定了正则表达式引擎的性能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接