Стоит ли использовать Python re.compile?


462

Есть ли преимущество в использовании компиляции для регулярных выражений в Python?

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

против

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

8
Кроме того факта, что в 2.6 re.subне будет аргумента флагов ...
new123456

58
Я только что натолкнулся на случай, когда использование re.compileдало улучшение в 10-50 раз. Мораль такова: если у вас есть много регулярных выражений (больше, чем MAXCACHE = 100), и вы используете их много раз каждый (и разделены более чем двумя регулярными выражениями MAXCACHE между ними, так что каждый из них сбрасывается из кэша: используйте то же самое много раз, а затем переход к следующему не считается), тогда это определенно поможет скомпилировать их. В противном случае это не имеет значения.
ShreevatsaR

8
Следует отметить одну небольшую вещь: для строк, которые не нуждаются в регулярном выражении, 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
Gamrix

@ShreevatsaR Интересно! Можете ли вы опубликовать ответ с примером, показывающим улучшение в 10–50 раз? Большинство ответов, приведенных здесь, фактически показывают 3-кратное улучшение в некоторых точных случаях, а в других случаях улучшение практически отсутствует.
Басж

1
@Basj Готово, опубликовал ответ . Я не стал копать то, для чего использовал Python в декабре 2013 года, но первая простая попытка, которую я попробовал, показывает то же поведение.
ShreevatsaR

Ответы:


436

У меня был большой опыт работы с скомпилированным регулярным выражением 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

Я до сих пор часто предварительно компилирую регулярные выражения, но только для того, чтобы связать их с красивым, многократно используемым именем, а не для ожидаемого увеличения производительности.


12
Ваш вывод не соответствует вашему ответу. Если регулярные выражения компилируются и сохраняются автоматически, в большинстве случаев нет необходимости делать это вручную.
JFS

84
Дж. Ф. Себастьян, это служит сигналом для программиста, что рассматриваемое регулярное выражение будет много использоваться и не должно быть отбрасыванием.
калейссин

40
Более того, я бы сказал, что если вы не хотите подвергаться ударам компиляции и кэширования в какой-то критически важной для вашего приложения части, лучше всего скомпилировать их перед установкой некритической части вашего приложения. ,
Эдди Паркер

20
Я вижу главное преимущество использования скомпилированного регулярного выражения, если вы повторно используете одно и то же регулярное выражение несколько раз, тем самым уменьшая вероятность опечаток. Если вы просто вызываете его один раз, то некомпилированный будет более читабельным.
Monkut

18
Итак, основное отличие будет в том, что когда вы используете много различных регулярных выражений (больше, чем _MAXCACHE), некоторые из них только один раз, а другие много раз ... тогда важно сохранить ваши скомпилированные выражения для тех, которые используются чаще, чтобы они не выгружаются из кеша, когда он полон.
Фортран

133

Для меня самое большое преимущество 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)

Хотя это довольно близко, последняя строка второй кажется более естественной и простой при повторном использовании.


5
Я согласен с этим ответом; часто использование re.compile приводит к большему, не менее читабельному коду.
Карл Мейер

1
Иногда, наоборот, верно - например, если вы определяете регулярное выражение в одном месте и используете соответствующие группы в другом отдаленном месте.
Кен Уильямс

1
@KenWilliams Не обязательно, хорошо названное регулярное выражение для конкретной цели должно быть ясным, даже если оно используется далеко от исходного определения. Например, us_phone_numberи social_security_numberт. Д.
Брайан М. Шелдон

2
@ BrianM. Шелдон, хорошо называя регулярное выражение, на самом деле не поможет вам понять, что представляют его различные группы захвата.
Кен Уильямс

69

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(...), то есть, похоже, никакого закулисного кэширования скомпилированного представления не происходит.


5
Основные проблемы с вашей методологией здесь, поскольку аргумент настройки НЕ включается во время. Таким образом, вы удалили время компиляции из второго примера и просто усреднили его в первом примере. Это не значит, что первый пример компилируется каждый раз.
Триптих

1
Да, я согласен, что это несправедливое сравнение двух случаев.
Кив

7
Я понимаю, что вы имеете в виду, но разве это не то, что происходит в реальном приложении, где регулярное выражение используется много раз?
ДФ.

