是否值得使用 Python 的 re.compile?
- 2025-01-03 14:18:00
- admin 原创
- 222
问题描述:
在 Python 中使用编译正则表达式有什么好处吗?
h = re.compile('hello')
h.match('hello world')
对比
re.match('hello', 'hello world')
解决方案 1:
我有过运行编译的正则表达式 1000 次和即时编译的经验,并没有发现任何明显的区别。显然,这只是传闻,当然也不是反对编译的有力论据,但我发现区别可以忽略不计。
编辑:快速浏览了一下实际的 Python 2.5 库代码后,我发现 Python 无论如何都会在内部编译并缓存正则表达式(包括调用re.match()
),因此您实际上只是在编译正则表达式时进行更改,并且不应该节省太多时间——只需要检查缓存(内部dict
类型的键查找)的时间。
来自模块 re.py(评论是我的):
def match(pattern, string, flags=0):
return _compile(pattern, flags).match(string)
def _compile(*key):
# Does cache check at top of function
cachekey = (type(key[0]),) + key
p = _cache.get(cachekey)
if p is not None: return p
# ...
# Does actual compilation on cache miss
# ...
# Caches compiled regex
if len(_cache) >= _MAXCACHE:
_cache.clear()
_cache[cachekey] = p
return p
我仍然经常预编译正则表达式,但只是为了将它们绑定到一个好用的可重复使用的名称,而不是为了获得任何预期的性能提升。
解决方案 2:
对我来说,最大的好处是re.compile
能够将正则表达式的定义与其使用分开。
即使是一个简单的表达式,例如0|[1-9][0-9]*
(十进制整数,无前导零)也可能非常复杂,以至于您不想重新输入它,检查是否有任何拼写错误,然后在开始调试时重新检查是否有拼写错误。此外,使用变量名(例如 num 或 num_b10)比 更好0|[1-9][0-9]*
。
当然可以存储字符串并将它们传递给 re.match;但是,这样做的可读性较差:
num = "..."
# then, much later:
m = re.match(num, input)
与编译相比:
num = re.compile("...")
# then, much later:
m = num.match(input)
尽管相当接近,但第二行的最后一行在重复使用时感觉更自然和简单。
解决方案 3:
仅供参考:
$ python -m timeit -s "import re" "re.match('hello', 'hello world')"
100000 loops, best of 3: 3.82 usec per loop
$ python -m timeit -s "import re; h=re.compile('hello')" "h.match('hello world')"
1000000 loops, best of 3: 1.26 usec per loop
因此,如果您要经常使用相同的正则表达式,那么这样做可能是值得的re.compile
(特别是对于更复杂的正则表达式)。
re.compile
反对过早优化的标准论点适用,但如果您怀疑正则表达式可能成为性能瓶颈,我认为使用它不会真正失去太多的清晰度/直接性。
更新:
在 Python 3.6(我怀疑上述计时是使用 Python 2.x 完成的)和 2018 硬件(MacBook Pro)下,我现在得到以下计时:
% python -m timeit -s "import re" "re.match('hello', 'hello world')"
1000000 loops, best of 3: 0.661 usec per loop
% python -m timeit -s "import re; h=re.compile('hello')" "h.match('hello world')"
1000000 loops, best of 3: 0.285 usec per loop
% python -m timeit -s "import re" "h=re.compile('hello'); h.match('hello world')"
1000000 loops, best of 3: 0.65 usec per loop
% python --version
Python 3.6.5 :: Anaconda, Inc.
我还添加了一个案例(请注意最后两次运行之间的引号差异),表明re.match(x, ...)
实际上 [大致] 等同于re.compile(x).match(...)
,即似乎没有发生编译表示的幕后缓存。
解决方案 4:
这是一个简单的测试用例:
~$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 're.match("[0-9]{3}-[0-9]{3}-[0-9]{4}", "123-123-1234")'; done
1 loops, best of 3: 3.1 usec per loop
10 loops, best of 3: 2.41 usec per loop
100 loops, best of 3: 2.24 usec per loop
1000 loops, best of 3: 2.21 usec per loop
10000 loops, best of 3: 2.23 usec per loop
100000 loops, best of 3: 2.24 usec per loop
1000000 loops, best of 3: 2.31 usec per loop
使用重新编译:
~$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 'r = re.compile("[0-9]{3}-[0-9]{3}-[0-9]{4}")' 'r.match("123-123-1234")'; done
1 loops, best of 3: 1.91 usec per loop
10 loops, best of 3: 0.691 usec per loop
100 loops, best of 3: 0.701 usec per loop
1000 loops, best of 3: 0.684 usec per loop
10000 loops, best of 3: 0.682 usec per loop
100000 loops, best of 3: 0.694 usec per loop
1000000 loops, best of 3: 0.702 usec per loop
因此,即使只匹配一次,在这个简单的情况下编译速度似乎也更快。
解决方案 5:
我刚刚亲自尝试过。对于从字符串中解析出数字并求和的简单情况,使用编译的正则表达式对象比使用这些re
方法快大约两倍。
正如其他人指出的那样,这些re
方法(包括re.compile
)在先前编译的表达式的缓存中查找正则表达式字符串。因此,在正常情况下,使用这些re
方法的额外成本只是缓存查找的成本。
但是,检查代码,显示缓存限制为 100 个表达式。这引出了一个问题,溢出缓存有多痛苦?代码包含正则表达式编译器的内部接口。re.sre_compile.compile
如果我们调用它,我们就会绕过缓存。事实证明,对于基本正则表达式(例如)来说,它大约慢了两个数量级r'w+s+([0-9_]+)s+w*'
。
这是我的测试:
#!/usr/bin/env python
import re
import time
def timed(func):
def wrapper(*args):
t = time.time()
result = func(*args)
t = time.time() - t
print '%s took %.3f seconds.' % (func.func_name, t)
return result
return wrapper
regularExpression = r'w+s+([0-9_]+)s+w*'
testString = "average 2 never"
@timed
def noncompiled():
a = 0
for x in xrange(1000000):
m = re.match(regularExpression, testString)
a += int(m.group(1))
return a
@timed
def compiled():
a = 0
rgx = re.compile(regularExpression)
for x in xrange(1000000):
m = rgx.match(testString)
a += int(m.group(1))
return a
@timed
def reallyCompiled():
a = 0
rgx = re.sre_compile.compile(regularExpression)
for x in xrange(1000000):
m = rgx.match(testString)
a += int(m.group(1))
return a
@timed
def compiledInLoop():
a = 0
for x in xrange(1000000):
rgx = re.compile(regularExpression)
m = rgx.match(testString)
a += int(m.group(1))
return a
@timed
def reallyCompiledInLoop():
a = 0
for x in xrange(10000):
rgx = re.sre_compile.compile(regularExpression)
m = rgx.match(testString)
a += int(m.group(1))
return a
r1 = noncompiled()
r2 = compiled()
r3 = reallyCompiled()
r4 = compiledInLoop()
r5 = reallyCompiledInLoop()
print "r1 = ", r1
print "r2 = ", r2
print "r3 = ", r3
print "r4 = ", r4
print "r5 = ", r5
</pre>
And here is the output on my machine:
<pre>
$ regexTest.py
noncompiled took 4.555 seconds.
compiled took 2.323 seconds.
reallyCompiled took 2.325 seconds.
compiledInLoop took 4.620 seconds.
reallyCompiledInLoop took 4.074 seconds.
r1 = 2000000
r2 = 2000000
r3 = 2000000
r4 = 2000000
r5 = 20000
“reallyCompiled”方法使用内部接口,从而绕过缓存。请注意,每次循环迭代时编译的方法只迭代 10,000 次,而不是 100 万次。
解决方案 6:
下面是一个例子,其中使用速度比要求的re.compile
快 50 倍以上。
要点与我在上面的评论中提到的一样,即re.compile
当您的使用方式无法从编译缓存中获益太多时,使用可能具有显著的优势。至少在一种特定情况下会发生这种情况(我在实践中遇到过),即当以下所有情况都成立时:
你有很多正则表达式模式(超过
re._MAXCACHE
,其默认值目前为 512),并且你经常使用这些正则表达式,并且
同一模式的连续使用之间被多个其他正则表达式分隔开
re._MAXCACHE
,因此每个正则表达式在连续使用之间都会从缓存中清除。
import re
import time
def setup(N=1000):
# Patterns 'a.*a', 'a.*b', ..., 'z.*z'
patterns = [chr(i) + '.*' + chr(j)
for i in range(ord('a'), ord('z') + 1)
for j in range(ord('a'), ord('z') + 1)]
# If this assertion below fails, just add more (distinct) patterns.
# assert(re._MAXCACHE < len(patterns))
# N strings. Increase N for larger effect.
strings = ['abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'] * N
return (patterns, strings)
def without_compile():
print('Without re.compile:')
patterns, strings = setup()
print('searching')
count = 0
for s in strings:
for pat in patterns:
count += bool(re.search(pat, s))
return count
def without_compile_cache_friendly():
print('Without re.compile, cache-friendly order:')
patterns, strings = setup()
print('searching')
count = 0
for pat in patterns:
for s in strings:
count += bool(re.search(pat, s))
return count
def with_compile():
print('With re.compile:')
patterns, strings = setup()
print('compiling')
compiled = [re.compile(pattern) for pattern in patterns]
print('searching')
count = 0
for s in strings:
for regex in compiled:
count += bool(regex.search(s))
return count
start = time.time()
print(with_compile())
d1 = time.time() - start
print(f'-- That took {d1:.2f} seconds.
')
start = time.time()
print(without_compile_cache_friendly())
d2 = time.time() - start
print(f'-- That took {d2:.2f} seconds.
')
start = time.time()
print(without_compile())
d3 = time.time() - start
print(f'-- That took {d3:.2f} seconds.
')
print(f'Ratio: {d3/d1:.2f}')
我在笔记本电脑上得到的示例输出(Python 3.7.7):
With re.compile:
compiling
searching
676000
-- That took 0.33 seconds.
Without re.compile, cache-friendly order:
searching
676000
-- That took 0.67 seconds.
Without re.compile:
searching
676000
-- That took 23.54 seconds.
Ratio: 70.89
timeit
由于差异如此明显,我没费心,但每次我得到的数字都定性相似。请注意,即使没有re.compile
,多次使用相同的正则表达式并转到下一个也不算太糟糕(仅比使用慢 2 倍左右re.compile
),但按照另一种顺序(循环遍历许多正则表达式),情况明显更糟,正如预期的那样。此外,增加缓存大小也有效:只需re._MAXCACHE = len(patterns)
在setup()
上面设置(当然我不建议在生产中这样做,因为带下划线的名称通常是“私有的”)即可将约 23 秒的时间缩短至约 0.7 秒,这也符合我们的理解。
解决方案 7:
我同意 Honest Abe 的观点,即match(...)
所给示例中的函数是不同的。它们不是一对一的比较,因此结果各不相同。为了简化我的回答,我使用 A、B、C、D 来表示所讨论的函数。哦,是的,我们处理的是 4 个函数,re.py
而不是 3 个。
运行这段代码:
h = re.compile('hello') # (A)
h.match('hello world') # (B)
与运行此代码相同:
re.match('hello', 'hello world') # (C)
因为,从源头来看re.py
,(A + B) 意味着:
h = re._compile('hello') # (D)
h.match('hello world')
而(C)实际上是:
re._compile('hello').match('hello world')
因此,(C) 与 (B) 不同。事实上,(C) 在调用 (D) 之后调用 (B),而 (D) 也被 (A) 调用。换句话说,(C) = (A) + (B)
。因此,在循环内比较 (A + B) 与在循环内比较 (C) 的结果相同。
乔治regexTest.py
向我们证明了这一点。
noncompiled took 4.555 seconds. # (C) in a loop
compiledInLoop took 4.620 seconds. # (A + B) in a loop
compiled took 2.323 seconds. # (A) once + (B) in a loop
每个人都感兴趣的是如何得到 2.323 秒的结果。为了确保compile(...)
只调用一次,我们需要将编译后的正则表达式对象存储在内存中。如果我们使用类,我们可以存储该对象并在每次调用函数时重用它。
class Foo:
regex = re.compile('hello')
def my_function(text)
return regex.match(text)
如果我们不使用类(这是我今天的要求),那么我无话可说。我仍在学习在 Python 中使用全局变量,我知道全局变量是一件坏事。
还有一点,我认为使用(A) + (B)
方法更有优势。以下是我观察到的一些事实(如果我错了,请纠正我):
调用一次 A,它会在后面进行一次搜索,
_cache
然后sre_compile.compile()
创建一个正则表达式对象。调用两次 A,它会进行两次搜索和一次编译(因为正则表达式对象被缓存了)。如果
_cache
在这期间被刷新,那么正则表达式对象就会从内存中释放,而且 Python 需要重新编译。(有人认为 Python 不会重新编译。)如果我们使用 (A) 保留正则表达式对象,则正则表达式对象仍将进入 _cache 并以某种方式被刷新。但我们的代码保留了对它的引用,并且正则表达式对象不会从内存中释放。这些,Python 不需要再次编译。
George 的测试编译循环与编译后的 2 秒差异主要是构建密钥和搜索 _cache 所需的时间。它并不意味着正则表达式的编译时间。
George 的 reallycompile 测试显示了如果每次都重新进行编译会发生什么:速度会慢 100 倍(他将循环从 1,000,000 减少到 10,000)。
以下是 (A + B) 优于 (C) 的唯一情况:
如果我们可以在类内部缓存正则表达式对象的引用。
如果我们需要重复调用(B)(在循环内或多次),我们必须在循环外缓存对正则表达式对象的引用。
(C)足够好的情况:
我们无法缓存引用。
我们只是偶尔使用它。
总的来说,我们没有太多的正则表达式(假设编译后的正则表达式永远不会被刷新)
回顾一下,以下是 ABC:
h = re.compile('hello') # (A)
h.match('hello world') # (B)
re.match('hello', 'hello world') # (C)
感谢阅读。
解决方案 8:
大多数情况下,使用与否都没有什么区别。在内部,所有函数都是按照编译步骤实现的:
def match(pattern, string, flags=0):
return _compile(pattern, flags).match(string)
def fullmatch(pattern, string, flags=0):
return _compile(pattern, flags).fullmatch(string)
def search(pattern, string, flags=0):
return _compile(pattern, flags).search(string)
def sub(pattern, repl, string, count=0, flags=0):
return _compile(pattern, flags).sub(repl, string, count)
def subn(pattern, repl, string, count=0, flags=0):
return _compile(pattern, flags).subn(repl, string, count)
def split(pattern, string, maxsplit=0, flags=0):
return _compile(pattern, flags).split(string, maxsplit)
def findall(pattern, string, flags=0):
return _compile(pattern, flags).findall(string)
def finditer(pattern, string, flags=0):
return _compile(pattern, flags).finditer(string)
此外,re.compile() 绕过了额外的间接和缓存逻辑:
_cache = {}
_pattern_type = type(sre_compile.compile("", 0))
_MAXCACHE = 512
def _compile(pattern, flags):
# internal: compile pattern
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):
if flags:
raise ValueError(
"cannot process flags argument with a compiled pattern")
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 not (flags & DEBUG):
if len(_cache) >= _MAXCACHE:
_cache.clear()
if p.flags & LOCALE:
if not _locale:
return p
loc = _locale.setlocale(_locale.LC_CTYPE)
else:
loc = None
_cache[type(pattern), pattern, flags] = p, loc
return p
除了使用re.compile带来的小幅速度优势之外,人们还喜欢通过命名潜在复杂的模式规范并将其与应用的业务逻辑分开而带来的可读性:
#### Patterns ############################################################
number_pattern = re.compile(r'd+(.d*)?') # Integer or decimal number
assign_pattern = re.compile(r':=') # Assignment operator
identifier_pattern = re.compile(r'[A-Za-z]+') # Identifiers
whitespace_pattern = re.compile(r'[ ]+') # Spaces and tabs
#### Applications ########################################################
if whitespace_pattern.match(s): business_logic_rule_1()
if assign_pattern.match(s): business_logic_rule_2()
请注意,另一个受访者错误地认为pyc文件直接存储编译后的模式;然而,实际上每次加载 PYC 时都会重建它们:
>>> from dis import dis
>>> with open('tmp.pyc', 'rb') as f:
f.read(8)
dis(marshal.load(f))
1 0 LOAD_CONST 0 (-1)
3 LOAD_CONST 1 (None)
6 IMPORT_NAME 0 (re)
9 STORE_NAME 0 (re)
3 12 LOAD_NAME 0 (re)
15 LOAD_ATTR 1 (compile)
18 LOAD_CONST 2 ('[aeiou]{2,5}')
21 CALL_FUNCTION 1
24 STORE_NAME 2 (lc_vowels)
27 LOAD_CONST 1 (None)
30 RETURN_VALUE
上述反汇编来自tmp.py
包含以下内容的 PYC 文件:
import re
lc_vowels = re.compile(r'[aeiou]{2,5}')
解决方案 9:
使用 re.compile() 还有一个额外的好处,就是可以使用 re.VERBOSE 为我的正则表达式模式添加注释
pattern = '''
hello[ ]world # Some info on my pattern logic. [ ] to recognize space
'''
re.search(pattern, 'hello world', re.VERBOSE)
虽然这不会影响代码的运行速度,但我喜欢这样做,因为这是我注释习惯的一部分。当我想进行修改时,我非常不喜欢花时间去记住 2 个月后代码背后的逻辑。
解决方案 10:
根据 Python文档:
序列
prog = re.compile(pattern)
result = prog.match(string)
相当于
result = re.match(pattern, string)
但是,re.compile()
当表达式在单个程序中多次使用时,使用并保存生成的正则表达式对象以供重用会更有效率。
所以我的结论是,如果你要对许多不同的文本匹配相同的模式,你最好对其进行预编译。
解决方案 11:
总的来说,我发现使用标志更容易(至少更容易记住如何使用),比如re.I
在编译模式时比内联使用标志。
>>> foo_pat = re.compile('foo',re.I)
>>> foo_pat.findall('some string FoO bar')
['FoO']
对比
>>> re.findall('(?i)foo','some string FoO bar')
['FoO']
解决方案 12:
使用给定的示例:
h = re.compile('hello')
h.match('hello world')
上面例子中的匹配方法与下面使用的不一样:
re.match('hello', 'hello world')
re.compile()返回一个正则表达式对象,也就是说h
是一个正则表达式对象。
regex 对象有自己的match方法,带有可选的pos和endpos参数:
regex.match(string[, pos[, endpos]])
位置
可选的第二个参数pos给出了字符串中搜索开始的索引;默认为 0。这并不完全等同于切片字符串;
'^'
模式字符匹配字符串的实际开头和换行符之后的位置,但不一定匹配搜索开始的索引。
结束位置
可选参数endpos限制了字符串的搜索范围;假设字符串长度为endpos个字符,则只会在pos到 之间的字符中
endpos - 1
搜索匹配项。如果endpos小于pos,则不会找到匹配项;否则,如果rx是已编译的正则表达式对象,`rx.search(string, 0,
50)则相当于
rx.search(string[:50], 0)`。
regex 对象的search、findall和finditer方法也支持这些参数。
re.match(pattern, string, flags=0)
如您所见,不支持它们,其search、findall和finditer
对应项
也不支持它们。
匹配对象具有补充这些参数的属性:
匹配位置
传递给正则表达式对象的 search() 或 match() 方法的 pos 值。这是 RE 引擎开始查找匹配项的字符串索引。
匹配.endpos
传递给正则表达式对象的 search() 或 match() 方法的 endpos 值。这是 RE 引擎不会超出的字符串索引。
正则表达式对象具有两个独特且可能有用的属性:
regex.groups
模式中的捕获组的数量。
regex.groupindex
将 (?P) 定义的任何符号组名称映射到组号的字典。如果模式中未使用任何符号组,则字典为空。
最后,匹配对象具有以下属性:
匹配
其 match() 或 search() 方法生成此匹配实例的正则表达式对象。
解决方案 13:
除了表演之外。
使用compile
帮助我区分
1. module(re),
2. 正则表达式对象
3. 匹配对象
的概念
当我开始学习正则表达式时
#regex object
regex_object = re.compile(r'[a-zA-Z]+')
#match object
match_object = regex_object.search('1.Hello')
#matching content
match_object.group()
output:
Out[60]: 'Hello'
V.S.
re.search(r'[a-zA-Z]+','1.Hello').group()
Out[61]: 'Hello'
作为补充,我制作了一个详尽的模块备忘单re
供您参考。
regex = {
'brackets':{'single_character': ['[]', '.', {'negate':'^'}],
'capturing_group' : ['()','(?:)', '(?!)' '|', '\\', 'backreferences and named group'],
'repetition' : ['{}', '*?', '+?', '??', 'greedy v.s. lazy ?']},
'lookaround' :{'lookahead' : ['(?=...)', '(?!...)'],
'lookbehind' : ['(?<=...)','(?<!...)'],
'caputuring' : ['(?P<name>...)', '(?P=name)', '(?:)'],},
'escapes':{'anchor' : ['^', '', '$'],
'non_printable' : ['
', ' ', '
', '', ''],
'shorthand' : ['d', 'w', 's']},
'methods': {['search', 'match', 'findall', 'finditer'],
['split', 'sub']},
'match_object': ['group','groups', 'groupdict','start', 'end', 'span',]
}
解决方案 14:
除了性能差异之外,使用 re.compile 并使用已编译的正则表达式对象进行匹配(无论什么与正则表达式相关的操作)使得 Python 运行时的语义更清晰。
我在调试一些简单代码时有过痛苦的经历:
compare = lambda s, p: re.match(p, s)
稍后我会使用比较
[x for x in data if compare(patternPhrases, x[columnIndex])]
其中patternPhrases
应该是一个包含正则表达式字符串的变量,x[columnIndex]
是一个包含字符串的变量。
patternPhrases
我遇到了与某些预期字符串不匹配的问题!
但如果我使用 re.compile 形式:
compare = lambda s, p: p.match(s)
然后在
[x for x in data if compare(patternPhrases, x[columnIndex])]
Python 会抱怨“字符串没有匹配属性”,因为位置参数映射中compare
,x[columnIndex]
被用作正则表达式!而我实际上的意思是
compare = lambda p, s: p.match(s)
就我而言,使用 re.compile 可以更明确正则表达式的目的,当它的值对肉眼来说是隐藏的时,因此我可以从 Python 运行时检查中获得更多帮助。
因此,我的课程的寓意是,当正则表达式不仅仅是文字字符串时,我应该使用 re.compile 让 Python 帮助我断言我的假设。
解决方案 15:
作为替代答案,由于我发现之前没有提到过,因此我将继续引用Python 3 文档:
您应该使用这些模块级函数,还是应该自己获取模式并调用其方法?如果您在循环内访问正则表达式,则预编译它将节省一些函数调用。在循环之外,由于内部缓存,没有太大区别。
解决方案 16:
有趣的是,编译对我来说确实更有效率(Win XP 上的 Python 2.5.2):
import re
import time
rgx = re.compile('(w+)s+[0-9_]?s+w*')
str = "average 2 never"
a = 0
t = time.time()
for i in xrange(1000000):
if re.match('(w+)s+[0-9_]?s+w*', str):
#~ if rgx.match(str):
a += 1
print time.time() - t
先按原样运行上述代码一次,然后再将这两if
行注释反过来运行一次,编译后的正则表达式速度会快一倍
解决方案 17:
在偶然发现此处的讨论之前,我进行了这项测试。不过,运行之后,我想至少发布一下我的结果。
我窃取并篡改了 Jeff Friedl 的《掌握正则表达式》中的示例。这是在运行 OSX 10.6(2Ghz intel core 2 duo,4GB 内存)的 MacBook 上进行的。Python 版本是 2.6.1。
运行 1 - 使用 re.compile
import re
import time
import fpformat
Regex1 = re.compile('^(a|b|c|d|e|f|g)+$')
Regex2 = re.compile('^[a-g]+$')
TimesToDo = 1000
TestString = ""
for i in range(1000):
TestString += "abababdedfg"
StartTime = time.time()
for i in range(TimesToDo):
Regex1.search(TestString)
Seconds = time.time() - StartTime
print "Alternation takes " + fpformat.fix(Seconds,3) + " seconds"
StartTime = time.time()
for i in range(TimesToDo):
Regex2.search(TestString)
Seconds = time.time() - StartTime
print "Character Class takes " + fpformat.fix(Seconds,3) + " seconds"
Alternation takes 2.299 seconds
Character Class takes 0.107 seconds
运行 2 - 不使用 re.compile
import re
import time
import fpformat
TimesToDo = 1000
TestString = ""
for i in range(1000):
TestString += "abababdedfg"
StartTime = time.time()
for i in range(TimesToDo):
re.search('^(a|b|c|d|e|f|g)+$',TestString)
Seconds = time.time() - StartTime
print "Alternation takes " + fpformat.fix(Seconds,3) + " seconds"
StartTime = time.time()
for i in range(TimesToDo):
re.search('^[a-g]+$',TestString)
Seconds = time.time() - StartTime
print "Character Class takes " + fpformat.fix(Seconds,3) + " seconds"
Alternation takes 2.508 seconds
Character Class takes 0.109 seconds
解决方案 18:
这个答案可能来得晚了,但这是一个有趣的发现。如果您打算多次使用正则表达式,使用编译确实可以节省您的时间(文档中也提到了这一点)。下面您可以看到,当直接调用 match 方法时,使用编译的正则表达式是最快的。将编译的正则表达式传递给 re.match 会使它变得更慢,而将带有模式字符串的 re.match 传递则介于两者之间。
>>> ipr = r'D+((([0-2][0-5]?[0-5]?).){3}([0-2][0-5]?[0-5]?))D+'
>>> average(*timeit.repeat("re.match(ipr, 'abcd100.10.255.255 ')", globals={'ipr': ipr, 're': re}))
1.5077415757028423
>>> ipr = re.compile(ipr)
>>> average(*timeit.repeat("re.match(ipr, 'abcd100.10.255.255 ')", globals={'ipr': ipr, 're': re}))
1.8324008992184038
>>> average(*timeit.repeat("ipr.match('abcd100.10.255.255 ')", globals={'ipr': ipr, 're': re}))
0.9187896518778871
解决方案 19:
尽管这两种方法在速度方面具有可比性,但您应该知道,仍然存在一些可以忽略不计的时间差异,如果您要处理数百万次迭代,这可能会引起您的担忧。
以下进行速度测试:
import re
import time
SIZE = 100_000_000
start = time.time()
foo = re.compile('foo')
[foo.search('bar') for _ in range(SIZE)]
print('compiled: ', time.time() - start)
start = time.time()
[re.search('foo', 'bar') for _ in range(SIZE)]
print('uncompiled:', time.time() - start)
得出以下结果:
compiled: 14.647532224655151
uncompiled: 61.483458042144775
在我的 PC 上(使用 Python 3.7.0),编译方法的速度始终快 4 倍左右。
正如文档中解释的那样:
如果您在循环内访问正则表达式,则预编译它将节省一些函数调用。在循环之外,由于内部缓存的存在,区别并不大。
解决方案 20:
我非常尊重上述所有答案。在我看来是的!当然,使用 re.compile 而不是每次都编译正则表达式是值得的。
使用re.compile可使您的代码更具动态性,因为您可以调用已编译的正则表达式,而不必一次又一次地进行编译。这在以下情况下对您有好处:
处理器的努力
时间复杂度。
使正则表达式通用。(可用于 findall、search、match)
并使你的程序看起来很酷。
例子 :
example_string = "The room number of her room is 26A7B."
find_alpha_numeric_string = re.compile(r"w+")
在 Findall 中使用
find_alpha_numeric_string.findall(example_string)
在搜索中使用
find_alpha_numeric_string.search(example_string)
同样,你可以使用它来:匹配和替换
解决方案 21:
(几个月后)在 re.match 周围添加自己的缓存或其他任何内容都很容易——
""" Re.py: Re.match = re.match + cache
efficiency: re.py does this already (but what's _MAXCACHE ?)
readability, inline / separate: matter of taste
"""
import re
cache = {}
_re_type = type( re.compile( "" ))
def match( pattern, str, *opt ):
""" Re.match = re.match + cache re.compile( pattern )
"""
if type(pattern) == _re_type:
cpat = pattern
elif pattern in cache:
cpat = cache[pattern]
else:
cpat = cache[pattern] = re.compile( pattern, *opt )
return cpat.match( str )
# def search ...
一个 wibni,如果:cachehint(size= ),cacheinfo() -> size,hits,nclear ... 那不是很好吗?
解决方案 22:
我有过运行编译好的正则表达式数千次与即时编译的经验,并没有发现任何明显的区别
对已接受答案的投票导致人们假设 @Triptych 所说的内容在所有情况下都是正确的。但这不一定正确。一个很大的区别是,当您必须决定是否接受正则表达式字符串或已编译的正则表达式对象作为函数的参数时:
>>> timeit.timeit(setup="""
... import re
... f=lambda x, y: x.match(y) # accepts compiled regex as parameter
... h=re.compile('hello')
... """, stmt="f(h, 'hello world')")
0.32881879806518555
>>> timeit.timeit(setup="""
... import re
... f=lambda x, y: re.compile(x).match(y) # compiles when called
... """, stmt="f('hello', 'hello world')")
0.809190034866333
最好编译你的正则表达式,以备需要重复使用它们。
请注意,上面 timeit 中的示例模拟了在导入时创建一次已编译的正则表达式对象,而不是在匹配时“即时”创建。
解决方案 23:
在 Ubuntu 22.04 上:
$ python --version
Python 3.10.6
$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 're.match("[0-9]{3}-[0-9]{3}-[0-9]{4}", "123-123-1234")'; done
1 loop, best of 5: 972 nsec per loop
:0: UserWarning: The test results are likely unreliable. The worst time (186 usec) was more than four times slower than the best time (972 nsec).
10 loops, best of 5: 819 nsec per loop
:0: UserWarning: The test results are likely unreliable. The worst time (13.9 usec) was more than four times slower than the best time (819 nsec).
100 loops, best of 5: 763 nsec per loop
1000 loops, best of 5: 699 nsec per loop
10000 loops, best of 5: 653 nsec per loop
100000 loops, best of 5: 655 nsec per loop
1000000 loops, best of 5: 656 nsec per loop
$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 'r = re.compile("[0-9]{3}-[0-9]{3}-[0-9]{4}")' 'r.match("123-123-1234")'; done
1 loop, best of 5: 985 nsec per loop
:0: UserWarning: The test results are likely unreliable. The worst time (134 usec) was more than four times slower than the best time (985 nsec).
10 loops, best of 5: 775 nsec per loop
:0: UserWarning: The test results are likely unreliable. The worst time (13.9 usec) was more than four times slower than the best time (775 nsec).
100 loops, best of 5: 756 nsec per loop
1000 loops, best of 5: 701 nsec per loop
10000 loops, best of 5: 704 nsec per loop
100000 loops, best of 5: 654 nsec per loop
1000000 loops, best of 5: 651 nsec per loop
解决方案 24:
使用第二个版本时,正则表达式在使用前会先进行编译。如果您要多次执行它,最好先进行编译。如果不是每次匹配一次就进行编译,那就没问题。
解决方案 25:
这是个好问题。你经常会看到人们毫无理由地使用 re.compile。这会降低可读性。但当然有很多时候需要预编译表达式。比如当你在循环中重复使用它时。
这就像编程的一切(实际上生活中的一切)。运用常识。
解决方案 26:
可读性/认知负荷偏好
对我来说,主要的收获是我只需要记住并阅读复杂的正则表达式 API 语法的一种<compiled_pattern>.method(xxx)
形式 -形式,而不是那种形式和那种re.func(<pattern>, xxx)
形式。
确实,这re.compile(<pattern>)
是一些额外的样板。
但就正则表达式而言,额外的编译步骤不太可能成为认知负荷的主要原因。事实上,对于复杂的模式,你甚至可以通过将声明与随后调用的正则表达式方法分开来获得清晰度。
我倾向于首先在像 Regex101 这样的网站中,甚至在单独的最小测试脚本中调整复杂的模式,然后将它们带入我的代码中,因此将声明与其使用分开也适合我的工作流程。
解决方案 27:
我想说明的是,预编译在概念上和“文学上”(如“文学编程”)都是有利的。看一下这个代码片段:
from re import compile as _Re
class TYPO:
def text_has_foobar( self, text ):
return self._text_has_foobar_re_search( text ) is not None
_text_has_foobar_re_search = _Re( r"""(?i)foobar""" ).search
TYPO = TYPO()
在你的申请中你可以写:
from TYPO import TYPO
print( TYPO.text_has_foobar( 'FOObar ) )
就功能而言,这已经是最简单的了。因为这个例子太短了,所以我把_text_has_foobar_re_search
所有方法都放在一行中。这段代码的缺点是,无论TYPO
库对象的生命周期是多长,它都会占用一点内存;优点是,当进行 foobar 搜索时,你只需要调用两个函数和两次类字典查找。这里与多少个正则表达式被缓存re
以及该缓存的开销无关。
将其与下面更常见的样式进行比较:
import re
class Typo:
def text_has_foobar( self, text ):
return re.compile( r"""(?i)foobar""" ).search( text ) is not None
在应用程序中:
typo = Typo()
print( typo.text_has_foobar( 'FOObar ) )
我欣然承认,我的风格对于 Python 来说极不寻常,甚至是有争议的。然而,在更接近 Python 最常用方式的示例中,为了进行单个匹配,我们必须实例化一个对象,执行三个实例字典查找,并执行三个函数调用;此外,re
当使用超过 100 个正则表达式时,我们可能会遇到缓存问题。而且,正则表达式隐藏在方法体内,大多数时候这不是一个好主意。
可以说,每个措施的子集——有针对性的、别名的导入语句;适用的别名方法;减少函数调用和对象字典查找——都可以帮助减少计算和概念复杂性。
解决方案 28:
我的理解是,这两个例子实际上是等价的。唯一的区别是,在第一个例子中,你可以在其他地方重用已编译的正则表达式,而不会导致它再次被编译。
这里有一个参考:http://diveintopython3.ep.io/refactoring.html
使用字符串“M”调用已编译模式对象的搜索函数与使用正则表达式和字符串“M”调用 re.search 可实现相同的功能。只是速度要快得多。(实际上,re.search 函数只是编译正则表达式并为您调用生成的模式对象的搜索方法。)