【问题标题】:Is it worth using Python's re.compile?值得使用 Python 的 re.compile 吗?
【发布时间】:2010-10-01 21:55:38
【问题描述】:

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

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

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

【问题讨论】:

  • 其他的事实是,在 2.6 re.sub 中不会接受标志参数...
  • 我刚遇到一个案例,使用 re.compile 可以提高 10-50 倍。寓意是 if 你有很多正则表达式(超过 MAXCACHE = 100)并且你每次都使用它们很多次(并且由超过 MAXCACHE 正则表达式分隔之间,以便每个都从缓存中刷新:所以多次使用同一个然后继续下一个不算),那么它肯定有助于编译它们.否则,它没有任何区别。
  • 需要注意的一点是,对于不需要正则表达式的字符串,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
  • @ShreevatsaR 有趣!您能否通过一个显示 10 倍至 50 倍改进的示例发布答案?此处给出的大多数答案实际上在某些精确情况下显示了 3 倍的改进,而在其他情况下几乎没有改进。
  • @Basj 完成,发布an answer。在 2013 年 12 月,我没有费心去挖掘我使用 Python 的目的,但我尝试的第一个简单的事情显示了相同的行为。

标签: python regex


【解决方案1】:

在使用第二个版本时,正则表达式在使用之前被编译。如果你要多次执行它,最好先编译它。如果不是每次匹配一次都编译就可以了。

【讨论】:

    【解决方案2】:

    我的理解是这两个示例实际上是等效的。唯一的区别是,在第一种情况下,您可以在其他地方重用已编译的正则表达式,而不会导致再次编译。

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

    使用字符串“M”调用编译模式对象的搜索函数与使用正则表达式和字符串“M”调用 re.search 完成相同的事情。只是快得多。 (实际上,re.search 函数只是简单地编译正则表达式并为您调用生成的模式对象的搜索方法。)

    【讨论】:

    • 我没有对你投反对票,但从技术上讲这是错误的:Python 无论如何都不会重新编译
    【解决方案3】:

    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
    

    所以,如果您要经常使用 same 正则表达式,那么使用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(...),即似乎没有对已编译表示进行幕后缓存发生。

    【讨论】:

    • 您的方法存在重大问题,因为设置参数不包括在计时中。因此,您已经从第二个示例中删除了编译时间,并在第一个示例中对其进行了平均。这并不意味着第一个示例每次都会编译。
    • 是的,我同意这不是两个案例的公平比较。
    • 我明白你的意思,但这不正是在多次使用正则表达式的实际应用程序中会发生的情况吗?
    • @Triptych, @Kiv:编译正则表达式与使用分开的全部意义以最小化编译;从时序中删除它正是 dF 应该做的,因为它最准确地代表了现实世界的使用。编译时间与 timeit.py 在这里的计时方式特别无关;它运行了几次,只报告最短的一次,此时已编译的正则表达式被缓存。您在这里看到的额外成本不是编译正则表达式的成本,而是在已编译的正则表达式缓存(字典)中查找它的成本。
    • @Triptych 是否应该将import re 移出设置?这完全取决于您要测量的位置。如果我多次运行 python 脚本,它将会有import re 时间命中。在比较两者时,将两条线分开以进行计时很重要。是的,正如你所说,这是你有时间的时候。比较表明,要么您将时间命中一次并通过编译重复较短的时间命中,要么您每次都假定缓存在调用之间被清除,正如已经指出的那样,这可能会发生。添加h=re.compile('hello') 的时间有助于澄清。
    【解决方案4】:

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

    编辑: 在快速浏览了实际的 Python 2.5 库代码后,我发现无论何时使用它们(包括对re.match() 的调用),Python 都会在内部编译并缓存正则表达式,因此您实际上只是在正则表达式被编译时进行更改,并且应该根本不会节省太多时间 - 只是检查缓存所需的时间(内部 dict 类型的键查找)。

    来自模块 re.py(cmets 是我的):

    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
    

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

    【讨论】:

    • 您的结论与您的答案不一致。如果正则表达式是自动编译和存储的,则在大多数情况下无需手动进行。
    • J. F. Sebastian,它向程序员发出了一个信号,即所讨论的正则表达式将被大量使用,而不是一次性使用。
    • 不仅如此,我想说的是,如果您不想在应用程序的某些性能关键部分遭受编译和缓存命中,最好在提交之前编译它们应用程序的非关键部分。
    • 如果您多次重复使用同一个正则表达式,我看到了使用编译正则表达式的主要优势,从而减少了拼写错误的可能性。如果您只调用一次,那么未编译的可读性更高。
    • 因此,主要区别在于当您使用许多不同的正则表达式(超过 _MAXCACHE)时,其中一些只使用一次,而另一些则很多次......那么保持编译的表达式很重要对于那些使用较多的缓存,当缓存已满时不会从缓存中清除。
    【解决方案5】:

    这是个好问题。你经常看到人们无缘无故地使用 re.compile。它降低了可读性。但是肯定有很多时候需要预编译表达式。就像你在循环中重复使用它一样。

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

    【讨论】:

    • 从我的简短浏览中可以看出,Python in a Nutshell 没有提到不使用 re.compile(),这让我很好奇。跨度>
    • 正则表达式对象在上下文中增加了一个对象。正如我所说,在许多情况下 re.compile() 都有它的位置。 OP给出的例子不是其中之一。
    【解决方案6】:

    对我来说,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 会产生更多而不是更少可读性的代码。
    • 有时情况正好相反——例如如果您在一个地方定义正则表达式并在另一个遥远的地方使用它的匹配组。
    • @KenWilliams 不一定,即使在远离原始定义的情况下,用于特定目的的命名良好的正则表达式也应该清楚。例如us_phone_numbersocial_security_number 等。
    • @BrianM.Sheldon 很好地命名正则表达式并不能真正帮助您了解它的各种捕获组代表什么。
    【解决方案7】:

    有趣的是,编译对我来说确实更有效(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 行注释掉,编译的正则表达式的速度是原来的两倍

    【讨论】:

    • 与 dF 的性能比较相同的问题。除非您将编译语句本身的性能成本包括在内,否则这并不公平。
    • 卡尔,我不同意。编译只执行一次,而匹配循环执行一百万次
    • @eliben:我同意卡尔迈耶的观点。在这两种情况下都会进行编译。 Triptych 提到涉及到缓存,因此在最佳情况下(重新保留在缓存中),两种方法都是 O(n+1),尽管当您不显式使用 re.compile 时 +1 部分是隐藏的。跨度>
    • 不要编写自己的基准测试代码。学习使用 timeit.py,它包含在标准发行版中。
    • 您有多少时间在 for 循环中重新创建模式字符串。这种开销不是微不足道的。
    【解决方案8】:

    总的来说,我发现在编译模式时使用标志(至少更容易记住如何使用)比使用内联标志更容易,例如 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 的第三个参数。
    【解决方案9】:

    (几个月后)在 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 ...

    【讨论】:

      【解决方案10】:

      我在偶然发现这里的讨论之前运行了这个测试。但是,运行它后,我想我至少会发布我的结果。

      我盗用了 Jeff Friedl 的“掌握正则表达式”中的示例并将其混为一谈。这是在运行 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
      

      【讨论】:

        【解决方案11】:

        我自己也试过了。对于从字符串中解析数字并将其求和的简单情况,使用编译的正则表达式对象的速度大约是使用 re 方法的两倍。

        正如其他人所指出的,re 方法(包括re.compile)在先前编译的表达式的缓存中查找正则表达式字符串。因此,在正常情况下,使用re 方法的额外成本只是缓存查找的成本。

        但是,对code 的检查表明,缓存限制为 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分钟。我看不懂上面的答案...
        【解决方案12】:

        我想强调预编译在概念上和“文学”(如“文学编程”中)都是有利的。看看这段代码sn-p:

        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 的主要使用方式更接近的示例中,为了进行单次匹配,我们必须实例化一个对象,进行三个实例字典查找,并执行三个函数调用;此外,当使用超过 100 个正则表达式时,我们可能会遇到re 缓存问题。此外,正则表达式隐藏在方法体中,这在大多数情况下并不是一个好主意。

        可以说每个度量子集---有针对性的别名导入语句;适用的别名方法;减少函数调用和对象字典查找——有助于降低计算和概念复杂性。

        【讨论】:

          【解决方案13】:

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

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

          所以,用这个简单的例子编译似乎更快,即使你只匹配一次

          【讨论】:

          • 这是哪个版本的 Python?
          • 没关系,关键是在你将运行代码的环境中尝试基准测试
          • 对我来说,1000 次或更多循环的性能几乎完全相同。对于 1-100 个循环,编译后的版本更快。 (在 python 2.7 和 3.4 上)。
          • 在我的 Python 2.7.3 设置中几乎没有任何区别。有时 compile 更快,有时 ist 更慢。差异始终小于 5%,因此我将差异视为测量不确定性,因为该设备只有一个 CPU。
          • 在 Python 3.4.3 中看到两个单独的运行:使用已编译甚至比未编译慢。
          【解决方案14】:

          使用给定的例子:

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

          上例中的match方法与下例不同:

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

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

          正则表达式对象有自己的match 方法,带有可选的posendpos 参数:

          regex.match(string[, pos[, endpos]])

          位置

          可选的第二个参数 pos 给出字符串中的索引,其中 搜索开始;它默认为 0。这并不完全 相当于对字符串进行切片; '^' 模式字符匹配于 字符串的真正开头和紧接在 a 之后的位置 换行符,但不一定在要搜索的索引处 开始。

          结束位置

          可选参数 endpos 限制字符串的长度 搜索;就好像字符串的长度是 endpos 个字符,所以 只会搜索从 posendpos - 1 的字符 匹配。如果 endpos 小于 pos,则找不到匹配项;否则, 如果 rx 是编译的正则表达式对象,rx.search(string, 0, 50) 等价于 rx.search(string[:50], 0)

          正则表达式对象的 searchfindallfinditer 方法也支持这些参数。

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

          match object 具有补充这些参数的属性:

          ma​​tch.pos

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

          ma​​tch.endpos

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


          regex object 有两个独特的、可能有用的属性:

          regex.groups

          模式中的捕获组数。

          regex.groupindex

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


          最后,match object 有这个属性:

          ma​​tch.re

          match() 或 search() 方法的正则表达式对象 产生了这个匹配实例。

          【讨论】:

            【解决方案15】:

            抛开性能差异不谈,使用 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 帮助我断言我的假设。

            【讨论】:

              【解决方案16】:

              我同意 Honest Abe 的观点,即给定示例中的 match(...) 是不同的。它们不是一对一的比较,因此结果各不相同。为了简化我的回答,我对那些有问题的函数使用 A、B、C、D。哦,是的,我们正在处理 re.py 中的 4 个函数而不是 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) 具有相同的结果。

              George 的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) 方法有优势。以下是我观察到的一些事实(如果我错了,请纠正我):

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

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

              3. 如果我们使用 (A) 保留 regex 对象,regex 对象仍然会进入 _cache 并以某种方式被刷新。但是我们的代码在它上面保留了一个引用,并且正则表达式对象不会从内存中释放出来。那些,Python 不需要再次编译。

              4. George 的测试编译循环与编译的 2 秒差异主要是构建 key 和搜索 _cache 所需的时间。并不代表正则表达式的编译时间。

              5. George 的 realcompile 测试展示了如果它真的每次都重新编译会发生什么:它会慢 100 倍(他将循环从 1,000,000 减少到 10,000)。

              只有以下几种情况 (A + B) 优于 (C):

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

              (C) 足够好的情况:

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

              只是回顾一下,这里是 A B C:

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

              感谢阅读。

              【讨论】:

                【解决方案17】:

                使用 re.compile() 还有一个额外的好处,就是使用 re.VERBOSE 将 cmets 添加到我的正则表达式模式中

                pattern = '''
                hello[ ]world    # Some info on my pattern logic. [ ] to recognize space
                '''
                
                re.search(pattern, 'hello world', re.VERBOSE)
                

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

                【讨论】:

                • 我已经编辑了你的答案。我认为提及re.VERBOSE 是值得的,它确实增加了其他答案似乎遗漏的东西。但是,用“我在这里发帖,因为我还不能发表评论”来引导你的回答肯定会被删除。请不要将答案框用于答案以外的任何内容。您距离能够在任何地方发表评论(50 代表)只有一两个好的答案,所以请耐心等待。当您知道不应该将 cmets 放入答案框中时,不会让您更快地到达那里。它会让你投反对票并删除答案。
                【解决方案18】:

                这个答案可能会迟到,但这是一个有趣的发现。如果您计划多次使用正则表达式,使用 compile 可以真正节省您的时间(这也在文档中提到)。下面你可以看到,当直接调用 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】:

                  我在运行已编译的 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 中的示例模拟了在导入时创建已编译的正则表达式对象,而不是在需要匹配时“即时”创建。

                  【讨论】:

                    【解决方案20】:

                    大多数情况下,您是否使用 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
                    

                    上述反汇编来自tmp.py的PYC文件,其中包含:

                    import re
                    lc_vowels = re.compile(r'[aeiou]{2,5}')
                    

                    【讨论】:

                    • " 中的 def search(pattern, string, flags=0):" 是错字吗?
                    • 请注意,如果pattern 已经是编译模式,缓存开销会变得很大:散列SRE_Pattern 的开销很大,并且模式永远不会写入缓存,因此每次查找都会失败KeyError.
                    【解决方案21】:

                    我真的尊重以上所有答案。在我看来 是的!当然值得使用 re.compile 而不是编译正则表达式,一次又一次,每次。

                    使用 re.compile 使您的代码更加动态,因为您可以调用已经编译的正则表达式,而不是一次又一次地编译。在以下情况下,这对您有好处:

                    1. 处理器工作
                    2. 时间复杂度。
                    3. 使正则表达式通用。(可用于查找、搜索、匹配)
                    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)
                    

                    您也可以将其用于:匹配和替换

                    【讨论】:

                      【解决方案22】:

                      除了表演。

                      使用compile 可以帮助我区分
                      1 的概念。模块(重新)
                      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'          : ['^', '\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',]
                      }
                      

                      【讨论】:

                        【解决方案23】:

                        根据 Python documentation:

                        顺序

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

                        等价于

                        result = re.match(pattern, string)
                        

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

                        所以我的结论是,如果你要为许多不同的文本匹配相同的模式,你最好预编译它。

                        【讨论】:

                          【解决方案24】:

                          作为替代答案,正如我看到之前没有提到的那样,我将继续引用Python 3 docs

                          您应该使用这些模块级函数,还是应该获取模式并自己调用它的方法?如果您在循环中访问正则表达式,预编译它将节省一些函数调用。在循环之外,由于内部缓存,没有太大区别。

                          【讨论】:

                            【解决方案25】:

                            易读性/认知负荷偏好

                            对我来说,主要收获是我只需要记住并阅读复​​杂的正则表达式 API 语法的 一个 形式 - &lt;compiled_pattern&gt;.method(xxx) 形式而不是 and re.func(&lt;pattern&gt;, xxx) 表单。

                            re.compile(&lt;pattern&gt;) 是一个额外的样板,是的。

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

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

                            【讨论】:

                              【解决方案26】:

                              这是一个示例,使用 re.compile 的速度比 requested 快 50 倍以上。

                              这一点与我在上面的评论中所说的相同,即当您的使用无法从编译缓存中受益时,使用re.compile 可能是一个显着的优势。这至少发生在一种特殊情况下(我在实践中遇到过),即当以下所有情况都为真时:

                              • 您有很多正则表达式模式(超过 re._MAXCACHE,其 default 目前为 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,多次使用相同的正则表达式并继续下一个也不是那么糟糕(仅比re.compile 慢大约 2 倍),但以另一种顺序(循环通过许多正则表达式),正如预期的那样,情况明显更糟。此外,增加缓存大小也可以:只需在上面的setup() 中设置re._MAXCACHE = len(patterns)(当然我不建议在生产环境中这样做,因为带有下划线的名称通常是“私有的”)将~23 秒降低到~0.7 秒,也符合我们的理解。

                              【讨论】:

                              • PS:如果我在整个代码中使用 only 3 个正则表达式模式,每个模式都使用(没有任何特定顺序)数百次,正则表达式缓存将保留预编译的自动正则表达式,对吗?
                              • @Basj 我想你可以试试看 :) 但我敢肯定,答案是肯定的:在这种情况下,AFAICT 的唯一额外成本就是 looking up the pattern in the cache .还要注意缓存是全局的(模块级),所以原则上你可以有一些依赖库在你的之间进行正则表达式搜索,所以很难完全确信你的程序只使用了 3 个(或任何数量的)正则表达式模式,但否则会很奇怪:)
                              【解决方案27】:

                              虽然这两种方法在速度方面相当,但您应该知道仍然存在一些可忽略不计的时间差异,如果您要处理数百万次迭代,您可能会担心这些时间差异。 p>

                              以下速度测试:

                              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 倍左右。

                              documentation中所述:

                              如果您在循环中访问正则表达式,预编译它将节省一些函数调用。在循环之外,由于内部缓存,没有太大区别。

                              【讨论】:

                                猜你喜欢
                                • 1970-01-01
                                • 1970-01-01
                                • 2013-12-21
                                • 2018-04-26
                                • 2011-08-03
                                • 1970-01-01
                                • 1970-01-01
                                • 2011-10-08
                                相关资源
                                最近更新 更多