632 Stimmen

Lohnt es sich, Pythons re.compile zu verwenden?

Gibt es einen Vorteil bei der Verwendung von Kompilieren für reguläre Ausdrücke in Python?

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

gegen

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

9 Stimmen

Mit Ausnahme der Tatsache, dass in 2.6 re.sub lässt sich nicht auf ein Fahnenargument ein...

84 Stimmen

Ich bin gerade auf einen Fall gestoßen, bei dem die Verwendung von re.compile führte zu einer 10-50fachen Verbesserung. Die Moral ist, dass wenn Sie haben sehr viele Regexe (mehr als MAXCACHE = 100) y Sie verwenden sie jeweils sehr oft (und zwar mit mehr als MAXCACHE Regexen dazwischen, so dass jede einzelne aus dem Cache gelöscht wird: wenn Sie also dieselbe Regex sehr oft verwenden und dann zur nächsten übergehen, zählt das nicht), dann wäre es sicherlich hilfreich, sie zusammenzustellen. Ansonsten macht es keinen Unterschied.

19 Stimmen

Eine kleine Anmerkung ist, dass für Strings, die keinen Regex benötigen, die in string substring test ist VIEL schneller: >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

20voto

ShreevatsaR Punkte 37033

Hier ist ein Beispiel für die Verwendung von re.compile ist über 50 Mal schneller, als angefordert .

Der Punkt ist genau derselbe wie in meinem obigen Kommentar, nämlich die Verwendung von re.compile kann ein erheblicher Vorteil sein, wenn Ihre Nutzung so beschaffen ist, dass der Kompilierungscache nicht viel bringt. Dies ist zumindest in einem bestimmten Fall der Fall (den ich in der Praxis erlebt habe), nämlich wenn alle der folgenden Punkte zutreffen:

  • Sie haben eine Menge Regex-Muster (mehr als re._MAXCACHE , dessen Standard ist derzeit 512), und
  • Sie verwenden diese Regexe häufig, und
  • Sie aufeinanderfolgende Verwendungen desselben Musters mit einem Abstand von mehr als re._MAXCACHE andere Regexe dazwischen, so dass jede Regex zwischen aufeinanderfolgenden Verwendungen aus dem Cache gespült wird.

    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}')

