Есть ли польза от использования компиляции регулярных выражений в Python?
h = re.compile('hello')
h.match('hello world')
против
re.match('hello', 'hello world')
Я только что столкнулся со случаем, когда использование re.compile дало улучшение в 10-50 раз. Мораль заключается в том, что если у вас много регулярных выражений (более 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 Готово, отправил ответ. Я не стал раскапывать, для чего я использовал Python в декабре 2013 года, но первая простая вещь, которую я попробовал, показывает такое же поведение.






Регулярные выражения компилируются перед использованием при использовании второй версии. Если вы собираетесь выполнять его много раз, определенно лучше сначала его скомпилировать. Если не компилировать каждый раз, когда вы сопоставляете один раз, это нормально.
Насколько я понимаю, эти два примера фактически эквивалентны. Единственное отличие состоит в том, что в первом случае вы можете повторно использовать скомпилированное регулярное выражение в другом месте, не вызывая его повторной компиляции.
Вот вам справка: http://diveintopython3.ep.io/refactoring.html
Calling the compiled pattern object's search function with the string 'M' accomplishes the same thing as calling re.search with both the regular expression and the string 'M'. Only much, much faster. (In fact, the re.search function simply compiles the regular expression and calls the resulting pattern object's search method for you.)
Я не голосовал против вас, но технически это неправильно: Python все равно не компилируется
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(...), то есть никакого закулисного кэширования скомпилированного представления не происходит.
Здесь есть серьезные проблемы с вашей методологией, поскольку аргумент установки НЕ учитывается во времени. Таким образом, вы удалили время компиляции из второго примера и просто усреднили его в первом примере. Это не означает, что первый пример компилируется каждый раз.
Да, я согласен с тем, что это несправедливое сравнение двух случаев.
Я понимаю, что вы имеете в виду, но разве это не то, что могло бы произойти в реальном приложении, где регулярное выражение используется много раз?
@dF: Вы правы, ЕСЛИ вы заботитесь о производительности только в одной конкретной части вашего кода, и вы можете предварительно скомпилировать регулярное выражение в другой части. В противном случае вам нужно рассчитать время вызова re.compile и включить его во второе число, чтобы сравнение было справедливым.
@Triptych, @Kiv: весь смысл компиляции регулярных выражений отдельно от использования является для минимизации компиляции; удаление его из тайминга - это именно то, что должен был сделать dF, потому что он наиболее точно представляет реальное использование. Время компиляции особенно не имеет отношения к тому, как timeit.py выполняет здесь свои временные параметры; он выполняет несколько запусков и сообщает только о самом коротком, после чего скомпилированное регулярное выражение кэшируется. Дополнительные затраты, которые вы видите здесь, - это не стоимость компиляции регулярного выражения, а стоимость поиска его в кэше скомпилированного регулярного выражения (словаре).
Этот тест вводит в заблуждение. Время выполнения общее обоих тестов было бы эквивалентным в реальном коде. Компиляция сначала позволяет вам решить, что когда съедает эти циклы ЦП, а не если.
@Triptych Следует ли вынуть import re из установки? Все дело в том, где вы хотите измерить. Если я запустил скрипт на Python несколько раз, у него было бы время import re. При сравнении этих двух важно разделить две линии для определения времени. Да, как вы говорите, это когда у вас будет время ударить. Сравнение показывает, что либо вы принимаете временное совпадение один раз и повторяете меньшее время, полученное путем компиляции, либо вы принимаете попадание каждый раз, предполагая, что кеш очищается между вызовами, что, как было указано, могло произойти. Добавление времени h=re.compile('hello') поможет прояснить ситуацию.
Если вы работаете в Linux-подобной операционной системе, просто используйте time python -m... вместо python -m..., и ваши результаты будут такими же, как у меня, вы увидите, что предварительная компиляция действительно дает значительный выигрыш в производительности (для меня сначала 3.87s CPU итого, секунда - 1,64 с.
Регулярное выражение - это простая строка. Результатом времени является время поиска по регулярному выражению + время поиска в кэше для первого раза. Оставляя в стороне начальное время компиляции и другие недостатки этого метода, если бы мы резко увеличили время регулярного выражения, потребовала бы ли разница между двумя временными интервалами предварительную компиляцию?
Запуск python -m timeit -s "import re; n=1000" "h=re.compile('hello'); [ h.match('hello world') for i in range(n) ]" против python -m timeit -s "import re; n=1000" "[ re.match('hello', 'hello world') for i in range(n) ]" по-прежнему дает в 2 раза более быстрое время выполнения предварительно скомпилированного регулярного выражения. Это противоречит предыдущим комментариям, в которых говорилось, что это несправедливое сравнение. Предлагаю включить эти тесты в ответ.
У меня был большой опыт запуска скомпилированного регулярного выражения 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
Я по-прежнему часто предварительно компилирую регулярные выражения, но только для того, чтобы связать их с красивым, многоразовым именем, а не для ожидаемого увеличения производительности.
Ваш вывод несовместим с вашим ответом. Если регулярные выражения компилируются и сохраняются автоматически, в большинстве случаев нет необходимости делать это вручную.
Дж. Ф. Себастьяна, это служит сигналом для программиста, что рассматриваемое регулярное выражение будет часто использоваться и не предназначено для выбрасывания.
Более того, я бы сказал, что если вы не хотите, чтобы компиляция и кеш-файлы попадали в какую-то критическую для производительности часть вашего приложения, вам лучше скомпилировать их заранее в некритичной части вашего приложения. .
Могу только добавить, что _MAXCACHE = 100 в 2.5+ и 3.0.
Я вижу главное преимущество использования скомпилированного регулярного выражения, если вы повторно используете одно и то же регулярное выражение несколько раз, тем самым уменьшая возможность опечаток. Если вы вызываете его только один раз, то некомпилированный код будет более читабельным.
Итак, основное различие будет заключаться в том, что вы используете много разных регулярных выражений (больше, чем _MAXCACHE), некоторые из них только один раз, а другие много раз ... тогда важно сохранить ваши скомпилированные выражения для тех, которые используются чаще, чтобы они не выгружаются из кеша, когда он заполнен.
@ J.F. - Кроме того, если вы полагаетесь на компиляцию и кеширование, кто знает, когда кеш может быть очищен, и тогда ваше регулярное выражение придется перекомпилировать.
Если вы используете python <2.7 или 3.1, в re.sub отсутствует параметр flags. Итак, если, скажем, вы хотите выполнить re.sub без учета регистра, вы застряли при выполнении re.compile("...", re.I).sub(...).
Я думаю, что все упустили суть в целом, даже если вы исключите опечатки и неизвестное время сборки мусора, факт в том, что если вам нужно запускать одно и то же регулярное выражение 100000 раз подряд, не выполняя поиск в кеше, в 100000 раз быстрее, давайте подумаем о это с точки зрения синтаксического анализа большого файла журнала с помощью регулярного выражения, каждый шаг, который должен предпринять lanauage, должен быть лучше.
Еще одна причина избегать этапа компиляции - приблизить регулярные выражения к месту их использования. У меня была петля с десятками замен. Придумывание имен для скомпилированных регулярных выражений и необходимость поиска количества скобок RE в начале цикла сделали программу менее читаемой.
При переполнении очищается весь кеш?!?!? Я собирался использовать кеш LFU или LRU. Еще больше причин для компиляции паттернов, которые я собираюсь использовать более одного раза. Вы никогда не знаете, будет ли какой-нибудь другой модуль, который также импортирует re, заполнять и очищать кеш.
@WojonsTech Я тоже думал об этом, но имеет ли это какое-то существенное значение?
"" У меня был большой опыт запуска скомпилированного регулярного выражения 1000 раз по сравнению с компиляцией на лету, и я не заметил какой-либо заметной разницы. ... "* Это слишком расплывчато и вводит в заблуждение. Использование предварительно скомпилированного регулярного выражения в 3 раза быстрее по сравнению со вторым использованием, и даже первое использование в 2 раза быстрее. Дело в том. Вопрос в том, важна ли скорость регулярного выражения для определенного В противном случае прямое использование шаблона ad hoc сделает в большинстве случаев более простым для записи, чтения и отладки.
Я также думаю, что ваш ответ вводит в заблуждение: я запустил более 10000 предложений и сделал в них цикл для итерации для регулярных выражений, когда регулярные выражения не были скомпилированы и рассчитывались каждый раз, когда прогноз полного выполнения составлял 8 часов после создания словаря в соответствии с для индексации с помощью скомпилированных шаблонов регулярных выражений я запускаю все это в течение 2 минут. Я не могу понять этого ответа ...
Python 3 прямо упоминает об этом: «Скомпилированные версии самых последних шаблонов, переданных в re.compile (), и функции сопоставления на уровне модуля кэшируются, поэтому программам, использующим только несколько регулярных выражений одновременно, не нужно беспокоиться о компиляции регулярных выражений. »Это следует добавить к ответу.
Это хороший вопрос. Часто можно увидеть, как люди используют re.compile без причины. Это снижает удобочитаемость. Но, конечно, бывает много раз, когда требуется предварительная компиляция выражения. Например, когда вы используете его несколько раз в цикле или что-то подобное.
Это похоже на все, что касается программирования (на самом деле все в жизни). Применяйте здравый смысл.
Насколько я могу судить из моего краткого обзора, Python в двух словах не упоминает об использовании без re.compile (), что вызвало у меня любопытство.
Объект регулярного выражения добавляет в контекст еще один объект. Как я уже сказал, существует множество ситуаций, когда re.compile () имеет свое место. Пример, приведенный ОП, не входит в их число.
Для меня самое большое преимущество re.compile - это возможность отделить определение регулярного выражения от его использования.
Даже простое выражение, такое как 0|[1-9][0-9]* (целое число в базе 10 без начальных нулей), может быть достаточно сложным, чтобы вам не приходилось вводить его повторно, проверять, допустили ли вы какие-либо опечатки, а позже придется перепроверить, есть ли опечатки при запуске. отладка. Кроме того, лучше использовать имя переменной, такое как num или num_b10, чем 0|[1-9][0-9]*.
Конечно, можно сохранять строки и передавать их на повторное сопоставление; однако это меньше читается:
num = "..."
# then, much later:
m = re.match(num, input)
По сравнению с компиляцией:
num = re.compile("...")
# then, much later:
m = num.match(input)
Хотя это довольно близко, последняя строка второй кажется более естественной и простой при повторном использовании.
Я согласен с этим ответом; часто использование re.compile приводит к более, а не менее читаемому коду.
Однако иногда верно и обратное - например, если вы определяете регулярное выражение в одном месте и используете его совпадающие группы в другом отдаленном месте.
@KenWilliams Не обязательно, регулярное выражение с правильным названием для конкретной цели должно быть четким, даже если оно используется далеко от исходного определения. Например us_phone_number или social_security_number и т. д.
@ BrianM.Sheldon, правильно называя регулярное выражение, на самом деле не помогает вам узнать, что представляют собой его различные группы захвата.
Интересно, что компиляция оказалась для меня более эффективной (Python 2.5.2 в Win XP):
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: Я согласен с Карлом Мейером. Компиляция происходит в обоих случаях. Триптих упоминает, что задействовано кеширование, поэтому в оптимальном случае (остается в кеше) оба подхода - O (n + 1), хотя часть +1 как бы скрыта, если вы не используете re.compile явно.
Не пишите свой собственный тестовый код. Научитесь использовать timeit.py, который входит в стандартный дистрибутив.
Сколько из этого времени вы воссоздаете строку шаблона в цикле for. Эти накладные расходы не могут быть тривиальными.
В общем, я считаю, что проще использовать флаги (по крайней мере, легче запомнить, как), например 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.
(несколько месяцев спустя) легко добавить свой собственный кеш вокруг 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 ...
Я провел этот тест, прежде чем наткнулся на обсуждение здесь. Однако, запустив его, я подумал, что хотя бы выложу свои результаты.
Я украл и испортил пример из книги Джеффа Фридла «Освоение регулярных выражений». Это на MacBook под управлением OSX 10.6 (2 ГГц Intel Core 2 Duo, 4 ГБ оперативной памяти). Версия 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
Я просто пробовал это сам. В простом случае выделения числа из строки и его суммирования использование скомпилированного объекта регулярного выражения примерно в два раза быстрее, чем использование методов 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
Методы «действительно компилированный» используют внутренний интерфейс, который обходит кеш. Обратите внимание, что тот, который компилируется на каждой итерации цикла, повторяется только 10 000 раз, а не один миллион.
Я согласен с вами, что скомпилированные регулярные выражения работают намного быстрее, чем некомпилированные. Я запустил более 10000 предложений и сделал в них цикл для повторения регулярных выражений, когда регулярные выражения не были скомпилированы и вычислялись каждый раз, когда прогнозирование полного выполнения составляло 8 часов, после создания словаря в соответствии с индексом с помощью скомпилированных шаблонов регулярных выражений, которые я запускал все это за 2 минуты. Я не могу понять ответы выше ...
я хотел бы мотивировать это предварительная компиляция как концептуально, так и «грамотно» (как в «грамотном программировании») выгодна. взгляните на этот фрагмент кода:
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, для того, чтобы выполнить одно совпадение, мы должны создать экземпляр объекта, выполнить три поиска по словарю экземпляров и выполнить три вызова функций; Кроме того, мы можем столкнуться с проблемами кеширования re при использовании более 100 регулярных выражений. кроме того, регулярное выражение скрывается внутри тела метода, что в большинстве случаев не очень хорошая идея.
можно сказать, что каждое подмножество мер --- целевые операторы импорта с псевдонимами; методы с псевдонимами, где это применимо; сокращение вызовов функций и поиска в словаре объектов --- может помочь снизить вычислительную и концептуальную сложность.
Вот простой тестовый пример:
~$ 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
с 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
Таким образом, в этом простом случае, даже если вы соответствуете только один раз, кажется, что компиляция выполняется быстрее.
Какая это версия Python?
на самом деле это не имеет значения, дело в том, чтобы попробовать тест в среде, в которой вы будете запускать код
Для меня производительность почти одинакова для 1000 петель и более. Скомпилированная версия быстрее на 1-100 циклов. (На обоих питонах 2.7 и 3.4).
В моей настройке Python 2.7.3 почти нет никакой разницы. Иногда компиляция выполняется быстрее, иногда - медленнее. Разница всегда <5%, поэтому я считаю разницу как погрешность измерения, поскольку устройство имеет только один процессор.
В Python 3.4.3 видно два отдельных прогона: использование скомпилированного было даже медленнее, чем некомпилированное.
При запуске на python 3.6.5 на Ubuntu 16.04 результаты для двух операторов аналогичны.
Используя приведенные примеры:
h = re.compile('hello')
h.match('hello world')
Метод матч в приведенном выше примере отличается от того, который используется ниже:
re.match('hello', 'hello world')
re.compile () возвращает объект регулярного выражения, что означает, что h является объектом регулярного выражения.
У объекта регулярного выражения есть собственный метод матч с необязательными параметрами позиция и endpos:
regex.match(string[, pos[, endpos]])
позиция
The optional second parameter pos gives an index in the string where the search is to start; it defaults to 0. This is not completely equivalent to slicing the string; the
'^'pattern character matches at the real beginning of the string and at positions just after a newline, but not necessarily at the index where the search is to start.
endpos
The optional parameter endpos limits how far the string will be searched; it will be as if the string is endpos characters long, so only the characters from pos to
endpos - 1will be searched for a match. If endpos is less than pos, no match will be found; otherwise, if rx is a compiled regular expression object,rx.search(string, 0, 50)is equivalent torx.search(string[:50], 0).
Методы поиск, найти все и фидитер объекта регулярного выражения также поддерживают эти параметры.
re.match(pattern, string, flags=0) их не поддерживает, как видите,
равно как и его аналоги поиск, найти все и фидитер.
сопоставить объект имеет атрибуты, которые дополняют эти параметры:
match.pos
The value of pos which was passed to the search() or match() method of a regex object. This is the index into the string at which the RE engine started looking for a match.
match.endpos
The value of endpos which was passed to the search() or match() method of a regex object. This is the index into the string beyond which the RE engine will not go.
объект регулярного выражения имеет два уникальных, возможно, полезных атрибута:
regex.groups
The number of capturing groups in the pattern.
regex.groupindex
A dictionary mapping any symbolic group names defined by (?P) to group numbers. The dictionary is empty if no symbolic groups were used in the pattern.
И, наконец, сопоставить объект имеет этот атрибут:
match.re
The regular expression object whose match() or search() method produced this match instance.
Помимо разницы в производительности, использование 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 пожаловался бы, что «строка не имеет атрибута соответствия», поскольку при сопоставлении позиционных аргументов в comparex[columnIndex] используется как регулярное выражение !, когда я на самом деле имел в виду
compare = lambda p, s: p.match(s)
В моем случае использование re.compile более четко отражает цель регулярного выражения, когда его значение скрыто невооруженным глазом, поэтому я мог бы получить дополнительную помощь от проверки времени выполнения Python.
Итак, мораль моего урока такова: если регулярное выражение - это не просто буквальная строка, тогда я должен использовать re.compile, чтобы Python помог мне подтвердить мое предположение.
Я согласен с Честным Абэ в том, что 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) вызывает (B) после вызова (D), который также вызывается (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(...) вызывается только один раз, нам нужно сохранить скомпилированный объект регулярного выражения в памяти. Если мы используем класс, мы могли бы сохранить объект и повторно использовать его при каждом вызове нашей функции.
class Foo:
regex = re.compile('hello')
def my_function(text)
return regex.match(text)
Если мы не используем класс (о чем я сегодня прошу), то у меня нет комментариев. Я все еще учусь использовать глобальную переменную в Python и знаю, что глобальная переменная - это плохо.
Еще один момент, я считаю, что использование подхода (A) + (B) имеет преимущество. Вот некоторые факты, которые я заметил (поправьте меня, если я ошибаюсь):
Вызов A один раз, он выполнит один поиск в _cache, за которым следует один sre_compile.compile(), чтобы создать объект регулярного выражения. Вызов A дважды, он выполнит два поиска и одну компиляцию (поскольку объект регулярного выражения кэшируется).
Если _cache между ними сбрасывается, то объект регулярного выражения освобождается из памяти, и Python необходимо снова компилировать. (кто-то предполагает, что Python не будет перекомпилировать.)
Если мы сохраним объект регулярного выражения с помощью (A), объект регулярного выражения все равно попадет в _cache и каким-то образом будет сброшен. Но наш код сохраняет ссылку на него, и объект регулярного выражения не будет освобожден из памяти. Те, Python не нужно компилировать заново.
Разница в 2 секунды в тесте Джорджа compiledInLoop и compiled - это в основном время, необходимое для создания ключа и поиска в _cache. Это не означает время компиляции регулярного выражения.
Тест действительно компиляции Джорджа показывает, что произойдет, если он действительно будет каждый раз заново выполнять компиляцию: он будет в 100 раз медленнее (он уменьшил цикл с 1 000 000 до 10 000).
Вот единственные случаи, когда (A + B) лучше, чем (C):
Случай, когда (C) достаточно хорош:
Просто резюмируйте, вот A B C:
h = re.compile('hello') # (A)
h.match('hello world') # (B)
re.match('hello', 'hello world') # (C)
Спасибо за прочтение.
Есть еще одно преимущество использования re.compile () в виде добавления комментариев к моим шаблонам регулярных выражений с помощью re.VERBOSE
pattern = '''
hello[ ]world # Some info on my pattern logic. [ ] to recognize space
'''
re.search(pattern, 'hello world', re.VERBOSE)
Хотя это не влияет на скорость выполнения вашего кода, мне нравится делать это таким образом, поскольку это часть моей привычки комментировать. Я совершенно не люблю тратить время на попытки вспомнить логику, лежащую в основе моего кода через 2 месяца, когда я хочу внести изменения.
Я отредактировал ваш ответ. Я думаю, что упоминание re.VERBOSE имеет смысл, и оно добавляет кое-что, что, похоже, упустили другие ответы. Однако если вы начнете свой ответ со слов «Я публикую здесь, потому что пока не могу комментировать», он обязательно удалит его. Пожалуйста, не используйте поле для ответов ни для чего другого, кроме ответов. От вас отделяет всего один или два хороших ответа от возможности комментировать где-либо (50 повторений), поэтому, пожалуйста, проявите терпение. Помещая комментарии в поля для ответов, когда вы знаете, что не должны, вы не получите туда быстрее. Это даст вам отрицательные голоса и удаленные ответы.
Этот ответ может прийти с опозданием, но это интересная находка. Использование компиляции действительно может сэкономить ваше время, если вы планируете использовать регулярное выражение несколько раз (это также упоминается в документации). Ниже вы можете видеть, что использование скомпилированного регулярного выражения является самым быстрым, когда для него напрямую вызывается метод сопоставления. передача скомпилированного регулярного выражения в 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
I've had a lot of experience running a compiled regex 1000s of times versus compiling on-the-fly, and have not noticed any perceivable difference
Голосование за принятый ответ приводит к предположению, что то, что говорит @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 или нет. Внутри все функции реализованы на этапе компиляции:
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}')
" в def search(pattern, string, flags=0):" - опечатка?
Обратите внимание, что если pattern уже является скомпилированным шаблоном, накладные расходы на кэширование становятся значительными: хеширование SRE_Pattern является дорогостоящим, и шаблон никогда не записывается в кеш, поэтому поиск каждый раз завершается ошибкой с KeyError.
Я очень уважаю все приведенные выше ответы. На мой взгляд Да! Конечно, стоит использовать re.compile вместо компиляции регулярного выражения снова и снова, каждый раз.
Using re.compile makes your code more dynamic, as you can call the already compiled regex, instead of compiling again and aagain. This thing benefits you in cases:
Пример :
example_string = "The room number of her room is 26A7B."
find_alpha_numeric_string = re.compile(r"\b\w+\b")
find_alpha_numeric_string.findall(example_string)
find_alpha_numeric_string.search(example_string)
Similarly you can use it for: Match and Substitute
Помимо выступления.
Использование 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 документация:
Последовательность
prog = re.compile(pattern)
result = prog.match(string)
эквивалентно
result = re.match(pattern, string)
но использование re.compile() и сохранение полученного объекта регулярного выражения для повторного использования более эффективно, когда выражение будет использоваться несколько раз в одной программе.
Итак, я пришел к выводу, что если вы собираетесь сопоставить один и тот же шаблон для множества разных текстов, вам лучше предварительно скомпилировать его.
В качестве альтернативного ответа, поскольку я вижу, что об этом раньше не упоминалось, я процитирую Документы Python 3:
Should you use these module-level functions, or should you get the pattern and call its methods yourself? If you’re accessing a regex within a loop, pre-compiling it will save a few function calls. Outside of loops, there’s not much difference thanks to the internal cache.
Для меня главное преимущество состоит в том, что мне нужно только запомнить и прочитать форму один сложного синтаксиса API регулярных выражений - форму <compiled_pattern>.method(xxx), а не форму а такжеre.func(<pattern>, xxx).
re.compile(<pattern>), правда, немного лишний шаблон.
Но что касается регулярных выражений, этот дополнительный шаг компиляции вряд ли станет большой причиной когнитивной нагрузки. Фактически, в сложных шаблонах вы можете даже получить ясность, отделив объявление от любого метода регулярного выражения, который вы затем вызываете для него.
Я обычно сначала настраиваю сложные шаблоны на веб-сайте, таком как Regex101, или даже в отдельном минимальном тестовом сценарии, а затем добавляю их в свой код, поэтому разделение объявления от его использования также подходит для моего рабочего процесса.
Вот пример, в котором использование 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 использование одного и того же регулярного выражения несколько раз и переход к следующему было не так уж плохо (всего в 2 раза медленнее, чем с re.compile), но в другом порядке (цикл через множество регулярных выражений) значительно хуже, как и ожидалось. Кроме того, увеличение размера кэша тоже работает: просто установка re._MAXCACHE = len(patterns) в setup() выше (конечно, я не рекомендую делать такие вещи в продакшене, поскольку имена с подчеркиванием обычно являются «частными») снижает ~ 23 секунды до ~ 0,7 секунды, что также соответствует нашему пониманию.
PS: если я использую Только 3 шаблона регулярного выражения во всем моем коде, каждый из которых используется (без какого-либо определенного порядка) сотни раз, кеш регулярного выражения автоматически сохранит предварительно скомпилированное регулярное выражение, верно?
@Basj Я думаю, вы могли бы просто попробовать и посмотреть :) Но ответ, я почти уверен, да: единственная дополнительная стоимость в этом случае AFAICT - это просто поиск шаблона в кеше. Также обратите внимание, что кеш является глобальным (на уровне модуля), поэтому в принципе у вас может быть некоторая библиотека зависимостей, выполняющая поиск по регулярным выражениям между вами, поэтому трудно быть полностью уверенным, что ваша программа когда-либо использует только 3 (или любое другое количество) регулярных выражений паттерны, но иначе было бы довольно странно :)
Кроме того, что в 2.6
re.subне принимает аргумент флагов ...