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


461

在Python中对正则表达式使用compile有什么好处?

h = re.compile('hello')
h.match('hello world')

re.match('hello', 'hello world')

8
除此之外,在2.6中re.sub不会接受标记的事实……
new123456 2011年

58
我只是遇到一个案例,其中使用re.compile给出了10-50倍的改进。道德是,如果您有很多正则表达式(超过MAXCACHE = 100个),并且每次都使用它们很多次(并且之间间隔了不止MAXCACHE个正则表达式,那么每个正则表达式都会从缓存中清除:所以使用多次重复相同的操作,然后再进行下一个操作就不算在内),那么绝对可以帮助编译它们。否则,它不会有所作为。
ShreevatsaR 2013年

8
需要注意的一件事是,对于不需要正则表达式的in字符串,字符串子字符串测试的速度更快:>python -m timeit -s "import re" "re.match('hello', 'hello world')" 1000000 loops, best of 3: 1.41 usec per loop >python -m timeit "x = 'hello' in 'hello world'" 10000000 loops, best of 3: 0.0513 usec per loop
Gamrix 2015年

@ShreevatsaR有趣!您可以通过示例将答案提高10到50倍来发布答案吗?实际上,这里给出的大多数答案在某些精确的情况下显示出3倍的改进,而在其他情况下几乎没有改善。
巴吉

1
@Basj完成,发布了答案。在2013年12月,我没有费心去挖掘使用Python的东西,但是我尝试的第一件简单的事情却显示出相同的行为。
ShreevatsaR

Answers:


436

与动态编译相比,我有1000多次运行已编译的正则表达式的经验,并且没有注意到任何可察觉的差异。显然,这是轶事,当然也不是反对编译的一个很好的论据,但是我发现区别可以忽略不计。

