【问题标题】:Why do these three regexes have different step counts?为什么这三个正则表达式的步数不同?
【发布时间】:2016-10-08 05:46:56
【问题描述】:

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

三种模式如下:

  • 有趣的条件句:/(^)?!next(?(1)|$)/demo - 86 步)
  • 经典交替:^!next|!next$demo - 58 步)
  • 讨厌的外观:!next(?:(?<=^.{5})|(?=$))demo - 35 步)

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

【问题讨论】:

  • (^)?实际上意味着“可选地从行首开始”,是吗?
  • 没错。它还在模式末尾启用条件,因为它“捕获”了零长度断言。
  • @Tibrogargan:更好的翻译是“捕获是否从行首开始”。
  • 我们确定 regex101.com 显示的“步数”是对正则表达式效率的精确衡量吗?
  • @ruakh:我认为它是一个不错的代理,因为即使是现代正则表达式引擎基本上也是具有一些额外内存的复杂状态机。

标签: regex pcre


【解决方案1】:

TL;DR

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

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

真实故事,如何采取步骤

考虑到这个正则表达式/^x/gm,如果主题字符串是abc,您认为引擎将采取多少步骤来返回“不匹配”?你是对的,两个。

  1. 断言字符串的开头
  2. 匹配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 个步骤,这与调试器步骤的数量大致相同。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-27
    • 1970-01-01
    • 1970-01-01
    • 2013-12-05
    • 1970-01-01
    相关资源
    最近更新 更多