在answer to a recent question中,我设计了几个聪明的小正则表达式(在提问者的请求下)来匹配字符串开头或结尾的子字符串。然而,当在Regex101上运行时,我注意到不同的模式具有不同的步数(表明正则表达式引擎必须为一个与另一个做更多的工作)。但是,在我看来,没有直观的理由认为应该如此。
这三种模式如下:
/(^)?!next(?(1)|$)/
(demo - 86步)^!next|!next$
(demo - 58步)!next(?:(?<=^.{5})|(?=$))
(demo - 35步)为什么第一种模式的效率低于第二种模式,而且最令人困惑的是,为什么第三种模式如此有效?
答案 0 :(得分:5)
为什么第一个模式的效率低于第二个模式,并且, 最令人困惑的是,为什么第三个如此高效?
因为前两个是锚定的,第三个不是。
考虑到这个正则表达式/^x/gm
,您认为引擎将采取多少步骤来返回&#34;不匹配&#34;如果主题字符串是abc
?你是对的,两个。
x
然后整体匹配失败,因为在字符串断言开始后没有x
立即出现。
我说谎了。这不是我讨厌,它只是让你更容易理解即将发生的事情。根据{{3}},它根本不采取任何步骤:
这次你相信吗?是。不,我们来看看。
PCRE
启动优化 PCRE
,善待其用户,提供一些功能来加速称为启动优化的事情。根据正在使用的正则表达式,它会进行一些依赖的优化。
这些优化的一个重要特征是主题字符串的预扫描,以确保:
如果找不到匹配函数,则永远不会运行。
如果我们的正则表达式为/x/
且主题字符串为abc
,那么启用启动优化时,就会进行预扫描以查找x
,如果没有发现整场比赛失败或更好,它甚至不会费心去完成匹配过程。
那么这些信息如何帮助?
让我们回顾一下我们的第一个例子并稍微改变我们的正则表达式。从:
/^x/gm
到
/^x/g
差异是m
标志未被设置。对于那些不知道m
标志如何设置的人:
它改变了^
和$
符号的含义,因为它们不再是字符串的开头和结尾,而是行的开头和结尾。
现在,如果我们在主题字符串/^x/g
上运行此正则表达式abc
,该怎么办?我们是否应该预期步进引擎的差异与否? 绝对,是的。让我们看一下返回的信息:
我真的鼓励你这次相信它。这是真实的。
发生了什么?
嗯,这看起来有点令人困惑,但我们会启发。如果没有设置m
修饰符,则预扫描会查找断言字符串的开头(已知起点)。如果断言通过则实际匹配函数运行,否则&#34;不匹配&#34;会回来的。
但等等......每个主题字符串肯定都有一个且只有字符串位置的开头,并且它总是在它的开头。那么扫描显然不是必要的吗?是的,引擎在这里没有进行预扫描。使用/^x/g
它会立即断言字符串的开始,然后像这样失败(因为它匹配^
它会经历实际的匹配过程)。这就是我们看到显示步数 2 的原因。
但是......设置m
修饰符会有所不同。现在改变了^
和$
锚点的含义。在^
匹配起始行的情况下,主题字符串abc
中的相同位置的断言发生但下一个直接字符不是x
,在实际匹配过程中并且从g
标志开始已开启,下一场比赛从b
之前的位置开始并失败,此试错将继续到主题字符串结束。
调试器显示 6 步骤,但主页面显示 0 步骤,为什么?
我不确定后者但是为了调试,regex101调试器与(*NO_START_OPT)
一起运行,所以只有设置了这个动词才有6个步骤。我说我不确定后者,因为 所有锚定模式阻止了进一步的扫描前优化 ,我们应该知道什么可以称为 { {3}} :
如果所有的模式都由PCRE自动锚定 顶级替代品以下列之一开头:
^
除非设置PCRE_MULTILINE
\A
总是\G
总是.*
如果设置了PCRE_DOTALL
并且没有后向引用 出现.*
的子模式
现在,当我在m
中/^x/g
标志 设置为m
时没有预先扫描时,您完全得到了我所说的内容:它是被认为是一种禁用预扫描优化的锚定模式。因此,当/^x/gm
标志打开时,这不再是锚定模式:\A
因此可以进行预扫描优化。
引擎知道字符串锚^
的开始(或多行模式禁用时^
)只在匹配时发生一次,因此它不会在下一个位置继续。
前两个被锚定(m
与(*NO_START_OPT)!next(?:(?<=^.{5})|(?=$))
标志一起),第三个不是。也就是说,第三个正则表达式受益于预扫描优化。您可以相信 35 步骤,因为优化导致了这一步骤。但是,如果禁用启动优化:
{{1}}
您将看到57个步骤,这些步骤与调试器步骤的数量大致相同。