簡體   English   中英

值得使用 Python 的 re.compile 嗎?

[英]Is it worth using Python's re.compile?

在 Python 中對正則表達式使用編譯有什么好處嗎?

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

對比

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

我有很多運行編譯的正則表達式 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

我仍然經常預編譯正則表達式,但只是將它們綁定到一個漂亮的、可重用的名稱,而不是為了任何預期的性能提升。

對我來說, 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並不會真正失去太多的清晰度/直接性。

更新:

在 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

重新編譯:

~$ 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 = ", 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 萬次。

我同意 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 兩次,它將執行兩次搜索和一次編譯(因為正則表達式對象已緩存)。

  2. 如果_cache在兩者之間被刷新,那么正則表達式對象將從內存中釋放並且 Python 需要再次編譯。 (有人建議 Python 不會重新編譯。)

  3. 如果我們使用 (A) 保留 regex 對象,regex 對象仍會進入 _cache 並以某種方式刷新。 但是我們的代碼保留了對它的引用,並且正則表達式對象不會從內存中釋放。 那些,Python 不需要再次編譯。

  4. George 的測試編譯循環和編譯循環的 2 秒差異主要是構建密鑰和搜索 _cache 所需的時間。 這並不意味着正則表達式的編譯時間。

  5. George 的reallycompile 測試顯示了如果每次都重新編譯會發生什么:它會慢100 倍(他將循環從1,000,000 減少到10,000)。

以下是 (A + B) 優於 (C) 的唯一情況:

  1. 如果我們可以在類中緩存正則表達式對象的引用。
  2. 如果我們需要重復調​​用 (B)(在循環內或多次),我們必須在循環外緩存對正則表達式對象的引用。

(C) 足夠好的情況:

  1. 我們無法緩存引用。
  2. 我們只偶爾使用它一次。
  3. 總的來說,我們沒有太多的正則表達式(假設編譯的一個永遠不會被刷新)

簡單回顧一下,這里是ABC:

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

謝謝閱讀。

這是一個示例,根據要求,使用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 ,多次使用相同的正則表達式並繼續使用下一個也不是那么糟糕(只有re.compile慢約 2 倍),但以另一種順序(循環通過許多正則表達式),正如預期的那樣,情況明顯更糟。 此外,增加緩存大小也有效:只需在上面的setup()設置re._MAXCACHE = len(patterns) (當然我不建議在生產中做這樣的事情,因為帶有下划線的名稱通常是“私有的”)丟棄 ~ 23 秒回落到~0.7 秒,這也符合我們的理解。

大多數情況下,是否使用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 文件, tmp.py包含:

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

使用 re.compile() 有一個額外的好處,即使用 re.VERBOSE 在我的正則表達式模式中添加注釋

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

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

雖然這不會影響運行代碼的速度,但我喜歡這樣做,因為這是我評論習慣的一部分。 當我想進行修改時,我非常不喜歡花時間試圖記住我的代碼背后的邏輯 2 個月后。

根據 Python文檔

序列

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

相當於

result = re.match(pattern, string)

但是當表達式將在單個程序中多次使用時,使用re.compile()並保存生成的正則表達式對象以供重用會更有效。

所以我的結論是,如果你要為許多不同的文本匹配相同的模式,你最好預編譯它。

使用給定的例子:

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字符一樣長,所以只有從posendpos - 1的字符會被搜索匹配。 如果endpos小於pos ,則找不到匹配項; 否則,如果rx是編譯后的正則表達式對象, rx.search(string, 0, 50)等價於rx.search(string[:50], 0)

正則表達式對象的searchfindallfinditer方法也支持這些參數。

如您所見, re.match(pattern, string, flags=0)不支持它們,
它的searchfindallfinditer 也不對應。

匹配對象具有補充這些參數的屬性:

匹配位置

傳遞給正則表達式對象的 search() 或 match() 方法的 pos 值。 這是 RE 引擎開始尋找匹配的字符串的索引。

匹配.endpos

傳遞給正則表達式對象的 search() 或 match() 方法的 endpos 的值。 這是 RE 引擎不會超過的字符串索引。


正則表達式對象有兩個獨特的、可能有用的屬性:

正則表達式組

模式中捕獲組的數量。

正則表達式.groupindex