26
@Triptych, @Kiv: Смысл компиляции регулярных выражений отдельно от использования заключается в минимизации компиляции; удаление его из времени - это именно то, что dF должен был сделать, потому что он наиболее точно отражает реальное использование. Время компиляции особенно не имеет отношения к тому, как timeit.py выполняет свои настройки здесь; он выполняет несколько запусков и сообщает только самый короткий, после чего скомпилированное регулярное выражение кэшируется. Дополнительные затраты, которые вы видите здесь, это не стоимость компиляции регулярного выражения, а стоимость поиска его в скомпилированном кэше регулярного выражения (словаре).
Джемфинч

3
@Triptych Должен ли import reбыть выведен из установки? Это все о том, где вы хотите измерить. Если бы я запускал сценарий на Python много раз, это было бы import reударом по времени. При сравнении двух важно разделить две строки для определения времени. Да, как вы говорите, это когда у вас будет время. Сравнение показывает, что либо вы берете удар по времени один раз и повторяете меньшее попадание при компиляции, либо вы берете удар каждый раз, предполагая, что кэш очищается между вызовами, что, как было указано, может произойти. Добавление сроков h=re.compile('hello')поможет уточнить.
Том Мидделтын

39

Вот простой тестовый пример:

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

Таким образом, в этом простом случае может показаться, что компиляция выполняется быстрее, даже если вы совпадаете только один раз .


2
Какая версия Python это?
Кайл Стрэнд,

2
на самом деле это не имеет значения, смысл в том, чтобы попробовать эталонный тест в среде, в которой вы будете запускать код
Дэвид

1
Для меня производительность почти одинакова для 1000 и более петель. Скомпилированная версия быстрее на 1-100 циклов. (На обоих питонах 2.7 и 3.4).
Цитракс

2
На моей установке Python 2.7.3 почти нет разницы. Иногда компиляция происходит быстрее, иногда медленнее. Разница всегда <5%, поэтому я считаю разницу как погрешность измерения, поскольку устройство имеет только один процессор.
Даккарон

1
В Python 3.4.3 наблюдается два отдельных запуска: использование скомпилировано было даже медленнее, чем не скомпилировано.
Зельфир Кальцталь

17

Я просто попробовал это сам. Для простого случая разбора числа из строки и его суммирования использование скомпилированного объекта регулярного выражения примерно в два раза быстрее, чем использование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 раз, а не один миллион.


Я согласен с вами, что скомпилированные регулярные выражения работают намного быстрее, чем не скомпилированные. Я выполнил более 10 000 предложений и создал цикл для повторения регулярных выражений, когда регулярные выражения не были скомпилированы и вычислялись каждый раз, когда прогноз полного цикла составлял 8 часов, после создания словаря по индексу с скомпилированными шаблонами регулярных выражений, которые я запускаю все это за 2 минуты. Я не могу понять ответы выше ...
Эли Бородач

12

Я согласен с Честным Абэ, что match(...)в приведенных примерах они разные. Они не являются взаимно-однозначными сравнениями и, следовательно, результаты различны. Чтобы упростить свой ответ, я использую A, B, C, D для этих функций. О да, мы имеем дело с 4 функциями вre.py вместо 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)подхода имеет преимущество. Вот некоторые факты, которые я заметил (поправьте меня, если я ошибаюсь):

  1. Вызывает A один раз, он выполнит один поиск, _cacheа затем один, sre_compile.compile()чтобы создать объект регулярного выражения. Вызов A дважды, он выполнит два поиска и одну компиляцию (потому что объект регулярного выражения кэшируется).

  2. Если _cacheпромежуточное значение сбрасывается, то объект регулярного выражения освобождается из памяти, и Python должен снова скомпилироваться. (кто-то предполагает, что Python не будет перекомпилирован.)

  3. Если мы сохраним объект regex с помощью (A), объект regex все равно попадет в _cache и каким-то образом очистится. Но наш код сохраняет ссылку на него, и объект regex не будет освобожден из памяти. Те, Python не нужно компилировать снова.

  4. Разница в 2 секунды в тесте Джорджа compiledInLoop vs compiled - это в основном время, необходимое для создания ключа и поиска в _cache. Это не означает время компиляции регулярных выражений.

  5. По-настоящему тестовый тест Джорджа показывает, что произойдет, если он действительно будет повторять компиляцию каждый раз: это будет в 100 раз медленнее (он уменьшил цикл с 1 000 000 до 10 000).

Вот единственные случаи, когда (A + B) лучше, чем (C):

  1. Если мы можем кэшировать ссылку на объект регулярного выражения внутри класса.
  2. Если нам нужно вызывать (B) повторно (внутри цикла или несколько раз), мы должны кэшировать ссылку на объект регулярного выражения вне цикла.

