Стоит ли использовать Python re.compile? - PullRequest
391 голосов
/ 17 января 2009

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

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

против

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

Ответы [ 24 ]

382 голосов
/ 17 января 2009

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

EDIT: После быстрого взгляда на реальный код библиотеки 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

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

114 голосов
/ 17 января 2009

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

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

54 голосов
/ 17 января 2009

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

39 голосов
/ 30 ноября 2012

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

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

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

14 голосов
/ 14 апреля 2010

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

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

Однако проверка кода показывает, что кэш ограничен 100 выражениями. Возникает вопрос: насколько больно переполнять кеш? Код содержит внутренний интерфейс для компилятора регулярных выражений re.sre_compile.compile. Если мы это называем, мы обходим кеш. Оказывается, что оно на два порядка медленнее для основного регулярного выражения, такого как r'\w+\s+([0-9_]+)\s+\w*'.

Вот мой тест:

<code>#!/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
А вот и вывод на мою машину:
$ 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
</code>

Методы «на самом деле скомпилированные» используют внутренний интерфейс, который обходит кеш. Обратите внимание, что тот, который компилируется на каждой итерации цикла, повторяется только 10 000 раз, а не один миллион.

10 голосов
/ 29 июля 2014

Я согласен с Честным Абэ, что 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. Вызовы Один раз он выполнит один поиск в _cache, а затем один sre_compile.compile(), чтобы создать объект регулярного выражения. Вызов A дважды, он выполнит два поиска и одну компиляцию (потому что объект регулярного выражения кэшируется).

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

  3. Если мы сохраним объект регулярного выражения с помощью (A), объект регулярного выражения все равно попадет в _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. В целом, у нас не так уж много регулярных выражений (предположим, что скомпилированное никогда не сбрасывается)

Просто подведем итоги, вот A B C:

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

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

7 голосов
/ 21 февраля 2017

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

В целом я считаю, что легче использовать флаги (по крайней мере, легче запомнить, как), например 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']
4 голосов
/ 20 марта 2015

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

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

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

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

4 голосов
/ 11 марта 2013

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

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

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

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

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

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

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

* пос 1032 *

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

endpos

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

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

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

A совпадающий объект имеет атрибуты, которые дополняют следующие параметры:

match.pos

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

match.endpos

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


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

regex.groups

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

regex.groupindex

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


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

match.re

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...