编辑:快速浏览一下实际的Python 2.5库代码后,我发现无论何时使用Python(包括对的调用re.match()),Python都会在内部编译和缓存正则表达式,因此您实际上只是在更改正则表达式时进行更改,因此根本不会节省太多时间-仅节省检查缓存(在内部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

我仍然经常预编译正则表达式,但只是将它们绑定到一个不错的,可重用的名称上,而不是为了获得预期的性能提升。


12
您的结论与您的答案不一致。如果正则表达式是自动编译和存储的,则在大多数情况下无需手动进行。
jfs

84
JF Sebastian,它向程序员发出信号,即正则表达式中的正则表达式将被大量使用,而不是一掷千金。
kaleissin

40
更重要的是,我想说的是,如果您不想在应用程序的某些性能关键部分遭受编译和缓存的打击,那么最好在将其移交给应用程序的非关键部分之前先对其进行编译。
艾迪·帕克

20
如果您多次重复使用同一个正则表达式,我将看到使用已编译正则表达式的主要优势,从而减少了输入错误的可能性。如果您只调用它一次,那么未编译的可读性更高。
09年

18
因此,主要区别在于,当您使用许多不同的正则表达式(而不是_MAXCACHE)时,其中一些仅使用一次,而另一些则使用很多次……那么,重要的是要为更多使用的表达式保留编译表达式,以便它们充满时不会从缓存中清除。
fortran

133

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

尽管距离很近,但是第二遍的最后一行在重复使用时感觉更自然,更简单。


5
我同意这个答案。经常使用re.compile会产生更多而不是更少的可读性代码。
卡尔·迈尔

1
但是,有时候情况恰恰相反-例如,如果您在一个地方定义正则表达式,然后在另一个遥远的地方使用它的匹配组。
肯·威廉姆斯

1
@KenWilliams不一定,即使用于原始定义的正则表达式也应清楚明确。例如us_phone_numbersocial_security_number等等
Brian M. Sheldon

2
@ BrianM.Sheldon很好地命名正则表达式并不能真正帮助您了解其各个捕获组所代表的含义。
肯·威廉姆斯

68

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如果您怀疑正则表达式可能会成为性能瓶颈,那么我认为您不会因为使用正则表达式而损失太多的清晰度/简单性。

更新:

在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(...),即,似乎没有发生已编译表示形式的后台缓存。


5
这里的方法存在主要问题,因为时间不包括setup参数。因此,您已从第二个示例中删除了编译时间,并在第一个示例中将其平均了。这并不意味着第一个示例每次都会编译。
三联画

1
是的,我同意这不是两种情况的公平比较。
科伊夫

7
我明白您的意思,但这不是在经常使用regexp的实际应用程序中会发生的情况吗?
dF。

26
@ Triptych,@ Kiv:将正则表达式与使用分开编译的全部目的使编译最少。从时序中删除它正是dF应该做的,因为它最准确地表示了实际使用情况。编译时间与timeit.py在此处进行计时的方式尤其无关。它执行几次运行,并且仅报告最短的一次,此时将缓存已编译的正则表达式。您在这里看到的额外开销不是编译正则表达式的开销,而是在已编译的正则表达式缓存(字典)中查找的开销。
jemfinch

3
@Triptych是否应将其import re移出设置?这就是您要测量的地方。如果我多次运行python脚本,那会很费import re时间。比较两者时,重要的是将两条线分开以进行计时。是的,正如您所说的,这是您需要付出时间的时候。比较结果表明,要么一次点击一次命中,然后重复一次较小的一次编译命中,要么每次都假设一次命中,就假设两次调用之间的缓存被清除了,这可能会发生。添加的时间h=re.compile('hello')有助于澄清。
Tom Myddeltyn '16年

39

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

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

因此,即使只匹配一次,使用这种简单的情况似乎编译起来也会更快。


2
这是哪个版本的Python?
Kyle Strand

2
其实并不重要,关键是在您将要运行代码的环境中尝试基准测试
david king

1
对我来说,对于1000个循环或更多循环,性能几乎完全相同。编译版本对于1-100个循环更快。(在python 2.7和3.4上)。
Zitrax 2015年

2
在我的Python 2.7.3设置上几乎没有任何区别。有时编译速度更快,有时ist速度更慢。差异始终小于5%,因此我将该差异计为测量不确定性,因为该设备只有一个CPU。
达卡龙2015年

1
在Python 3.4.3中,有两个不同的运行时间:使用编译甚至比未编译要慢。
Zelphir Kaltstahl '16

17

我自己尝试过。对于从字符串中解析数字并将其求和的简单情况,使用已编译的正则表达式对象的速度大约是使用正则表达式对象的两倍。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万次。


我同意您的看法,即已编译的正则表达式比未编译的正则表达式运行快得多。我运行了10,000多个句子,并在其中进行了循环,以便在不编译正则表达式时进行迭代,并在每次预测运行8个小时后计算一次正则表达式,然后根据已编译正则表达式模式的索引创建字典整个过程持续2分钟。我无法理解上面的答案...
Eli Borodach,

12

我同意诚实的安倍晋三的观点,即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(...)仅被调用一次,我们需要将已编译的regex对象存储在内存中。如果使用的是类,则可以存储对象并在每次调用函数时重用。

class Foo:
    regex = re.compile('hello')
    def my_function(text)
        return regex.match(text)

如果我们不使用类(今天是我的要求),那么我无可奉告。我仍在学习在Python中使用全局变量,并且我知道全局变量是一件坏事。

还有一点,我认为使用(A) + (B)方法具有优势。这是我观察到的一些事实(如果我记错了,请纠正我):

  1. 调用A一次,它将先搜索一次,然后再搜索_cache一次sre_compile.compile()以创建正则表达式对象。调用A两次,它将进行两次搜索和一次编译(因为正则表达式对象已缓存)。

  2. 如果 _cache两者之间被刷新,则正则表达式对象将从内存中释放出来,Python需要再次编译。(有人建议Python不会重新编译。)

  3. 如果我们使用(A)保留regex对象,则regex对象仍将进入_cache并以某种方式刷新。但是我们的代码对此保留了引用,并且正则表达式对象不会从内存中释放。这些,Python无需再次编译。

  4. 乔治测试的compedInLoop与已编译的测试之间的2秒差异主要是构建密钥和搜索_cache所需的时间。这并不意味着正则表达式的编译时间。

  5. George的真正编译测试显示了每次真正重新进行编译会发生什么情况:它将慢100倍(他将循环从1,000,000减少到10,000)。

以下是(A + B)优于(C)的情况:

  1. 如果我们可以在一个类中缓存正则表达式对象的引用。
  2. 如果需要重复(在循环内或多次)调用(B),则必须在循环外缓存对regex对象的引用。

(C)足够好的情况:

  1. 我们无法缓存参考。
  2. 我们只会偶尔使用一次。
  3. 总的来说,我们没有太多的正则表达式(假设编译过的正则表达式永远不会被刷新)

回顾一下,这里是ABC:

h = re.compile('hello')                   # (A)
h.match('hello world')                    # (B)
re.match('hello', 'hello world')          # (C)

谢谢阅读。


8

通常,是否使用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}')