Случай, который (C) достаточно хорош:

  1. Мы не можем кэшировать ссылку.
  2. Мы используем его только время от времени.
  3. В целом, у нас не так уж много регулярных выражений (предположим, что скомпилированный никогда не сбрасывается)

Просто резюме, вот азбука:

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

Спасибо за чтение.


8

В основном, нет разницы, используете ли вы 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}')

1
это "в def search(pattern, string, flags=0):"опечатка?
phuclv

1
Обратите внимание, что если patternэто уже скомпилированный шаблон, затраты на кэширование становятся значительными: хеширование a обходится SRE_Patternдорого, и шаблон никогда не записывается в кэш, поэтому поиск каждый раз завершается ошибкой с помощью a KeyError.
Эрик

5

В общем, я считаю, что легче использовать флаги (по крайней мере, легче запомнить, как), как 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любом случае, вы можете использовать флаги в качестве третьего аргумента .
aderchox

5

Используя приведенные примеры:

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

Метод match в приведенном выше примере отличается от того, который используется ниже:

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

re.compile () возвращает объект регулярного выражения , что означает hобъект регулярного выражения.

У объекта regex есть собственный метод match с необязательными параметрами pos и endpos :

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

позиция

Необязательный второй параметр pos дает индекс в строке, с которой начинается поиск; по умолчанию это 0. Это не полностью эквивалентно разрезанию строки; '^'шаблон символы соответствуют в реальном начале строки и в позициях сразу после символа новой строки, но не обязательно в индексе , где начинается поиск.

endpos

Необязательный параметр endpos ограничивает область поиска строки; это будет как если строка endpos символы, поэтому только персонажи из поз в endpos - 1будут искать совпадения. Если endpos меньше pos , совпадение не будет найдено; в противном случае, если rx является скомпилированным объектом регулярного выражения, rx.search(string, 0, 50)эквивалентно rx.search(string[:50], 0).

Методы search , findall и finditer объекта regex также поддерживают эти параметры.

re.match(pattern, string, flags=0)не поддерживает их, как вы можете видеть,
а также их поиск , поиск и поиск аналоги .

У совпадающего объекта есть атрибуты, которые дополняют эти параметры:

match.pos

Значение pos, которое было передано в метод search () или match () объекта регулярного выражения. Это указатель на строку, в которой механизм RE начал поиск совпадения.

match.endpos

Значение endpos, которое было передано в метод search () или match () объекта regex. Это указатель на строку, за которую двигатель RE не пойдет.


Объект регулярного выражения имеет два уникальных, возможно, полезных атрибута:

regex.groups

Количество групп захвата в шаблоне.

regex.groupindex

Словарь, отображающий любые символические имена групп, определенные (? P), на номера групп. Словарь является пустым, если в шаблоне не использовались символические группы.


И, наконец, объект сопоставления имеет этот атрибут:

match.re

Объект регулярного выражения, метод match () или search () которого создал этот экземпляр соответствия.


4

Помимо различия в производительности, использование 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 помог мне подтвердить мои предположения.


4

Существует одно дополнительное преимущество использования re.compile () в форме добавления комментариев к моим шаблонам регулярных выражений с использованием re.VERBOSE.

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

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

Хотя это не влияет на скорость выполнения вашего кода, мне нравится делать это таким образом, поскольку это является частью моей привычки комментировать. Мне совершенно не нравится тратить время, пытаясь вспомнить логику, которая стояла за моим кодом через 2 месяца, когда я хочу внести изменения.


1
Я отредактировал твой ответ. Я думаю, что упоминание re.VERBOSEимеет смысл, и оно добавляет то, что другие ответы, кажется, не учли. Однако если вы ответите «Я пишу здесь, потому что я пока не могу комментировать», то обязательно удалите его. Пожалуйста, не используйте поле для ответов ни для чего, кроме ответов. Вы только один или два хороших ответа от возможности комментировать в любом месте (50 повторений), поэтому, пожалуйста, будьте терпеливы. Помещая комментарии в поля для ответов, когда вы знаете, что не должны, вы быстрее туда не попадете. Это даст вам отрицательные отзывы и удаленные ответы.
skrrgwasme

4

Согласно документации Python :

Последовательность

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

эквивалентно

result = re.match(pattern, string)

