当前位置:  开发笔记 > 编程语言 > 正文

是否值得使用Python的re.compile?

如何解决《是否值得使用Python的re.compile?》经验,为你挑选了8个好方法。

在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

我仍然经常预编译正则表达式,但只是将它们绑定到一个漂亮的,可重用的名称,而不是任何预期的性能增益.



1> Triptych..:

我有很多运行编译正则表达式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

我仍然经常预编译正则表达式,但只是将它们绑定到一个漂亮的,可重用的名称,而不是任何预期的性能增益.


JF Sebastian,它向程序员发出一个信号,即正在使用的正则表达式将被大量使用,而不是一次性使用.
更重要的是,我要说如果你不想在你的应用程序的一些性能关键部分遭受编译和缓存命中,你最好在应用程序的非关键部分之前编译它们. .
如果您多次重复使用相同的正则表达式,我看到使用已编译的正则表达式的主要优点,从而减少了拼写错误的可能性.如果您只是调用它一次然后未编译就更具可读性.
所以,主要区别在于你使用了很多不同的正则表达式(超过_MAXCACHE),其中一些只有一次而其他的很多次......那么保持编译后的表达式对于那些使用得更多的表达式很重要当它满了时,它不会从缓存中刷新.
你的结论与你的答案不一致.如果自动编译和存储正则表达式,则在大多数情况下不需要手动执行.
我只能在2.5+和3.0中添加_MAXCACHE = 100.
我认为即使你排除错字和GC的未知时间,每个人都错过了整体观点,事实是如果你需要连续运行相同的正则表达100,000次而不必进行缓存查找100,000次更快,让我们考虑一下在解析带有正则表达式的大型日志文件方面,无论如何,lanauage都必须采取更好的措施.
如果您使用的是python <2.7或3.1,则"re.sub"缺少'flags'参数.因此,如果你想做不区分大小写的re.sub,你就会陷入``re.compile("...",re.I).sub(...)``.
如果它被饱和,整个缓存就被清除了?!?!?我要去的是LFU或LRU缓存.甚至更多的理由来编译我打算不止一次使用的模式.你永远不知道是否还有一些其他模块也会填充并清除缓存.
@JF - 另外,如果你依赖于编译和缓存,谁知道什么时候可以清除缓存,然后你的正则表达式将不得不重新编译.
""我有很多运行编译正则表达式1000次的经验,而不是即时编译,并没有注意到任何可察觉的差异......."*这太模糊和误导.使用预编译的正则表达式比第二次使用快3倍,甚至第一次使用速度快2倍.点.问题是,如果正则表达式速度对于某个特定的速度至关重要在大多数情况下,直接使用ad hoc模式**更容易编写,读取和调试.

2> 小智..:

对我来说,最大的好处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)

虽然它非常接近,但是当反复使用时,第二行的最后一行感觉更自然,更简单.


我同意这个答案; 通常使用re.compile会产生更多,而不是更少的可读代码.

3> dF...:

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(...),即编译表示的幕后缓存似乎没有发生.


@Triptych,@ Kiv:编译正则表达式与使用*分开的重点是*以最小化编译; 将它从时间中移除正是dF应该做的,因为它最准确地代表了现实世界的使用.编译时间与timeit.py在此处的计时方式特别无关; 它执行多次运行并且仅报告最短的运行,此时编译的正则表达式被缓存.您在这里看到的额外成本不是编译正则表达式的成本,而是在编译的正则表达式缓存(字典)中查找它的成本.
我明白你的意思了,但是在正确使用regexp多次的实际应用程序中究竟会发生什么?
这里你的方法存在的主要问题,因为setup参数不包括在时间中.因此,您已从第二个示例中删除了编译时间,并在第一个示例中将其平均化.这并不意味着第一个示例每次都会编译.
@Triptych应该将`import re`移出设置吗?这都是关于你想要衡量的地方.如果我多次运行python脚本,它将会导致`import re`时间.比较两者时,将两条线分开以进行计时非常重要.是的,就像你说的那样,你将有时间命中.比较显示,您要么花费一次时间命中并重复较少的时间来进行编译,要么每当假设缓存在两次调用之间被清除时您都会获得命中,这可能会发生.添加`h = re.compile('hello')的时间将有助于澄清.

4> david king..:

这是一个简单的测试用例:

~$ 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

因此,即使您只匹配一次,在这个简单的情况下,编译看起来似乎更快.


这是哪个版本的Python?
并不重要,关键是在将要运行代码的环境中尝试基准测试
在我的Python 2.7.3设置上几乎没有任何区别.有时编译速度更快,有时速度更慢.差异始终<5%,因此我将差异计算为测量不确定度,因为设备只有一个CPU.

5> George..:

我自己试过这个.对于从字符串中解析数字并对其求和的简单情况,使用编译的正则表达式对象的速度大约是使用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
And 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次,而不是一百万次.



6> John Pang..:

我同意诚实的安倍,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两次,它将执行两次搜索和一次编译(因为正则表达式对象被缓存).

    如果_cacheget刷新,则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)

谢谢阅读.



7> Raymond Hett..:

大多数情况下,无论是否使用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}')



8> 小智..:

一般来说,我发现使用标志更容易(至少更容易记住),比如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']

推荐阅读
Chloemw
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有