TL;DR
为什么第一种模式比第二种效率低得多,而且,
最令人困惑的是,为什么第三个如此高效?
因为前两个是锚定的,所以第三个不是。
真实故事,如何采取步骤
考虑到这个正则表达式/^x/gm,如果主题字符串是abc,您认为引擎将采取多少步骤来返回“不匹配”?你是对的,两个。
- 断言字符串的开头
- 匹配
x
然后整体匹配失败,因为在字符串断言开始后没有立即出现x。
好吧,我撒谎了。并不是我讨厌,它只是让人们更容易理解即将发生的事情。根据regex101.com,它根本不需要任何步骤:
这次你会相信吗?是的。不,让我们看看。
PCRE启动优化
PCRE 对其用户友善,提供了一些功能来加速被称为启动优化的事情。它会根据使用的正则表达式进行一些相关的优化。
这些优化的一个重要特性是对主题字符串进行预扫描,以确保:
- 主题字符串至少包含一个字符对应于匹配的第一个字符或
- 如果存在已知起点。
如果找不到匹配函数,则永远不会运行。
也就是说,如果我们的正则表达式是/x/,并且我们的主题字符串是abc,那么在启用启动优化的情况下,打算进行预扫描以查找x,如果没有找到完整的匹配失败或者更好的是,它甚至不费心去完成匹配过程。
那么这些信息有什么帮助?
让我们回顾一下我们的第一个例子,稍微改变一下我们的正则表达式。来自:
/^x/gm
到
/^x/g
不同之处在于 m 标志正在取消设置。对于那些不知道 m 标志在设置后会做什么的人:
它改变了^ 和$ 符号的含义,因为它们不再是字符串的开始和结束,而是行的开始和结束。
现在如果我们在主题字符串abc 上运行这个正则表达式/^x/g 会怎样?我们是否应该期望引擎采取的步骤有所不同? 当然,是的。我们来看看regex101.com返回的信息:
这次我真的鼓励你相信它。是真的。
发生了什么?
好吧,这似乎有点令人困惑,但我们会开导一些事情。当没有设置m 修饰符时,预扫描看起来断言字符串的开始(一个已知的起点)。如果断言通过,则运行实际匹配函数,否则将返回“不匹配”。
但是等等……每个主题字符串肯定只有一个字符串位置的开头,而且它总是在它的开头。那么预扫描不是显然没有必要吗?是的,引擎在这里不进行预扫描。使用/^x/g,它会立即断言字符串的开头,然后像这样失败(因为它在^ 匹配,所以它会经历实际的匹配过程)。这就是为什么我们看到 regex101.com 显示步数为 2。
但是...设置 m 修饰符情况会有所不同。现在 ^ 和 $ 锚的含义都已更改。随着^匹配行首,在主题字符串abc中的相同位置的断言发生但下一个立即字符不是x,在实际匹配过程中并且由于g标志打开,下一个匹配从位置开始在b 之前失败,这个反复试验一直持续到主题字符串的结尾。
调试器显示 6 个步骤,但主页显示 0 个步骤,为什么?
我不确定后者,但为了调试,regex101 调试器使用(*NO_START_OPT) 运行,因此仅当设置了此动词时,6 个步骤才是正确的。我说我不确定后者,因为所有锚定模式都会阻止进一步的预扫描优化,我们应该知道什么可以称为 anchored pattern :
一个模式会自动被 PCRE 锚定,如果它的所有
顶级备选方案以下列之一开头:
-
^ 除非设置了 PCRE_MULTILINE
-
\A 总是
-
\G 总是
-
.* 如果设置了 PCRE_DOTALL 并且没有对
.* 出现的子模式
现在你完全明白了我所说的,当m 标志未在/^x/g 中设置时不会发生预扫描:它被认为是一种锚定模式,它禁用预扫描扫描优化。所以当m 标志打开时,这不再是一个锚定模式:/^x/gm 因此可以进行预扫描优化。
引擎知道字符串锚点\A(或^,当多行模式被禁用时)仅在匹配时出现一次,因此它不会在下一个位置继续。
回到你自己的正则表达式
前两个是锚定的(^ 与 m 标志一起),第三个不是。也就是说,第三个正则表达式受益于预扫描优化。您可以相信 35 步,因为优化导致了它。但是如果你禁用启动优化:
(*NO_START_OPT)!next(?:(?<=^.{5})|(?=$))
您将看到 57 个步骤,这与调试器步骤的数量大致相同。