为啥编译的 python 正则表达式比较慢?
Posted
技术标签:
【中文标题】为啥编译的 python 正则表达式比较慢?【英文标题】:Why is a compiled python regex slower?为什么编译的 python 正则表达式比较慢? 【发布时间】:2018-05-08 17:01:18 【问题描述】:在another SO question 中,比较了正则表达式和Python 的in
运算符的性能。但是,接受的答案使用re.match
,它只匹配字符串的开头,因此行为与in
完全不同。另外,我想看看每次不重新编译 RE 的性能提升。
令人惊讶的是,我看到预编译版本似乎更慢。
有什么想法吗?
我知道这里还有很多其他问题想知道类似的问题。它们中的大多数执行它们的方式仅仅是因为它们没有正确重用已编译的正则表达式。如果这也是我的问题,请解释一下。
from timeit import timeit
import re
pattern = 'sed'
text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod' \
'tempor incididunt ut labore et dolore magna aliqua.'
compiled_pattern = re.compile(pattern)
def find():
assert text.find(pattern) > -1
def re_search():
assert re.search(pattern, text)
def re_compiled():
assert re.search(compiled_pattern, text)
def in_find():
assert pattern in text
print('str.find ', timeit(find))
print('re.search ', timeit(re_search))
print('re (compiled)', timeit(re_compiled))
print('in ', timeit(in_find))
输出:
str.find 0.36285957560356435
re.search 1.047689160564772
re (compiled) 1.575113873320307
in 0.1907925627077569
【问题讨论】:
你应该做compiled_pattern.search(text)
而不是re.search(compiled_pattern, text)
compiled_pattern.search(text)
将快 3 倍。不过,这仍然很奇怪,re.search(compiled_pattern, text)
似乎没有做同样的事情。
@tobias_k 查看声明,调用re.search
只是简单地执行return _compile(pattern, flags).search(string)
。不检查模式是否已经编译。
它确实。看看re.py:294
。 (Py3.6 的行号)。
FWIW,我链接的那个问题有 an answer 由 Python 核心开发人员 Raymond Hettinger 提供,但它添加得比较晚,所以它不在页面顶部附近。
【参考方案1】:
简答
如果直接调用compiled_pattern.search(text)
,根本不会调用_compile
,会比re.search(pattern, text)
快,比re.search(compiled_pattern, text)
快很多。
这种性能差异是由于缓存中的KeyError
s 和编译模式的哈希计算速度慢。
re
函数和 SRE_Pattern
方法
任何时候调用带有pattern
作为第一个参数的re
函数(例如re.search(pattern, string)
或re.findall(pattern, string)
),Python 会尝试首先使用_compile
编译pattern
,然后调用相应的方法在编译模式上。对于example:
def search(pattern, string, flags=0):
"""Scan through string looking for a match to the pattern, returning
a match object, or None if no match was found."""
return _compile(pattern, flags).search(string)
请注意,pattern
可以是字符串或已编译的模式(SRE_Pattern
实例)。
_编译
这是_compile
的精简版。我只是删除了调试和标志检查:
_cache =
_pattern_type = type(sre_compile.compile("", 0))
_MAXCACHE = 512
def _compile(pattern, flags):
try:
p, loc = _cache[type(pattern), pattern, flags]
if loc is None or loc == _locale.setlocale(_locale.LC_CTYPE):
return p
except KeyError:
pass
if isinstance(pattern, _pattern_type):
return pattern
if not sre_compile.isstring(pattern):
raise TypeError("first argument must be string or compiled pattern")
p = sre_compile.compile(pattern, flags)
if len(_cache) >= _MAXCACHE:
_cache.clear()
loc = None
_cache[type(pattern), pattern, flags] = p, loc
return p
_compile
带字符串模式
当_compile
用字符串模式调用时,编译后的模式保存在_cache
dict 中。下次调用相同的函数时(例如,在许多 timeit
运行期间),_compile
只需检查 _cache
是否已看到此字符串并返回相应的编译模式。
在 Spyder 中使用ipdb
调试器,在执行过程中很容易深入到re.py
。
import re
pattern = 'sed'
text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod' \
'tempor incididunt ut labore et dolore magna aliqua.'
compiled_pattern = re.compile(pattern)
re.search(pattern, text)
re.search(pattern, text)
在第二个re.search(pattern, text)
处有断点,可以看出对:
(<class 'str'>, 'sed', 0): (re.compile('sed'), None)
保存在_cache
。编译后的模式直接返回。
_compile
带有编译模式
慢散列
如果使用已编译的模式调用 _compile
会发生什么?
首先,_compile
检查模式是否在_cache
中。为此,它需要计算其哈希值。编译模式的计算比字符串慢得多:
In [1]: import re
In [2]: pattern = "(?:a(?:b(?:b\\é|sorbed)|ccessing|gar|l(?:armists|ternation)|ngels|pparelled|u(?:daciousness's|gust|t(?:horitarianism's|obiographi
...: es)))|b(?:aden|e(?:nevolently|velled)|lackheads|ooze(?:'s|s))|c(?:a(?:esura|sts)|entenarians|h(?:eeriness's|lorination)|laudius|o(?:n(?:form
...: ist|vertor)|uriers)|reeks)|d(?:aze's|er(?:elicts|matologists)|i(?:nette|s(?:ciplinary|dain's))|u(?:chess's|shanbe))|e(?:lectrifying|x(?:ampl
...: ing|perts))|farmhands|g(?:r(?:eased|over)|uyed)|h(?:eft|oneycomb|u(?:g's|skies))|i(?:mperturbably|nterpreting)|j(?:a(?:guars|nitors)|odhpurs
...: 's)|kindnesses|m(?:itterrand's|onopoly's|umbled)|n(?:aivet\\é's|udity's)|p(?:a(?:n(?:els|icky|tomimed)|tios)|erpetuating|ointer|resentation|
...: yrite)|r(?:agtime|e(?:gret|stless))|s(?:aturated|c(?:apulae|urvy's|ylla's)|inne(?:rs|d)|m(?:irch's|udge's)|o(?:lecism's|utheast)|p(?:inals|o
...: onerism's)|tevedore|ung|weetest)|t(?:ailpipe's|easpoon|h(?:ermionic|ighbone)|i(?:biae|entsin)|osca's)|u(?:n(?:accented|earned)|pstaging)|v(?
...: :alerie's|onda)|w(?:hirl|ildfowl's|olfram)|zimmerman's)"
In [3]: compiled_pattern = re.compile(pattern)
In [4]: % timeit hash(pattern)
126 ns ± 0.358 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [5]: % timeit hash(compiled_pattern)
7.67 µs ± 21 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
hash(compiled_pattern)
比 hash(pattern)
慢 60 倍。
KeyError
当 pattern
未知时,_cache[type(pattern), pattern, flags]
会失败并返回 KeyError
。
KeyError
被处理和忽略。只有这样_compile
才会检查模式是否已经编译。如果是,则返回,而不写入缓存。
这意味着下次使用相同的编译模式调用_compile
时,它会再次计算无用的慢散列,但仍然会失败并返回KeyError
。
错误处理代价高昂,我想这就是re.search(compiled_pattern, text)
比re.search(pattern, text)
慢的主要原因。
这种奇怪的行为可能是加快使用字符串模式调用的一种选择,但如果使用已编译的模式调用_compile
,则编写警告可能是个好主意。
【讨论】:
我刚刚遇到了这个性能问题,感谢您的出色解释!看起来类型检查(即if isinstance(pattern, _pattern_type)
)会比较快,所以我想知道他们为什么不把它放在_compile
的首位以上是关于为啥编译的 python 正则表达式比较慢?的主要内容,如果未能解决你的问题,请参考以下文章
python 正则表达式 re,compile速度慢 ,怎样可以使的re.compile的速度更快