【问题标题】:Comparing speed of non-matching regexp比较不匹配正则表达式的速度
【发布时间】:2012-11-01 14:19:28
【问题描述】:

以下 Python 代码非常慢:

import re
re.match( '([a]+)+c', 'a' * 30 + 'b' )

如果将 30 替换为更大的常数,情况会变得更糟。

我怀疑由于连续的+ 导致的解析歧义是罪魁祸首,但我在正则表达式解析和匹配方面不是很专家。这是 Python 正则表达式引擎的错误,还是任何合理的实现都会这样做?

我不是 Perl 专家,但下面的返回速度很快

perl -e '$s="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"; print "ok\n" if $s =~ m/([a]+)+c/;'

增加“a”的数量并不会显着改变执行速度。

【问题讨论】:

  • 看看catastrophic backtracking。如果引擎没有优化掉两个+s,这可能会变得非常糟糕,特别是因为在两次重复之间存在捕获组,并且捕获相对昂贵。
  • @m.buettner:是的,而且可能 Perl 的引擎足够聪明,可以将其过滤掉。让它成为答案!
  • 下面是如何在 Perl 中对其进行基准测试:perl -E 'use Benchmark ":hireswallclock"; $s="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"; $t0 = Benchmark->new; say "ok" if $s =~ m/([a]+)+c/; $t1 = Benchmark->new; say timestr(timediff($t0, $t1))'
  • 这里是 idoene 的 5 秒超时:ideone.com/QwOjSE
  • 啊,there's a bug

标签: python regex perl


【解决方案1】:

我假设 Perl 足够聪明,可以将两个 +s 合并为一个,而 Python 不是。现在让我们想象一下引擎会做什么,如果它没有被优化掉的话。请记住,捕获通常很昂贵。另请注意,+s 都是贪婪的,因此引擎将尝试在一个回溯步骤中使用尽可能多的重复。每个要点代表一个回溯步骤:

  • 引擎使用尽可能多的[a],并消耗所有三十个as。然后它不能再进一步了,所以它留下了第一次重复并捕获 30 as。现在下一次重复开始了,它试图用另一个([a]+) 消耗更多,但这当然不起作用。然后c 无法匹配b
  • 原路返回!丢弃内部重复消耗的最后一个a。在此之后,我们再次离开内部重复,因此引擎将捕获 29 as。然后另一个+ 开始,再次尝试内部重复(消耗第30 个a)。然后我们再次离开内部重复,这也离开了捕获组,所以第一个捕获被丢弃,引擎捕获最后一个ac 无法匹配 b
  • 原路返回!在里面扔掉另一个a。我们捕获 28 as。捕获组的第二个(外部重复)消耗最后 2 个被捕获as。 c 无法匹配 b
  • 原路返回!现在我们可以在第二个其他重复中回溯并丢弃两个as 中的第二个。剩下的将被捕获。然后我们第三次进入捕获组,capture最后一个ac 无法匹配 b
  • 原路返回!在第一次重复中减少到 27 个as。

这是一个简单的可视化。每一行代表一个回溯步骤,每组括号表示一次内部重复的消耗。大括号表示为该回溯步骤捕获的那些,而在此特定回溯步骤中不会重新访问普通括号。我省略了b/c,因为它永远不会匹配:

{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}
{aaaaaaaaaaaaaaaaaaaaaaaaaaaaa}{a}
{aaaaaaaaaaaaaaaaaaaaaaaaaaaa}{aa}
(aaaaaaaaaaaaaaaaaaaaaaaaaaaa){a}{a}
{aaaaaaaaaaaaaaaaaaaaaaaaaaa}{aaa}
(aaaaaaaaaaaaaaaaaaaaaaaaaaa){aa}{a}
(aaaaaaaaaaaaaaaaaaaaaaaaaaa){a}{aa}
(aaaaaaaaaaaaaaaaaaaaaaaaaaa)(a){a}{a}
{aaaaaaaaaaaaaaaaaaaaaaaaaa}{aaaa}
(aaaaaaaaaaaaaaaaaaaaaaaaaa){aaa}{a}
(aaaaaaaaaaaaaaaaaaaaaaaaaa){aa}{aa}
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(aa){a}{a}
(aaaaaaaaaaaaaaaaaaaaaaaaaa){a}{aaa}
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(a){aa}{a}
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(a){a}{aa}
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(a)(a){a}{a}

而且。所以。开。

请注意,最终引擎还将尝试a 子集的所有组合(回溯到前 29 个as,然后通过前 28 个as)只是为了发现 c也不匹配a

正则表达式引擎内部的解释是基于散布在regular-expressions.info周围的信息。

解决这个问题。只需删除+s 之一。 r'a+c' 或者如果您确实想要捕获as 的数量,请使用r'(a+)s'

最后,回答你的问题。我不会认为这是 Python 正则表达式引擎中的错误,而只是(如果有的话)缺乏优化逻辑。这个问题通常不是可以解决的,所以引擎假设你必须自己处理灾难性的回溯并不是太不合理。如果 Perl 足够聪明,能够识别出足够简单的情况,那就更好了。

【讨论】:

  • @Axeman 关键是在给定的情况下它找不到完整模式的匹配项(因为c 永远不会匹配b)。如果引擎不包含针对这些情况的任何优化,它无论如何都会回溯重复(因为它们可能包含更复杂的表达式,最终可能导致匹配)。
【解决方案2】:

重写您的正则表达式以消除"catastrophic backtracking",方法是删除嵌套量词(请参阅this question):

re.match( '([a]+)+c', 'a' * 30 + 'b' )
# becomes
re.match( 'a+c', 'a' * 30 + 'b' )

【讨论】:

  • 我的正则表达式只是一个例子,事实上,有一个等价的不会导致回溯问题。关键不在于如何获得有效的匹配,而在于效率低下是由于错误、次优实现还是内在问题造成的。
  • @Mapio 一般来说,修复的建议是:重写你的正则表达式。 (你是对的,在你的例子中这很容易,但一般来说会更难!)
猜你喜欢
  • 2015-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-29
  • 1970-01-01
相关资源
最近更新 更多