將 (?P) 定義的任何符號組名稱映射到組編號的字典。 如果模式中沒有使用符號組,則字典為空。


最后, 匹配對象具有以下屬性:

匹配文件

其 match() 或 search() 方法生成此匹配實例的正則表達式對象。

一般來說,我發現在編譯模式時使用標志(至少更容易記住如何使用)比使用內聯標志更容易,例如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.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 幫助我斷言我的假設。

在偶然發現這里的討論之前,我運行了這個測試。 但是,運行它后,我想我至少會發布我的結果。

我竊取了 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

這個答案可能會遲到,但這是一個有趣的發現。 如果您計划多次使用正則表達式,則使用 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

有趣的是,編譯確實證明對我來說更有效(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行,編譯后的正則表達式速度是原來的兩倍

除了表演。

使用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',]
}

作為替代答案,我看到之前沒有提到過,我將繼續引用Python 3 文檔

您應該使用這些模塊級函數,還是應該自己獲取模式並調用其方法? 如果您在循環中訪問正則表達式,預編譯它將節省一些函數調用。 在循環之外,由於內部緩存,沒有太大區別。

我真的很尊重上述所有答案。 在我看來是的! 肯定值得使用 re.compile 而不是每次都一次又一次地編譯正則表達式。

使用re.compile使您的代碼更加動態,因為您可以調用已編譯的正則表達式,而不是一遍又一遍地編譯。 這件事在以下情況下對您有益:

  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)

同樣,您可以將其用於:匹配和替換

(幾個月后)很容易在 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 ...

我有很多運行編譯的正則表達式 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 中的示例模擬在導入時創建編譯的正則表達式對象一次,而不是在匹配需要時“即時”創建。

這是一個很好的問題。 你經常看到人們無緣無故地使用 re.compile。 它降低了可讀性。 但是肯定有很多時候需要預編譯表達式。 就像當你在循環中重復使用它或類似的時候。

這就像關於編程的一切(實際上是生活中的一切)。 運用常識。

正則表達式在使用第二個版本時在使用前編譯。 如果您要多次執行它,最好先編譯它。 如果每次匹配一次都沒有編譯,那很好。

易讀性/認知負荷偏好

對我來說,主要的收獲是我只需要記住和閱讀復雜的正則表達式 API 語法的一種形式 - <compiled_pattern>.method(xxx)形式而不是那種形式re.func(<pattern>, xxx)形式。

re.compile(<pattern>)是一些額外的樣板,真的。

但是就正則表達式而言,額外的編譯步驟不太可能是造成認知負荷的主要原因。 事實上,對於復雜的模式,您甚至可以通過將聲明與您隨后對其調用的任何正則表達式方法分開來獲得清晰的認識。

我傾向於首先在像 Regex101 這樣的網站中調整復雜的模式,甚至在一個單獨的最小測試腳本中,然后將它們引入我的代碼中,因此將聲明與其使用分開也適合我的工作流程。

盡管這兩種方法在速度方面具有可比性,但您應該知道,如果您正在處理數百萬次迭代,仍然存在一些可以忽略不計的時間差異,這可能是您關注的問題。

以下速度測試:

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

文檔中所述:

如果您在循環中訪問正則表達式,預編譯它將節省一些函數調用。 在循環之外,由於內部緩存,沒有太大區別。

在 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

我想說明預編譯在概念上和“文學上”(如“文學編程”中)都是有利的。 看看這個代碼片段:

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緩存問題。 此外,正則表達式隱藏在方法體中,這在大多數情況下不是一個好主意。

可以說,每個措施的子集——有針對性的、別名的導入語句; 適用的別名方法; 減少函數調用和對象字典查找---可以幫助減少計算和概念的復雜性。

我的理解是這兩個例子實際上是等價的。 唯一的區別是,在第一個中,您可以在其他地方重用已編譯的正則表達式,而不會導致再次編譯。

這是給你的參考: http : //diveintopython3.ep.io/refactoring.html

使用字符串 'M' 調用已編譯模式對象的搜索函數與使用正則表達式和字符串 'M' 調用 re.search 完成相同的事情。 只是快得多。 (實際上,re.search 函數只是編譯正則表達式並為您調用生成的模式對象的搜索方法。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM