Чи варто використовувати re.compile Python?


461

Чи є якась користь у використанні компіляції для регулярних виразів у 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
МНОГО

@ShreevatsaR Цікаво! Чи можете ви опублікувати відповідь на прикладі, який показує поліпшення 10x-50x? Більшість наведених тут відповідей насправді показують 3-кратне покращення в деяких точних випадках, а в інших майже не покращення.
Бась

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

Відповіді:


436

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

EDIT: Після швидкого огляду фактичного бібліотечного коду Python 2.5, я бачу, що Python внутрішньо компілює і CACHES регулярно виражає всякий раз, коли ви їх використовуєте в будь-якому випадку (включаючи дзвінки re.match()), тому ви дійсно змінюєте лише КОГО регекс збирається, і не слід ' t взагалі не економить багато часу - лише час, необхідний для перевірки кеша (пошук ключа у внутрішньому 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
JF Себастьян, це служить сигналом програмісту, що регенекп, про який йде мова, буде використовуватися багато і не призначений для відкидання.
калейсін

40
Більше того, я хотів би сказати, що якщо ви не хочете зазнавати, що компіляція та кеш-пам'ять потрапляють у критичну частину вашої програми, вам краще скомпілювати їх перед подачею у некритичну частину вашої програми. .
Едді Паркер

20
Я бачу головну перевагу використання компільованого регулярного виразу, якщо ви повторно використовуєте один і той же регулярний вираз кілька разів, тим самим зменшуючи можливість помилкових помилок. Якщо ви просто зателефонували йому один раз, тоді некомпільований - це читабельніше.
monkut

18
Отже, головна відмінність полягатиме в тому, що ви використовуєте безліч різних регулярних виразів (більше ніж _MAXCACHE), деякі з них лише один раз, а інші багато разів ... тоді важливо зберегти складені вирази для тих, хто більше використовується, щоб вони Ви не видаляєте з кешу, коли він заповнений.
фортран

133

Для мене найбільша користь 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)

Versus компіляція:

num = re.compile("...")
# then, much later:
m = num.match(input)

Хоча він досить близький, останній рядок другого відчуває себе більш природним та простим, коли використовується багаторазово.


5
Я згоден з цією відповіддю; Часто використання re.compile призводить до більш не менш читаного коду.
Карл Мейєр

1
Іноді, правда, навпаки - наприклад, якщо ви визначаєте регулярне вираження в одному місці і використовуєте його групи, що відповідають, в іншому далекому місці.
Кен Вільямс

1
@KenWilliams Не обов'язково, добре названий регулярний вираз для конкретних цілей повинен бути зрозумілим, навіть якщо він використовується далеко від початкового визначення. Наприклад us_phone_numberабо social_security_numberін.
Брайан М. Шелдон

2
@ BrianM.Sheldon, називаючи регекс добре, насправді не допомагає вам знати, що представляють його різні групи захоплення.
Кен Вільямс

68

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
Я бачу, що ви маєте на увазі, але чи не це саме те, що трапиться в реальній програмі, де регулярно використовується регулярний вираз?
dF.

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

3
@Triptych Чи import reпотрібно перенести налаштування? Вся справа в тому, де ви хочете виміряти. Якщо я запускатимуть сценарій пітона багато разів, це би import reчас потрапило. При порівнянні двох важливо відокремити два рядки для визначення часу. Так, як ви кажете, це коли вас чекає час. Порівняння показує, що ви берете один раз за часом та повторюєте менший час звернення, компілюючи, або ви приймаєте хіт щоразу, припускаючи, що кеш буде очищений між викликами, що, як було зазначено, може статися. Додавання хронометражу h=re.compile('hello')допоможе уточнити.
Tom Myddeltyn

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).
Zitrax

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

Методи 'дійсно компільовані' використовують внутрішній інтерфейс, який обходить кеш. Зауважте, що компіляція на кожній ітерації циклу лише повторюється в 10000 разів, а не один мільйон.


Я згоден з вами, що компільовані регулярні вирази працюють набагато швидше, ніж некомпільовані. Я пробігаю понад 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')

і (С) насправді:

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 і якось очиститься. Але наш код зберігає посилання на нього, і об'єкт регулярного вираження не буде звільнений із пам'яті. Ті, Python не потрібно збирати знову.

  4. 2-секундні відмінності в тесті Джорджа, складеномуInLoop vs компільованим, - це головним чином час, необхідний для складання ключа та пошуку _cache. Це не означає час компіляції регулярного вираження.

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

Ось лише випадки, коли (A + B) кращий, ніж (C):

  1. Якщо ми можемо кешувати посилання на об'єкт регулярного вираження всередині класу.
  2. Якщо нам потрібно дзвонити (B) неодноразово (всередині циклу або кілька разів), ми повинні кешувати посилання на об'єкт регулярного виразів поза циклом.

Випадок, що (C) досить хороший:

  1. Ми не можемо кешувати посилання.
  2. Ми використовуємо його лише раз у раз.
  3. Загалом у нас не так вже й багато регексу (припустимо, що складений ніколи не розмивається)