но используя re.compile() и сохранение полученного объекта регулярного выражения для повторного использования более эффективно, когда выражение будет использоваться несколько раз в одной программе.

Итак, мой вывод: если вы собираетесь сопоставить один и тот же шаблон для множества разных текстов, вам лучше его предварительно скомпилировать.


3

Интересно, что компиляция оказывается более эффективной для меня (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строками, прокомментированными наоборот, скомпилированное регулярное выражение в два раза быстрее


2
Та же проблема, что и при сравнении производительности dF. Это не совсем справедливо, если вы не включите затраты на производительность самого оператора компиляции.
Карл Мейер

6
Карл, я не согласен. Компиляция выполняется только один раз, а соответствующий цикл выполняется миллион раз
Эли Бендерский,

@eliben: я согласен с Карлом Мейером. Компиляция происходит в обоих случаях. Триптих упоминает, что кеширование задействовано, поэтому в оптимальном случае (остается в кеше) оба подхода O (n + 1), хотя часть +1 скрыта, если вы не используете re.compile явно.
паприка

1
Не пишите свой собственный код для тестирования. Научитесь использовать timeit.py, который входит в стандартный дистрибутив.
Джемфинч

Сколько времени вы воссоздаете строку шаблона в цикле for. Эти накладные расходы не могут быть тривиальными.
IceArdor

3

Я провел этот тест, прежде чем наткнуться на обсуждение здесь. Однако, запустив его, я решил опубликовать результаты.

Я украл и убрал пример из «Мастеринг регулярных выражений» Джеффа Фридла. Это на 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

Run 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

3

Этот ответ может опоздать, но это интересная находка. Использование компиляции может реально сэкономить ваше время, если вы планируете использовать регулярные выражения несколько раз (это также упоминается в документации). Ниже вы можете увидеть, что использование скомпилированного регулярного выражения является самым быстрым, когда к нему напрямую вызывается метод 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

3

Помимо производительности.

Использование compileпомогает мне различать понятия:
1. module (re) ,
2. regex object
3. match object
Когда я начал изучать regex

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

2

Я действительно уважаю все вышеперечисленные ответы. С моего мнения да! Наверняка стоит использовать 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)

Точно так же вы можете использовать его для: совпадения и замены


1

Это хороший вопрос. Вы часто видите, как люди используют re.compile без причины. Это уменьшает читабельность. Но уверен, что во многих случаях требуется предварительная компиляция выражения. Например, когда вы используете это многократно в цикле или что-то подобное.

Это как все в программировании (все в жизни на самом деле). Примените здравый смысл.


Насколько я могу судить по краткому обзору, Python в двух словах не упоминает об использовании без re.compile (), что меня заинтересовало.
Мат

Объект регулярного выражения добавляет еще один объект в контекст. Как я уже сказал, существует много ситуаций, когда re.compile () имеет свое место. Пример, приведенный ФП, не является одним из них.
PEZ

1

(несколько месяцев спустя) легко добавить свой собственный кеш вокруг 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, hit, nclear ...


1

У меня был большой опыт запуска скомпилированного регулярного выражения в 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

Всегда лучше скомпилировать свои регулярные выражения на случай, если вам потребуется их повторно использовать.

Обратите внимание, что в приведенном выше примере время имитирует создание скомпилированного объекта регулярного выражения один раз во время импорта, а не «на лету», когда это требуется для сопоставления.


1

В качестве альтернативного ответа, поскольку я вижу, что он не упоминался ранее, я приведу цитаты из документов по Python 3 :

Должны ли вы использовать эти функции уровня модуля, или вы должны получить шаблон и вызывать его методы самостоятельно? Если вы обращаетесь к регулярному выражению в цикле, его предварительная компиляция сохранит несколько вызовов функций. Вне циклов нет особой разницы благодаря внутреннему кешу.


1

Вот пример, где использование 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 (или любое другое число) регулярного выражения. шаблоны, но было бы довольно странно быть иначе :)
ShreevatsaR

0

Регулярные выражения компилируются перед использованием при использовании второй версии. Если вы собираетесь выполнять его много раз, определенно лучше сначала скомпилировать его. Если не компилировать каждый раз, когда вы подходите для одного, это хорошо.


0

Удобочитаемость / когнитивная нагрузка

Для меня главный выигрыш в том , что мне нужно только помнить, и читать, одну форму осложненного регулярных выражений синтаксис API - в <compiled_pattern>.method(xxx)форме , а не что и в re.func(<pattern>, xxx)форме.