1
"def search(pattern, string, flags=0):"一个错字?
phuclv

1
请注意,如果pattern已经是已编译的模式,则缓存开销将变得非常大:对a SRE_Pattern进行哈希处理非常昂贵,并且永远不会将模式写入缓存,因此每次使用a都会导致查找失败KeyError
艾里克·杜米尼尔

5

通常,我发现使用标志(至少更容易记住操作方式)(例如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']

re.findall无论如何,您也可以将标志用作第三个参数。
aderchox

5

使用给定的示例:

h = re.compile('hello')
h.match('hello world')

上面的示例中的match方法与以下使用的方法不同:

re.match('hello', 'hello world')

re.compile()返回一个正则表达式对象,这意味着它h是一个正则表达式对象。

regex对象具有自己的match方法,该方法带有可选的posendpos参数:

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对象的searchfindallfinditer方法也支持这些参数。

re.match(pattern, string, flags=0)如您所见,它不支持它们,
也不支持searchfindallfinditer对应项。

一个匹配对象有补充这些参数属性:

match.pos

传递给正则表达式对象的search()或match()方法的pos值。这是RE引擎开始寻找匹配项的字符串索引。

match.endpos

传递给正则表达式对象的search()或match()方法的endpos的值。这是字符串的索引,RE引擎将超出该索引。


一个正则表达式对象有两个独特的,可能有用的,属性:

正则表达式组

模式中的捕获组数。

正则表达式

字典,将由(?P)定义的任何符号组名映射到组号。如果模式中未使用任何符号组,则词典为空。


最后,match对象具有以下属性:

匹配

其match()或search()方法产生此match实例的正则表达式对象。


4

除了性能差异外,使用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])]

蟒蛇会抱怨“字符串没有匹配的属性”,如在位置参数映射comparex[columnIndex]作为正则表达式!当我真正的意思

compare = lambda p, s: p.match(s)

在我的情况下,使用re.compile可以更清楚地说明正则表达式的目的,当它的值被肉眼隐藏时,因此可以从Python运行时检查中获得更多帮助。

因此,我这堂课的寓意是,当正则表达式不仅仅是文字字符串时,我应该使用re.compile来让Python帮助我断言自己的假设。


4

使用re.compile()有一个额外的好处,即使用re.VERBOSE向我的正则表达式模式添加注释

pattern = '''
hello[ ]world    # Some info on my pattern logic. [ ] to recognize space
'''

re.search(pattern, 'hello world', re.VERBOSE)

尽管这不会影响运行代码的速度,但我喜欢这样做,因为它是我注释习惯的一部分。当我想进行修改时,我完全不喜欢花时间试图记住代码后面2个月的逻辑。


1
我已经编辑了您的答案。我认为提及re.VERBOSE是值得的,它确实增加了其他答案似乎遗漏的内容。但是,以“我在这里发帖,因为我还不能发表评论”引导您的答案,一定会删除它。除答案外,请勿将答案框用于其他任何内容。能够在任何地方发表评论(50个代表)只是一个或两个好的答案,所以请耐心等待。当您知道自己不应该在注释框中添加评论不会更快地到达那里。它将使您不满意并删除答案。
skrrgwasme,2015年

4

根据Python 文档

序列

prog = re.compile(pattern)
result = prog.match(string)

相当于

result = re.match(pattern, string)

但是使用 re.compile(),当在单个程序中多次使用表达式时,并保存生成的正则表达式对象以供重用会更有效。

所以我的结论是,如果您要为许多不同的文本匹配相同的模式,则最好对其进行预编译。


3

