在Python中使用正则表达式编译有什么好处吗?
h = re.compile('hello') h.match('hello world')
VS
re.match('hello', 'hello world')
Triptych.. 401
我有很多运行编译正则表达式1000次的经验,而不是即时编译,并没有注意到任何可察觉的差异.显然,这是轶事,当然不是反编译的好理由,但我发现差异可以忽略不计.
编辑:在快速浏览一下实际的Python 2.5库代码之后,我看到Python内部编译AND CACHES正则表达式无论如何都要使用它们(包括调用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
我仍然经常预编译正则表达式,但只是将它们绑定到一个漂亮的,可重用的名称,而不是任何预期的性能增益.
我有很多运行编译正则表达式1000次的经验,而不是即时编译,并没有注意到任何可察觉的差异.显然,这是轶事,当然不是反编译的好理由,但我发现差异可以忽略不计.
编辑:在快速浏览一下实际的Python 2.5库代码之后,我看到Python内部编译AND CACHES正则表达式无论如何都要使用它们(包括调用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
我仍然经常预编译正则表达式,但只是将它们绑定到一个漂亮的,可重用的名称,而不是任何预期的性能增益.
对我来说,最大的好处re.compile
是没有任何一种过早优化的(这是万恶之源,反正).它能够将正则表达式的定义与其使用分开.
即使是一个简单的表达式,例如0|[1-9][0-9]*
(基数为10的整数,没有前导零)也足够复杂,你不必重新输入它,检查是否有任何拼写错误,然后在开始调试时必须重新检查是否存在拼写错误.另外,使用变量名称(例如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)
虽然它非常接近,但是当反复使用时,第二行的最后一行感觉更自然,更简单.
FWIW:
$ 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
如果你怀疑你的regexp可能成为性能瓶颈,我认为你真的不会失去太多的清晰度/直截了当.
更新:
在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(...)
,即编译表示的幕后缓存似乎没有发生.
这是一个简单的测试用例:
~$ 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
用re.compile:
~$ 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
因此,即使您只匹配一次,在这个简单的情况下,编译看起来似乎更快.
我自己试过这个.对于从字符串中解析数字并对其求和的简单情况,使用编译的正则表达式对象的速度大约是使用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 = ", r5And here is the output on my machine:
$ 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次,而不是一百万次.
我同意诚实的安倍,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),其也被(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)
方法有优势.以下是我观察到的一些事实(如果我错了请纠正我):
调用一次,它将在_cache
后面执行一次搜索sre_compile.compile()
以创建一个正则表达式对象.调用A两次,它将执行两次搜索和一次编译(因为正则表达式对象被缓存).
如果_cache
get刷新,则regex对象从内存中释放,Python需要再次编译.(有人建议Python不会重新编译.)
如果我们使用(A)保留正则表达式对象,则正则表达式对象仍将进入_cache并以某种方式刷新.但是我们的代码会对它进行引用,并且regex对象不会从内存中释放出来.那些,Python不需要再次编译.
George的testInLoop vs编译的2秒差异主要是构建密钥和搜索_cache所需的时间.它并不意味着正则表达式的编译时间.
George的真正编译测试显示了每次真正重新编译时会发生什么:它会慢100倍(他将循环从1,000,000减少到10,000).
以下是(A + B)优于(C)的唯一情况:
如果我们可以在类中缓存正则表达式对象的引用.
如果我们需要重复调用(B)(在循环内或多次),我们必须在循环外缓存对regex对象的引用.
(C)足够好的情况:
我们无法缓存参考.
我们偶尔使用它一次.
总的来说,我们没有太多的正则表达式(假设编译的一个永远不会被刷新)
简而言之,这是ABC:
h = re.compile('hello') # (A) h.match('hello world') # (B) re.match('hello', 'hello world') # (C)
谢谢阅读.
大多数情况下,无论是否使用re.compile,都没什么区别.在内部,所有函数都是在编译步骤中实现的:
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'[\t ]+') # 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
以上反汇编来自PYC文件,其中tmp.py
包含:
import re lc_vowels = re.compile(r'[aeiou]{2,5}')
一般来说,我发现使用标志更容易(至少更容易记住),比如re.I
编译模式比使用内联标志更容易.
>>> foo_pat = re.compile('foo',re.I) >>> foo_pat.findall('some string FoO bar') ['FoO']
VS
>>> re.findall('(?i)foo','some string FoO bar') ['FoO']