为什么这三个正则表达式的步数不同?

Why do these three regexes have different step counts?

answer to a recent question 中,我设计了几个聪明的小正则表达式(应提问者的要求)来匹配字符串开头或结尾的子字符串。然而,当在 Regex101 上 运行 时,我注意到不同的模式有不同的步数(表明正则表达式引擎必须为一个比另一个做更多的工作)。然而,在我看来,没有直觉上的理由应该如此。

三种模式如下:

为什么第一个模式比第二个模式效率低得多,而且最令人困惑的是,为什么第三个模式如此有效?

TL;DR

Why is the first pattern so much less efficient than the second, and, most confusingly, why is the third so efficient?

因为前两个是锚定的,第三个不是。

真实故事,步骤是怎样的

考虑到这个正则表达式 /^x/gm,如果主题字符串是 abc,您认为引擎需要多少步才能 return 到 "no match"?你是对的,两个。

  1. 断言字符串的开头
  2. 匹配x

然后整体匹配失败,因为在字符串断言开始后没有立即出现 x

好吧,我撒谎了。这并不是说我很讨厌,它只是让我更容易理解将要发生的事情。根据 regex101.com 它根本不需要任何步骤:

这次你信吗?是的。不,让我们看看。

PCRE 启动优化

PCRE,善待用户,提供了一些功能来加快速度,称为 启动优化 。它根据所使用的正则表达式进行一些相关的优化。

这些优化的一个重要特征是预扫描主题字符串,以确保:

  • 主题字符串包含至少一个与匹配项的第一个字符相对应的字符或
  • 如果存在已知起点。

如果找不到匹配函数,则永远不会 运行s.

也就是说,如果我们的正则表达式是 /x/ 并且我们的主题字符串是 abc 那么在启用启动优化的情况下,将进行预扫描以查找 x,如果没有找到整个匹配失败或更好,它甚至不会费心去完成匹配过程。

那么这些信息有什么帮助

让我们回顾一下我们的第一个例子,稍微改变一下我们的正则表达式。来自:

/^x/gm

/^x/g

不同之处在于 m 标志未设置。对于那些不知道设置 m 标志的人:

它改变了 ^$ 符号的含义,因为它们不再表示字符串的开始和结束,而是表示行的开始和结束。

现在,如果我们 运行 这个正则表达式 /^x/g 覆盖我们的主题字符串 abc 会怎么样?我们是否应该期望引擎采取的步骤有所不同? 当然可以。让我们看看 regex101.com returned 信息:

这次我真的鼓励你相信它。是真的。

发生什么事了?

好吧,这似乎有点令人困惑,但我们会开导一下。当没有设置 m 修饰符时,预扫描查找断言字符串的开头(已知起点)。如果断言通过,则实际匹配函数 运行s 否则 "no match" 将 return.

但是等等...每个主题字符串肯定只有一个字符串位置的开始,并且它总是在它的最开头。那么显然不需要预扫描吗?是的,引擎不会在这里进行预扫描。使用 /^x/g 它会立即断言字符串的开头,然后像这样失败(因为它在 ^ 处匹配,它会经历实际的匹配过程)。这就是为什么我们看到 regex101.com 显示步数是 2.

但是...设置 m 修饰符的情况有所不同。现在 ^$ 锚点的含义都已更改。使用 ^ 匹配行首,断言主题字符串 abc 中的相同位置发生但下一个直接字符不是 x,在实际匹配过程中并且因为 g 标志开启时,下一场比赛从 b 之前的位置开始,但失败了,这种反复试验一直持续到主题字符串的末尾。

调试器显示 6 步,但主页显示 0 步,为什么?

我不确定后者,但为了调试,regex101 调试器 运行s 与 (*NO_START_OPT) 因此只有设置此动词时 6 个步骤才是正确的。我说我不确定后者,因为 所有锚定模式都会阻止进一步的预扫描优化 我们应该知道什么可以称为anchored pattern:

A pattern is automatically anchored by PCRE if all of its top-level alternatives begin with one of the following:

  • ^ unless PCRE_MULTILINE is set
  • \A always
  • \G always
  • .* if PCRE_DOTALL is set and there are no back references to the subpattern in which .* appears

现在你完全明白了我所说的当我说 m 标志 没有 时没有预扫描发生在 /^x/g 中:它被认为是禁用预扫描优化的锚定模式。因此,当 m 标志打开时,这不再是锚定模式:/^x/gm 因此可以进行预扫描优化。

引擎知道字符串锚点的开始 \A(或 ^,而多行模式已禁用)仅在匹配时出现一次,因此它不会在下一个位置继续。

回到你自己的正则表达式

前两个是固定的(^m 标志结合),第三个不是。也就是说,第三个正则表达式受益于预扫描优化。您可以相信 35 个步骤,因为优化导致了它。但是如果你禁用启动优化:

(*NO_START_OPT)!next(?:(?<=^.{5})|(?=$))

您将看到 57 个步骤,这与调试器步骤的数量基本相同。