Тільки резюме, ось ABC:

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коштує дорого, і шаблон ніколи не записується в кеш, тому пошук щоразу завершується помилкою 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занадто.
адерхокс

5

Використовуючи наведені приклади:

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

Метод відповідності у наведеному вище прикладі не такий, як той, що використовується нижче:

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

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

Об'єкт regex має власний метод відповідності з необов'язковими параметрами pos і endpos :

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

поз

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

кінцеві

Необов’язковий параметр endpos обмежує, наскільки далеко буде шуканий рядок; це буде як якщо рядок endpos символи, тому тільки персонажі з поз в endpos - 1будуть шукати збіги. Якщо endpos менше, ніж pos , не буде знайдено відповідності; в іншому випадку, якщо rx - компільований об'єкт регулярного виразу, rx.search(string, 0, 50)еквівалентний rx.search(string[:50], 0).

Регулярний вираз об'єкта пошуку , FindAll , і finditer методи також підтримують ці параметри.

re.match(pattern, string, flags=0)не підтримує їх , як ви можете бачити,
ні робить його пошук , FindAll і finditer аналоги.

Об'єкт сірники має атрибути , які доповнюють ці параметри:

match.pos

Значення pos, яке було передане методу search () або match () об'єкта регулярного вираження. Це індекс у рядку, з якого двигун RE почав шукати відповідність.

match.endpos

Значення endpos, яке було передано методу search () або match () об'єкта регулярного вираження. Це індекс у рядку, за яким двигун 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, який входить у стандартний дистрибутив.
jemfinch

Скільки часу ви відтворюєте рядок візерунка в циклі for. Цей наклад не може бути тривіальним.
IceArdor

3

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

Я вкрав і знущався з прикладу у творі Джеффа Фрідля «Оволодіння регулярними виразами». Це на macbook під керуванням OSX 10.6 (2Ghz intel core 2 duo, 4GB ram). Версія 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

3

Ця відповідь може надходити пізно, але це цікава інформація. Використання компіляції може дійсно заощадити ваш час, якщо ви плануєте використовувати регулярний вираз кілька разів (це також зазначено в документах). Нижче ви бачите, що використання компільованого регулярного виразу є найшвидшим, коли метод відповідності безпосередньо на нього викликається. передача компільованого регулярного виразу в 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. модуль (re) ,
2. об'єкт regex
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',]
}

2

Я дуже поважаю всі наведені вище відповіді. З моєї думки Так! Напевно, варто використовувати re.compile замість того, щоб компілювати регулярний вираз, кожного разу.

Використання re.compile робить ваш код більш динамічним, тому що ви можете назвати вже складений регулярний вимір, замість того, щоб знову компілювати і знову. Ця річ приносить вам користь у випадках:

  1. Зусилля процесора
  2. Часова складність.
  3. Робить регулярний генекс (можна використовувати в пошуку, пошуку, збігу)
  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 в Nutshell не згадує використання без re.compile (), що викликало цікавість.
Мат

Об'єкт regex додає ще один об'єкт у контекст. Як я вже сказав, існує багато ситуацій, коли re.compile () має своє місце. Приклад, поданий ОП, не є одним із них.
ПЕЗ

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 () -> розмір, хіти, 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

Завжди краще зібрати свої регулярні вирази, якщо вам потрібно буде їх повторно використовувати.

Зверніть увагу на приклад у вищевикладеному timeit імітує створення скомпільованого об'єкта регулярних виразів одночасно під час імпорту порівняно з "на льоту", коли це потрібно для відповідності.


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 шаблони регулярних виразів у всьому своєму коді, кожен з них сотні разів використовує (без конкретного замовлення), кеш regex автоматично зберігатиме попередньо складений регулярний вираз, чи не так?
Бась

@Basj Я думаю, ви могли б просто спробувати і побачити :) Але відповідь, я впевнений, так: єдина додаткова вартість у такому випадку AFAICT - це лише просте пошуку шаблону в кеш-пам'яті . Зауважте також, що кеш-пам'ять є глобальною (на рівні модуля), тому в принципі ви можете мати деяку бібліотеку залежностей, яка здійснює пошук регулярного вибору між вашими, тому важко бути повністю впевненим, що ваша програма використовує лише 3 (або будь-яку кількість) регулярних виразів шаблонів, але було б досить дивно, якщо це інакше :)
ShreevatsaR

0

Регулярні вирази складаються перед використанням при використанні другої версії. Якщо ви збираєтесь його виконувати багато разів, то попередньо краще скомпілювати його. Якщо не збирати кожен раз, коли ви співпадаєте за виграш, це добре.


0

Розбірливість / перевага когнітивного навантаження

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

The 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' виконує те саме, що і виклик повторного пошуку як з регулярним виразом, так і з рядком 'M'. Тільки набагато, набагато швидше. (Насправді функція re.search просто компілює регулярний вираз і викликає метод пошуку об’єкта шаблону пошуку для вас.)


1
Я вас не оскаржував, але технічно це неправильно: Python все одно не буде перекомпілювати
Триптих
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.