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')
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')
Ich habe viele Erfahrungen damit gemacht, eine kompilierte Regex 1000-mal im Vergleich zur On-the-Fly-Kompilierung auszuführen, und ich habe keinen erkennbaren Unterschied festgestellt. Offensichtlich ist dies anekdotisch, und sicherlich nicht ein großes Argument gegen Kompilieren, aber ich habe festgestellt, dass der Unterschied vernachlässigbar ist.
EDIT: Nach einem kurzen Blick auf den aktuellen Python 2.5 Bibliothekscode sehe ich, dass Python intern Regexe kompiliert UND zwischenspeichert, wann immer man sie verwendet (einschließlich Aufrufen von re.match()
), so dass Sie wirklich nur ändern, WANN die Regex kompiliert wird, und sollte nicht viel Zeit überhaupt sparen - nur die Zeit, die es dauert, um den Cache zu überprüfen (ein Schlüssel Lookup auf eine interne dict
Typ).
Aus dem Modul re.py (Kommentare sind von mir):
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
Ich kompiliere immer noch oft reguläre Ausdrücke vor, aber nur, um sie an einen schönen, wiederverwendbaren Namen zu binden, nicht um einen Leistungsgewinn zu erwarten.
Für mich ist der größte Vorteil der re.compile
ist die Möglichkeit, die Definition der Regex von ihrer Verwendung zu trennen.
Selbst ein einfacher Ausdruck wie 0|[1-9][0-9]*
(Ganzzahl zur Basis 10 ohne führende Nullen) kann so komplex sein, dass man sie lieber nicht noch einmal eingeben und überprüfen möchte, ob man sich vertippt hat, und später bei der Fehlersuche erneut prüfen muss, ob es Tippfehler gibt. Außerdem ist es netter, einen Variablennamen wie num oder num_b10 zu verwenden als 0|[1-9][0-9]*
.
Es ist sicherlich möglich, Zeichenketten zu speichern und sie an re.match zu übergeben; allerdings ist das weniger lesbar:
num = "..."
# then, much later:
m = re.match(num, input)
Im Gegensatz zum Kompilieren:
num = re.compile("...")
# then, much later:
m = num.match(input)
Obwohl sie ziemlich nahe beieinander liegt, wirkt die letzte Zeile der zweiten bei wiederholter Verwendung natürlicher und einfacher.
NUR SO ZUM SPASS:
$ 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
Wenn Sie also die dieselbe regex häufig verwenden, kann es sich lohnen, die re.compile
(insbesondere für komplexere Regexe).
Es gelten die Standardargumente gegen eine vorzeitige Optimierung, aber ich glaube nicht, dass man wirklich viel Klarheit und Geradlinigkeit verliert, wenn man re.compile
wenn Sie vermuten, dass Ihre Regexps zu einem Leistungsengpass werden könnten.
Aktualisierung:
Unter Python 3.6 (ich vermute, dass die obigen Zeitmessungen mit Python 2.x durchgeführt wurden) und 2018er Hardware (MacBook Pro) erhalte ich nun die folgenden Zeitwerte:
% 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.
Ich habe auch einen Fall hinzugefügt (beachten Sie die Anführungszeichenunterschiede zwischen den letzten beiden Durchläufen), der zeigt, dass re.match(x, ...)
ist wörtlich [ungefähr] gleichbedeutend mit re.compile(x).match(...)
d.h. es scheint keine Zwischenspeicherung der kompilierten Darstellung hinter den Kulissen zu geben.
Hier ist ein einfacher Testfall:
~$ 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
mit re.compile:
~$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 'r = re.compile("[0-9]{3}-[0-9]{3}-[0-9]{4}")' 'r.match("123-123-1234")'; done
1 loops, best of 3: 1.91 usec per loop
10 loops, best of 3: 0.691 usec per loop
100 loops, best of 3: 0.701 usec per loop
1000 loops, best of 3: 0.684 usec per loop
10000 loops, best of 3: 0.682 usec per loop
100000 loops, best of 3: 0.694 usec per loop
1000000 loops, best of 3: 0.702 usec per loop
Es scheint also, dass das Kompilieren in diesem einfachen Fall schneller geht, auch wenn Sie nur einmal übereinstimmen .
Ich habe das gerade selbst ausprobiert. Für den einfachen Fall, eine Zahl aus einer Zeichenkette zu analysieren und zu summieren, ist die Verwendung eines kompilierten Objekts mit regulärem Ausdruck etwa doppelt so schnell wie die Verwendung der re
Methoden.
Wie andere bereits festgestellt haben, ist die re
Methoden (einschließlich re.compile
) die Zeichenkette des regulären Ausdrucks in einem Zwischenspeicher von zuvor kompilierten Ausdrücken nachschlagen. Daher sind im Normalfall die zusätzlichen Kosten für die Verwendung der re
Methoden sind einfach die Kosten für die Cache-Suche.
Die Untersuchung der code zeigt, dass der Cache auf 100 Ausdrücke begrenzt ist. Dies wirft die Frage auf, wie schmerzhaft es ist, den Cache zu überlaufen. Der Code enthält eine interne Schnittstelle für den Compiler für reguläre Ausdrücke, re.sre_compile.compile
. Wenn wir ihn aufrufen, umgehen wir den Cache. Es stellt sich heraus, dass es etwa zwei Größenordnungen langsamer für einen einfachen regulären Ausdruck ist, wie r'\w+\s+([0-9_]+)\s+\w*'
.
Hier ist mein Test:
#!/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
Die "reallyCompiled"-Methoden verwenden die interne Schnittstelle, mit der der Cache umgangen wird. Beachten Sie, dass die Methode, die bei jeder Schleifeniteration kompiliert, nur 10.000 Mal iteriert wird, nicht eine Million Mal.
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.
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
0 Stimmen
@ShreevatsaR Interessant! Können Sie eine Antwort mit einem Beispiel posten, das eine 10-50-fache Verbesserung zeigt? Die meisten Antworten, die hier gegeben werden, zeigen in einigen konkreten Fällen eine 3-fache Verbesserung und in anderen Fällen fast keine Verbesserung.
4 Stimmen
@Basj Done, gepostet eine Antwort . Ich habe mir nicht die Mühe gemacht, herauszufinden, wofür ich Python im Dezember 2013 verwendet habe, aber die erste einfache Sache, die ich ausprobiert habe, zeigt das gleiche Verhalten.