经过长时间的调试后,我发现为什么使用python regexps的应用程序运行缓慢。这是令我惊讶的事情:
import datetime
import re
pattern = re.compile('(.*)sol(.*)')
lst = ["ciao mandi "*10000 + "sol " + "ciao mandi "*10000,
"ciao mandi "*1000 + "sal " + "ciao mandi "*1000]
for s in lst:
print "string len", len(s)
start = datetime.datetime.now()
re.findall(pattern,s)
print "time spent", datetime.datetime.now() - start
print
我机器上的输出是:
string len 220004
time spent 0:00:00.002844
string len 22004
time spent 0:00:05.339580
第一个测试字符串长220K,匹配,匹配非常快。第二个测试字符串是20K长,不匹配,计算需要5秒!
我知道这个报告http://swtch.com/~rsc/regexp/regexp1.html,它说python,perl,ruby中的regexp实现有点不理想......这是什么原因?我没想到会发生这么简单的表达。
加入 我的原始任务是依次拆分一个字符串尝试不同的正则表达式。类似的东西:
for regex in ['(.*)sol(.*)', '\emph{([^{}])*)}(.*)', .... ]:
lst = re.findall(regex, text)
if lst:
assert len(lst) == 1
assert len(lst[0]) == 2
return lst[0]
这是为了解释为什么我不能使用split
。我按照Martijn的建议将(.*)sol(.*)
替换为(.*?)sol(.*)
解决了我的问题。
可能我应该使用match
代替findall
...但我不认为这会解决问题,因为正则表达式将匹配整个输入,因此找到应该在第一场比赛中停止。
无论如何,我的问题更多的是关于一个正则表达式新手有多容易陷入这个问题...我认为理解(.*?)sol(.*)
是解决方案并不是那么简单(例如{{1}不是)。
答案 0 :(得分:18)
Thompson NFA方法将正则表达式从默认贪婪更改为默认非贪婪。普通正则表达式引擎可以做同样的事情;只需将.*
更改为.*?
即可。当非贪婪时,你不应该使用贪婪的表达。
有人已经为Python构建了一个NFA正则表达式解析器:https://github.com/xysun/regex
它确实胜过病态案例的默认Python正则表达式解析器。 然而, 下
这个正则表达式引擎在正常输入上表现不如Python的模块(使用Glenn Fowler的测试套件 - 见下文)
以牺牲典型为代价来修复病理情况可能是不使用NFA方法作为默认引擎的一个很好的理由,而不是在可以简单地避免病理情况时。
另一个原因是某些功能(例如反向引用)要么非常难以实现,要么无法使用NFA方法实现。另请参阅response on the Python Ideas mailing list。
因此,如果您至少使其中一个模式不贪婪,以避免灾难性的回溯,那么您的测试可以表现得更好:
pattern = re.compile('(.*?)sol(.*)')
或根本不使用正则表达式;您可以使用str.partition()
来获取前缀和后缀:
before, sol, after = s.partition('sol')
e.g。并非所有的文本问题都是正则表达形式的,所以放下锤子并查看工具箱的其余部分!
此外,您可以查看替代re
模块regex
。该模块对病理案例进行了一些基本检查,并且可以灵活地避免它们,而无需诉诸Thompson NFA实现。引用an entry to a Python bug report tracking regex
:
内部引擎不再解释字节码的形式,而是解释 跟随一组链接的节点,它既可以广度也可以 深度优先,这使得它在面对其中一个时表现得更好 那些“病态的”正则表达。
这个引擎可以让您的病理情况快10万倍:
>>> import re, regex
>>> import timeit
>>> p_re = re.compile('(.*)sol(.*)')
>>> p_regex = regex.compile('(.*)sol(.*)')
>>> s = "ciao mandi "*1000 + "sal " + "ciao mandi "*1000
>>> timeit.timeit("p.findall(s)", 'from __main__ import s, p_re as p', number=1)
2.4578459990007104
>>> timeit.timeit("p.findall(s)", 'from __main__ import s, p_regex as p', number=100000)
1.955532732012216
注意数字;我将re
测试限制为1次运行并且花了2.46秒,而regex
测试在2秒内运行了100k次。
答案 1 :(得分:6)
我认为这与灾难性的回溯无关(或者至少是我对它的理解)。
问题是由(.*)
中的第一个(.*)sol(.*)
引起的,以及正则表达式没有锚定在任何地方的事实。
re.findall()
,在索引0失败后,将在索引1,2等处重试,直到字符串结束。
badbadbadbad...bad
^ Attempt to match (.*)sol(.*) from index 0. Fail
^ Attempt to match (.*)sol(.*) from index 1. Fail
^ Attempt to match (.*)sol(.*) from index 2. Fail (and so on)
它实际上具有二次复杂度O(n 2 ),其中n是字符串的长度。
问题可以通过锚定您的模式来解决,因此它会在您的模式无法匹配的位置立即失败。 (.*)sol(.*)
将在一行文本中搜索sol
(由行终止符分隔),因此如果它无法在该行的开头找到匹配项,则无法找到任何其余部分。
因此,您可以使用:
^(.*)sol(.*)
re.MULTILINE
选项。
运行此测试代码(从您的代码修改):
import datetime
import re
pattern = re.compile('^(.*)sol(.*)', re.MULTILINE)
lst = ["ciao mandi "*10000 + "sol " + "ciao mandi "*10000,
"ciao mandi "*10000 + "sal " + "ciao mandi "*10000]
for s in lst:
print "string len", len(s)
start = datetime.datetime.now()
re.findall(pattern,s)
print "time spent", datetime.datetime.now() - start
print
(请注意,传递和失败都是220004个字符)
给出以下结果:
string len 220004
time spent 0:00:00.002000
string len 220004
time spent 0:00:00.005000
这清楚地表明两种情况现在具有相同的数量级。
答案 2 :(得分:0)