有趣的是,编译对我来说确实更有效(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相反的方式注释两行,则编译后的regex的运行速度快一倍


2
与dF的性能比较存在相同的问题。除非您包括编译语句本身的性能成本,否则这是不公平的。
卡尔·迈尔

6
卡尔,我不同意。编译只执行一次,而匹配循环执行一百万次
Eli Bendersky

@eliben:我同意Carl Meyer的观点。两种情况下都会进行编译。Triptych提到涉及缓存,因此在最佳情况下(保留在缓存中),两种方法都是O(n + 1),尽管当您不显式使用re.compile时+1部分是隐藏的。
辣椒粉

1
不要编写自己的基准测试代码。学习使用timeit.py,它包含在标准发行版中。
jemfinch

您需要花费多少时间在for循环中重新创建模式字符串。这笔开销是微不足道的。
IceArdor 2014年

3

在绊倒这里的讨论之前,我进行了此测试。但是,运行它后,我认为我至少会发布结果。

我偷了杰夫·弗里德尔(Jeff Friedl)的“精通正则表达式”(Mastering Regular Expressions)中的示例并将其混为一谈。这是在运行OSX 10.6(2Ghz Intel Core 2 duo,4GB ram)的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

3

这个答案可能迟到了,但这是一个有趣的发现。如果您打算多次使用正则表达式,那么使用编译确实可以节省您的时间(在文档中也提到了这一点)。在下面您可以看到,直接在其上调用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

3

除了表现。

使用compile帮助我区分
1. module(re)
2. regex对象
3. match对象的概念
当我开始学习regex时

#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'          : ['^', '\b', '$'],
          'non_printable'   : ['\n', '\t', '\r', '\f', '\v'],
          'shorthand'       : ['\d', '\w', '\s']},
'methods': {['search', 'match', 'findall', 'finditer'],
              ['split', 'sub']},
'match_object': ['group','groups', 'groupdict','start', 'end', 'span',]
}

2

我真的尊重上述所有答案。我认为是的!当然,值得一次使用re.compile而不是一次编译regex。

使用re.compile可以使您的代码更具动态性,因为您可以调用已编译的regex,而无需再次编译。在以下情况下,这件事会使您受益:

  1. 处理器的工作
  2. 时间复杂度。
  3. 使正则表达式通用。(可用于findall,search,match)
  4. 并使您的程序看起来很酷。

范例:

  example_string = "The room number of her room is 26A7B."
  find_alpha_numeric_string = re.compile(r"\b\w+\b")

在Findall中使用

 find_alpha_numeric_string.findall(example_string)

在搜索中使用

  find_alpha_numeric_string.search(example_string)

同样,您可以将其用于:匹配和替换


1

这是一个很好的问题。您经常看到人们无缘无故地使用re.compile。它降低了可读性。但是请确保在很多时候需要对表达式进行预编译。就像您在循环中重复使用它或类似方法时一样。

就像关于编程的一切(实际上生活中的一切)一样。应用常识。


从我的简要介绍中可以看出,“ 坚果壳中的Python”没有提到没有re.compile()的用法,这使我感到好奇。

正则表达式对象将另外一个对象添加到上下文中。如我所说,在很多情况下re.compile()都有它的位置。OP给出的例子不是其中之一。
PEZ 2012年

1

(几个月后),您可以轻松地在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 ...


1

与动态编译相比,我有1000多次运行已编译的正则表达式的经验,并且没有注意到任何可察觉的差异

对已接受答案的投票导致一个假设,即@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中的示例在导入时一次模拟了一个已编译的regex对象的创建,而在进行匹配时则模拟了“即时”的创建。


1

作为一个替代的答案,如我所见,以前没有提到过,我将继续引用Python 3文档

您应该使用这些模块级功能,还是应该自己获取模式并调用其方法?如果要在循环中访问正则表达式,则对其进行预编译将节省一些函数调用。在循环之外,由于内部缓存,差异不大。


1

这是一个示例,其中使用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.\n')

start = time.time()
print(without_compile_cache_friendly())
d2 = time.time() - start
print(f'-- That took {d2:.2f} seconds.\n')

start = time.time()
print(without_compile())
d3 = time.time() - start
print(f'-- That took {d3:.2f} seconds.\n')

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使用,多次使用相同的regex并移至下一个也不是很糟糕(大约是的慢2倍re.compile),但以另一种顺序(遍历许多regexes),则更糟,正如预期的那样。另外,增加缓存大小也可以:仅re._MAXCACHE = len(patterns)setup()上面进行设置(当然,我不建议在生产环境中进行此类操作,因为带下划线的名称通常是“私有”的)将〜23秒降低为〜0.7秒,这也符合我们的理解。


PS:如果我在整个代码中使用3个正则表达式模式,每个模式都使用了数百次(无特定顺序),则正则表达式缓存将自动保留预编译的正则表达式,对吗?
巴吉

