为什么这三个正则表达式的步数不同?
Why do these three regexes have different step counts?
在 answer to a recent question 中,我设计了几个聪明的小正则表达式(应提问者的要求)来匹配字符串开头或结尾的子字符串。然而,当在 Regex101 上 运行 时,我注意到不同的模式有不同的步数(表明正则表达式引擎必须为一个比另一个做更多的工作)。然而,在我看来,没有直觉上的理由应该如此。
三种模式如下:
- 有趣的条件:
/(^)?!next(?(1)|$)/
(demo - 86 步)
- 经典交替:
^!next|!next$
(demo - 58 步)
- 讨厌的环顾四周:
!next(?:(?<=^.{5})|(?=$))
(demo - 35 步)
为什么第一个模式比第二个模式效率低得多,而且最令人困惑的是,为什么第三个模式如此有效?
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"?你是对的,两个。
- 断言字符串的开头
- 匹配
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 个步骤,这与调试器步骤的数量基本相同。
在 answer to a recent question 中,我设计了几个聪明的小正则表达式(应提问者的要求)来匹配字符串开头或结尾的子字符串。然而,当在 Regex101 上 运行 时,我注意到不同的模式有不同的步数(表明正则表达式引擎必须为一个比另一个做更多的工作)。然而,在我看来,没有直觉上的理由应该如此。
三种模式如下:
- 有趣的条件:
/(^)?!next(?(1)|$)/
(demo - 86 步) - 经典交替:
^!next|!next$
(demo - 58 步) - 讨厌的环顾四周:
!next(?:(?<=^.{5})|(?=$))
(demo - 35 步)
为什么第一个模式比第二个模式效率低得多,而且最令人困惑的是,为什么第三个模式如此有效?
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"?你是对的,两个。
- 断言字符串的开头
- 匹配
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:
^
unlessPCRE_MULTILINE
is set\A
always\G
always.*
ifPCRE_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 个步骤,这与调试器步骤的数量基本相同。