Beispielausgabe auf meinem Laptop (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

Ich habe mich nicht um die timeit weil der Unterschied so groß ist, aber ich erhalte jedes Mal qualitativ ähnliche Zahlen. Beachten Sie, dass auch ohne re.compile war es gar nicht so schlimm, dieselbe Regex mehrfach zu verwenden und zur nächsten zu wechseln (nur etwa 2-mal so langsam wie mit re.compile ), aber in der anderen Reihenfolge (Schleifenbildung durch viele Regexe) ist es erwartungsgemäß deutlich schlechter. Auch das Erhöhen der Cache-Größe funktioniert: Setzen Sie einfach re._MAXCACHE = len(patterns) en setup() oben (natürlich empfehle ich nicht, so etwas in der Produktion zu tun, da Namen mit Unterstrichen üblicherweise "privat" sind) senkt die ~23 Sekunden wieder auf ~0,7 Sekunden, was auch unserem Verständnis entspricht.

17voto

John Pang Punkte 2185

Ich stimme mit Honest Abe überein, dass die match(...) in den genannten Beispielen sind unterschiedlich. Sie sind keine Eins-zu-eins-Vergleiche und die Ergebnisse sind daher unterschiedlich. Um meine Antwort zu vereinfachen, verwende ich A, B, C, D für die fraglichen Funktionen. Oh ja, wir haben es mit 4 Funktionen in re.py anstelle von 3.

Ausführen dieses Code-Stücks:

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

ist dasselbe wie die Ausführung dieses Codes:

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

Denn, wenn man in die Quelle schaut re.py , (A + B) bedeutet:

h = re._compile('hello')                  # (D)
h.match('hello world')

und (C) ist tatsächlich:

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

Also ist (C) nicht dasselbe wie (B). Vielmehr ruft (C) (B) auf, nachdem es (D) aufgerufen hat, das wiederum von (A) aufgerufen wird. Mit anderen Worten, (C) = (A) + (B) . Daher hat der Vergleich von (A + B) innerhalb einer Schleife das gleiche Ergebnis wie (C) innerhalb einer Schleife.

Georges regexTest.py hat dies für uns bewiesen.

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

Jeder ist daran interessiert, wie man das Ergebnis von 2,323 Sekunden erreichen kann. Um sicherzustellen, dass compile(...) nur einmal aufgerufen wird, müssen wir das kompilierte Regex-Objekt im Speicher ablegen. Wenn wir eine Klasse verwenden, können wir das Objekt speichern und jedes Mal wiederverwenden, wenn unsere Funktion aufgerufen wird.

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

Wenn wir die Klasse nicht benutzen (was ich heute beantrage), habe ich keinen Kommentar. Ich lerne immer noch, wie man eine globale Variable in Python verwendet, und ich weiß, dass eine globale Variable eine schlechte Sache ist.

Ein weiterer Punkt: Ich glaube, dass die Verwendung von (A) + (B) Ansatz die Oberhand hat. Hier sind einige Fakten, die ich beobachtet habe (bitte korrigieren Sie mich, wenn ich falsch liege):

  1. Ruft A einmal auf, so wird eine Suche in der _cache gefolgt von einem sre_compile.compile() um ein Regex-Objekt zu erstellen. Ruft man A zweimal auf, werden zwei Suchen und eine Kompilierung durchgeführt (da das Regex-Objekt zwischengespeichert wird).

  2. Wenn die _cache dazwischen gespült wird, dann wird das Regex-Objekt aus dem Speicher freigegeben und Python muss erneut kompilieren. (Jemand behauptet, dass Python nicht neu kompiliert.)

  3. Wenn wir das Regex-Objekt mit (A) beibehalten, wird das Regex-Objekt trotzdem in den _cache gelangen und irgendwie gespült werden. Aber unser Code behält eine Referenz darauf und das Regex-Objekt wird nicht aus dem Speicher freigegeben. Das bedeutet, dass Python nicht neu kompiliert werden muss.

  4. Der Unterschied von 2 Sekunden zwischen der kompilierten Schleife in Georges Test und der kompilierten Schleife ist hauptsächlich die Zeit, die zum Erstellen des Schlüssels und zum Durchsuchen des _cache erforderlich ist. Es bedeutet nicht die Kompilierzeit der Regex.

  5. Georges reallycompile-Test zeigt, was passiert, wenn die Kompilierung wirklich jedes Mal wiederholt wird: Sie wird 100x langsamer (er hat die Schleife von 1.000.000 auf 10.000 reduziert).

Hier sind die einzigen Fälle, in denen (A + B) besser ist als (C):

  1. Wenn wir einen Verweis auf das Regex-Objekt innerhalb einer Klasse zwischenspeichern können.
  2. Wenn wir (B) wiederholt aufrufen müssen (innerhalb einer Schleife oder mehrfach), müssen wir den Verweis auf das Regex-Objekt außerhalb der Schleife zwischenspeichern.

Fall, dass (C) gut genug ist:

  1. Wir können eine Referenz nicht zwischenspeichern.
  2. Wir benutzen es nur ab und zu.
  3. Insgesamt haben wir nicht zu viele Regex (gehen Sie davon aus, dass die kompilierten Regex nie gespült werden)

Nur eine Zusammenfassung, hier sind die A B C:

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

Vielen Dank für die Lektüre.

12voto

Raymond Hettinger Punkte 197261

Meistens gibt es kaum einen Unterschied, ob Sie die re.kompilieren oder nicht. Intern sind alle Funktionen in Form eines Kompilierschritts implementiert:

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)

Darüber hinaus umgeht re.compile() die zusätzliche Umleitung und Zwischenspeicherlogik:

_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

Zusätzlich zu dem kleinen Geschwindigkeitsvorteil durch die Verwendung von re.kompilieren Auch die Lesbarkeit, die sich aus der Benennung potenziell komplexer Musterspezifikationen und ihrer Trennung von der angewandten Geschäftslogik ergibt, wird geschätzt:

#### 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()

Ein weiterer Befragter glaubte fälschlicherweise, dass pyc Dateien direkt kompilierte Muster gespeichert; in Wirklichkeit werden sie jedoch jedes Mal neu erstellt, wenn der PYC geladen wird:

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

Die obige Disassemblierung stammt aus der PYC-Datei für eine tmp.py enthalten:

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

7voto

cyneo Punkte 756

Ein weiterer Vorteil der Verwendung von re.compile() besteht darin, dass ich mit re.VERBOSE Kommentare zu meinen Regex-Mustern hinzufügen kann

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

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

Obwohl dies keinen Einfluss auf die Geschwindigkeit der Ausführung Ihres Codes hat, mache ich es gerne so, da es Teil meiner Gewohnheit ist, Kommentare zu schreiben. Ich verbringe nur ungern Zeit damit, mich an die Logik meines Codes zu erinnern, wenn ich 2 Monate später Änderungen vornehmen möchte.

6voto

Chris Wu Punkte 157

Gemäß der Python Dokumentation :

Die Reihenfolge

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

ist gleichbedeutend mit

result = re.match(pattern, string)

sondern mit re.compile() und das Speichern des resultierenden regulären Ausdrucks zur Wiederverwendung ist effizienter, wenn der Ausdruck mehrmals in einem einzigen Programm verwendet wird.

Daraus schließe ich: Wenn Sie dasselbe Muster für viele verschiedene Texte verwenden wollen, sollten Sie es besser vorkompilieren.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X