@Basj我想您可以尝试看一下:)但是,我敢肯定,答案是肯定的:在这种情况下,AFAICT唯一需要支付的额外费用就是仅在缓存中查找模式。还要注意,缓存是全局的(模块级),因此原则上您可以在它们之间使用一些依赖库来进行正则表达式搜索,因此很难完全确定您的程序仅使用了3个(或任意数量的)正则表达式模式,但否则会很奇怪:)
ShreevatsaR

0

使用第二个版本时,正则表达式在使用前先进行编译。如果要执行多次,最好先编译一下。如果您每次都不匹配,则无需编译。


0

易读性/认知负荷偏好

对我来说,主要的收获是,我只需要记住,读,一个复杂的正则表达式语法API的形式-的<compiled_pattern>.method(xxx)形式,而不是re.func(<pattern>, xxx)形式。

re.compile(<pattern>)有点多余样板的,真实的。

但是就正则表达式而言,额外的编译步骤不太可能是造成认知负担的主要原因。实际上,在复杂的模式上,您甚至可以通过将声明与随后在其上调用的任何regex方法分开来获得清晰度。

我倾向于首先在Regex101之类的网站中甚至在一个单独的最小测试脚本中调整复杂的模式,然后将它们引入我的代码中,因此将声明与使用分开也是适合我的工作流程的。


-1

我想激发预编译在概念上和“文学上”(如“文学编程”中)都是有利的。看一下下面的代码片段:

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非常不寻常,甚至值得商bat。但是,在更紧密地匹配python最常用方式的示例中,为了进行单个匹配,我们必须实例化一个对象,执行三个实例字典查找,并执行三个函数调用;另外,我们可能会进入re当使用100多个正则表达式时缓存问题。同样,正则表达式隐藏在方法体内,大多数情况下并不是一个好主意。

是否说措施的每个子集-有针对性的别名导入语句;适用的别名方法;减少函数调用和对象字典查找-可以帮助减少计算和概念上的复杂性。


2
WTF。您不仅要提出一个古老的,已回答的问题。您的代码在很多层面上都是非惯用的,而且是错误的 -(ab)使用类作为模块足够的命名空间,大写类名等。有关更好的实现,请参见pastebin.com/iTAXAWen。更不用说您使用的正则表达式也坏了。总体而言,-1

2
有罪。这是一个老问题,但我不介意在慢速对话中排在第100位。该问题尚未结束。我确实警告过我的代码可能会有些恶心。我认为,如果您可以将其视为仅仅是python中可行的演示,例如:如果我们将所有内容,我们相信的所有内容作为可选内容,然后以任何方式进行修改,那么这些东西看起来像什么,我们可以得到?我相信您可以分辨出此解决方案的优缺点,并且可以更清楚地进行投诉。否则我必须结束你会有不适要求多一点依靠比PEP008
流量

2
不,这与PEP8无关。那只是命名约定,我永远不会为不遵循这些约定而投票。我不赞成您,因为您显示的代码编写得很差。它无缘无故地违反了约定和习惯用法,是过时优化的化身:您必须从所有其他代码中优化生存日光,以使其成为瓶颈,即使如此,我提供的第三次重写也更短,更多。惯用的,并且根据您的推理也是如此(属性访问次数相同)。

“写得不好”-到底为什么?“违反惯例和习惯用语”-我警告过您。“没有理由”-是的,我确实有一个理由:简化复杂性没有目的的地方;“过早优化的化身”-我非常喜欢一种在可读性和效率之间寻求平衡的编程风格;OP要求引起“使用re.compile的好处”的启发,我理解这是一个关于效率的问题。“(将类用作命名空间)”-滥用您的话。那里有课程,因此您有一个“自我”参考点。我尝试为此目的使用模块,类工作得更好。
流动

“大写字母的班级名称”,“不,这与PEP8无关”-您显然非常生气,甚至不知道首先要吵什么。“ WTF”,“ ” ---看看你有多激动?请增加客观性,减少泡沫。

-5

我的理解是,这两个示例实际上是等效的。唯一的区别是,在第一个实例中,您可以在其他地方重用已编译的正则表达式,而无需再次对其进行编译。

这是给您的参考:http : //diveintopython3.ep.io/refactoring.html

用字符串“ M”调用已编译模式对象的搜索功能与使用正则表达式和字符串“ M”调用re.search的操作相同。只有很多,更快。(实际上,re.search函数只是编译正则表达式并为您调用结果模式对象的search方法。)


1
我没有投票给你,但是从技术上讲这是错误的:Python还是不会重新编译
Triptych
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.