Это re.compile(<pattern>)немного лишний шаблон, правда.

Но в том, что касается регулярных выражений, этот дополнительный шаг компиляции вряд ли будет большой причиной когнитивной нагрузки. И на самом деле, на сложных шаблонах вы могли бы даже получить ясность, отделив объявление от любого метода регулярного выражения, который затем вызываете для него.

Я склонен сначала настраивать сложные шаблоны на таком веб-сайте, как Regex101, или даже в отдельном минимальном тестовом сценарии, а затем вносить их в мой код, поэтому отделение объявления от его использования также подходит моему рабочему процессу.


-1

Я хотел бы мотивировать, что предварительная компиляция является и концептуальной, и «буквально» (как в «грамотном программировании») выгодной. взгляните на этот фрагмент кода:

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 регулярных выражений. Кроме того, регулярное выражение скрывается внутри тела метода, что в большинстве случаев не очень хорошая идея.

Следует сказать, что каждый набор мер - целевые, псевдонимы импортных заявлений; методы с псевдонимами, где это применимо; сокращение вызовов функций и поиска в словаре объектов --- может помочь уменьшить вычислительную и концептуальную сложность.


2
WTF. Не только ты откопал старый, отвеченный вопрос. Ваш код не является идиоматическим и неправильным на многих уровнях - (ab) использование классов в качестве пространств имен, где достаточно модуля, использование заглавных букв классов и т. Д. См. Pastebin.com/iTAXAWen для лучших реализаций. Не говоря уже о том, что используемое вами регулярное выражение нарушено. Всего -1

2
виновным. это старый вопрос, но я не возражаю против того, чтобы быть # 100 в замедленном разговоре. вопрос не был закрыт. Я предупреждал, что мой код может быть противником некоторых вкусов. я думаю, если бы вы могли рассматривать это как простую демонстрацию того, что выполнимо в python, например: если мы берем все, все, во что мы верим, как необязательные, а затем объединяем все, что угодно, как выглядят вещи, которые мы можем получить? Я уверен, что вы можете различить достоинства и недостатки этого решения и можете жаловаться более четко. в противном случае я должен заключить, что ваше утверждение о неправоте основано на чуть большем, чем PEP008
поток

2
Нет, дело не в PEP8. Это просто соглашения об именах, и я бы никогда не понизил голосование за их несоблюдение. Я отказался от вас, потому что код, который вы показали, просто плохо написан. Он игнорирует условные обозначения и идиомы без всякой причины и является воплощением преждевременной оптимизации: вам придется оптимизировать дневной свет из всего остального кода, чтобы это стало узким местом, и даже тогда третье предложенное мной переписывание короче, более идиоматично и так же быстро по вашим рассуждениям (такое же количество атрибутов доступа).

«плохо написано» - как, почему именно? «бросает вызов соглашениям и идиомам», - предупредил я вас. «без причины» - да, у меня есть причина: упрощать, когда сложность не имеет смысла; «воплощение преждевременной оптимизации» - я очень за стиль программирования, который выбирает баланс читабельности и эффективности; ОП попросил выявить «преимущество использования re.compile», что я понимаю как вопрос об эффективности. «(ab) использование классов в качестве пространств имен» - это ваши слова оскорбительны. класс есть, так что у вас есть «я» точка отсчета. Я пытался использовать модули для этой цели, классы работают лучше.
поток

«Использование заглавных букв в именах классов», «Нет, дело не в PEP8» - вы, очевидно, настолько безумно сердиты, что даже не можете сказать, что спорить в первую очередь. "WTF", " неправильно " --- видите, как вы эмоциональны? больше объективности и меньше пены, пожалуйста.
поток

-5

Насколько я понимаю, эти два примера фактически эквивалентны. Единственное отличие состоит в том, что в первом вы можете повторно использовать скомпилированное регулярное выражение в другом месте, не вызывая его повторную компиляцию.

Вот вам ссылка: http://diveintopython3.ep.io/refactoring.html

Вызов функции поиска объекта скомпилированного шаблона со строкой «M» выполняет то же самое, что и вызов re.search как с регулярным выражением, так и со строкой «M». Только намного, намного быстрее. (На самом деле, функция re.search просто компилирует регулярное выражение и вызывает для вас метод поиска результирующего объекта шаблона.)


1
я не отрицал вас, но технически это неправильно: Python все равно не перекомпилируется
Triptych
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.