Как стать автором
Обновить

Регулярные выражения в Python от простого к сложному. Подробности, примеры, картинки, упражнения

Время на прочтение25 мин
Количество просмотров1.5M
Всего голосов 99: ↑98 и ↓1+97
Комментарии66

Комментарии 66

Шикарная статья! Спасибо!
Мало картинок, мало примеров. И почти нет разумных задач.

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


Спасибо за проделанную работу.

Да, уже несколько дней так. Надеюсь, оживёт. Один из немногих визуализаторов, которые умеют
а) python flavor;
б) русские буквы;
Плюс там есть классная отладка

https://regex101.com/ неплох. есть python, русские буквы

Нехватает подобных статей. Часто берёшься за регулярку, один раз делаешь что-то монстрообразное, а потом через год бывает начинаешь разбираться, а уже всё забыл.
Макс, привет! :)
Описание \B «Не конец слова (либо внутри, либо вообще не в слове)». Регулярка: \Bвал Пример: перевал, вал
А разве «перевал» соответствует? Наверно «Перевалка». Или я не так понял описание?
Спасибо, поправил и расширил этот пример.
В \Bвал есть ограничение только на левый край. А на правый — нет.
Если было бы написано \Bвал\B, то да, перевал бы не подошёл, а Перевалка — подошла.
Автору огромный респект!
Всё подробно и актуально, добавил в закладки
Если адрес вводит пользователь, то пусть вводит почти что угодно, лишь бы там была собака.

После которой, где-то идет хотябы одна точка
Вообще говоря в домене может не быть ни одной точки. Конечно, никто таких адресов не использует (денег столько нету), но… То есть у кого-нибудь может быть адрес ivanoff@yandex.
(Вроде бы так, где-то про это читал, но пруфлинка пока нет)
Может использоваться локальный домен, где нет точек.
Решил я давеча моим школьникам дать задачек на регулярные выражения
Вы «регулярки» в контексте теории автоматов (регулярные грамматики, DFA/NFA…) используете?

Интересно, зачем школьникам теория автоматов для использования регулярок?

Скорее: зачем школьникам «регулярки» без теории автоматов? Для «общего развития»?

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


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


Я на ваш вопрос ответил. Теперь ответьте на мой, пожалуйста. Зачем по вашему мнению школьникам теория автоматов для использования регулярок?

для обработки текста
Вы, вероятно, подразумеваете эффективную обработку больших объёмов текста и по всей видимости, предлагаете сделать из каждого школьника как минимум (очень) эффективную секретаршу.
Зачем по вашему мнению школьникам теория автоматов для использования регулярок?
Для использования — совершенно ни к чему.
И как по всей видимости, предлагаете сделать из каждого школьника как минимум эффективную секретаршу.

Я считаю правильным в первую очередь дать школьникам удобный инструмент для решения человекопонятных задач. А когда они его освоят на уровне пользователя, тогда и рассказывать как оно работает под капотом: теорию графов, теорию автоматов, написание компиляторов и все сопутствующие дисциплины. Иначе вся эта теория будет абстрактным конем в вакууме.

Добавление? делает их анти-жадными,
они захватывают минимально возможное число символов


Может не анти-жадными, а ленивыми? Ну или хотя бы не жадными. А то уж больно глаз режет анти-жадные
К слову на википедии довольно неплохая статья про основы Регулярных выражений.
Спасибо, поправил.
Отличная статья! Большое спасибо!
Прекрасная статья! Подробно, в картинках, с пояснениями! Мне, как новичку, все прозрачно и ясно!
Спасибо за отличную статью!
Многое из описанного в статье применимо не только к питону, но и к другим языкам программирования. Bash, например.
Да, конечно. Но в JS, например, нет lookbehind и нужно ставить /.../g. Везде есть тонкости именно в использовании регулярок в языке.
Скажем, в bash я бы ре стал писать
rm <регулярка,_которая_в_питоне_делает_то,_что_нужно>

В блоке «Простые шаблоны, соответствующие позиции» рекомендовал бы разобраться с определениями «строка», «строчка», «вся строка», иначе присутствует неоднозначность. Ввести и разъяснить два определения: что такое «строка» и что такое «текст». Тогда всё становится очень просто и однозначно: это начало текста, \Z это конец текста. ^ / $ — начало/конец текста ИЛИ строки и данное поведение управляется флагом мультилайн.

Спасибо, поправил.
Правильно писать жадные (greedy), ленивые (lazy) и супержадные (Possessive).

Тема с типами квантификаторов плохо раскрыта. По какой-то причине вы в примере, которым хотели пояснить жадность квантификаторов, написали пример с ограничением позиции (в начале слова).

А пример с жадными и ленивыми квантификаторами отлично поясняется на примере вложенных шаблонов, например вложенных кавычек типа:

текст1 «текст2» текст3 «текст4»

при поиске ".*" жадных, ".*?" ленивых и ".*+" сверхжадных квантификаторов разница сразу становится понятной
А вы знаете какие-то реальные применения сверхжадных квантификаторов? Кроме попыток ускорения работы регулярок в некоторых случаях (с риском отстрелить себе ногу, если ошибся)? Про жадность/ленивость у меня пример со скобками такой же по смыслу.

Есть ещё atomic groups, (?>…), это — полезная штука, хотя немного сложная для восприятия. Может быть, добавлю.
Да, супержадные — исключительно чтобы что-то ускорить, если на бэкенде с нагрузкой используется сложная регулярка.

В остальном это синтаксический мусор, так как всегда найдется более читабельный (если можно это слово применить к регуляркам =) ) вариант.

К задаче 15:
А чем вам индийские номера в примере не угодили, что вы их сразу fail? :) не +7 же единым жив человек.

Хорошо, что мы не про реальные адреса спорим. Там такой беспредел бывает… :)
Но про локализацию номеров уточню, да.
Спасибо! Суперстатья!
Замечательная статья, спасибо!
Хочется еще посоветовать хороший тренажер для регулярок: regexcrossword.com
Вот спасибо :) Буду у нас в курсе давать основы, а за деталями — к Вам в статью отправлять. Ну и задачек пару Ваших дам, ок?
Fill free. Как бы для того и делалось.
Спасибо за статью. Но почему решили не упоминать re.match и особенно re.compile?
ИМХО, re.match — способ отстрелить себе ногу. По имени от re.search фиг отличишь, а поведение совсем другое. re.fullmatch называется понятно: полностью-соответствует.

re.compile частично упомянут в «Прочие фичи».
re.compile добавляет фичу, связанную с указанием позиций в строке, на которые нужно смотреть. Без лишнего среза. Ещё в некоторых случаях немного ускорят работу, но не сильно, так как python кеширует регулярки.

re.match и re.compile в данном контексте вступает в противоречие с куском zen of python:
There should be one-- and preferably only one --obvious way to do it.

Поэтому не стал упоминать.
Ну re.match действительно нужен только в специфических случаях, а вот re.compile при обработке больших объемов однотипных данных все же дает прирост в производительности(не смотря на кеширование), да и код за счет него выходит читабельнее.
Ускорения кот наплакал, кроме случая, когда тексты очень-очень короткие. Тогда ускорения 30%.
Берём 10 регулярок.
    r'\b[a-z]+\b',  # слова только из маленьких букв
    r'\b[A-Z]\w+\b',  # слова с заглавной
    r'\b(\w{10})\b',  # слова из 10 символов с сохранением
    r'te\w*st',  # Ищем тест
    r'a\w*b\w*c',  # a*b*c
    r'\(([^)]*)\)',  # (...) с сохранением
    r'\W{3,}',  # Длинные не-слова
    r'[aeiouy]+',  # Только гласные
    r'(?:[aeiouy][bcdfghjklmnpqrstvwxz])+',  # Читаем по слогам
    r'[\s,.!?;]+',  # Для сплит'а


Если берём 1000 текстов по 10000 символов и каждый послед. прогоняем по этим 10 regex:
100000 finditer runs total. 50.33 sec for raw VS 48.33 sec for compiled
Raw      regexp run: 0.000503 seconds per regexp, x0.960 faster
Compiled regexp run: 0.000483 seconds per regexp, x1.041 faster

Если берём 10000 текстов по 1000 символов и каждый послед. прогоняем по этим 10 regex:
1000000 finditer runs total. 50.72 sec for raw VS 50.44 sec for compiled
Raw      regexp run: 5.07e-05 seconds per regexp, x0.994 faster
Compiled regexp run: 5.04e-05 seconds per regexp, x1.006 faster

Если берём 100000 текстов по 100 символов и каждый послед. прогоняем по этим 10 regex:
10000000 finditer runs total. 89.23 sec for raw VS 74.75 sec for compiled
Raw      regexp run: 8.92e-06 seconds per regexp, x0.838 faster
Compiled regexp run: 7.47e-06 seconds per regexp, x1.194 faster

Если берём 500000 текстов по 20 символов и каждый послед. прогоняем по этим 10 regex:
15000000 finditer runs total. 76.47 sec for raw VS 56.42 sec for compiled
Raw      regexp run: 5.1e-06 seconds per regexp, x0.738 faster
Compiled regexp run: 3.76e-06 seconds per regexp, x1.355 faster


Код для тестирования
from time import perf_counter
import re
import random
from string import ascii_lowercase, ascii_uppercase
chars = ''.join(chr(i) for i in range(33, 127))
chars += ascii_uppercase * 1 + ascii_lowercase * 7
chars += ' ' * 30

NUM_RUNS = 10
NUM_TEXTS = 10000
TEXT_LENS = 1000

texts = []
for __ in range(NUM_TEXTS):
    texts.append(''.join(random.choices(chars, k=TEXT_LENS)))

regexps = [
    r'\b[a-z]+\b',  # слова только из маленьких букв
    r'\b[A-Z]\w+\b',  # слова с заглавной
    r'\b(\w{10})\b',  # слова из 10 символов с сохранением
    r'te\w*st',  # Ищем тест
    r'a\w*b\w*c',  # a*b*c
    r'\(([^)]*)\)',  # (...) с сохранением
    r'\W{3,}',  # Длинные не-слова
    r'[aeiouy]+',  # Только гласные
    r'(?:[aeiouy][bcdfghjklmnpqrstvwxz])+',  # Читаем по слогам
    r'[\s,.!?;]+',  # Для сплит'а
]

def test_raw():
    tot = 0
    st = perf_counter()
    for text in texts:
        for regex in regexps:
            tot += sum(1 for m in re.finditer(regex, text))
    en = perf_counter()
    print(f'{tot} matches found in {en-st:0.4} seconds (without compiling)')
    return en-st


def test_compiled():
    tot = 0
    st = perf_counter()
    regexps_compiled = [re.compile(r) for r in regexps]
    for text in texts:
        for regex in regexps_compiled:
            tot += sum(1 for m in regex.finditer(text))
    en = perf_counter()
    print(f'{tot} matches found in {en-st:0.4} seconds (with compiling)')
    return en-st


raw_durs = [test_raw() for __ in range(NUM_RUNS)]
compiled_durs = [test_compiled() for __ in range(NUM_RUNS)]
tot_runs = NUM_RUNS*NUM_TEXTS*len(regexps)
raw_per_regex = sum(raw_durs) / tot_runs
comp_per_regex = sum(compiled_durs) / tot_runs

print(f'{tot_runs} finditer runs total. {sum(raw_durs):.2f} sec for raw VS {sum(compiled_durs):.2f} sec for compiled')
print(f'Raw      regexp run: {raw_per_regex:.3} seconds per regexp, x{comp_per_regex/raw_per_regex:.3f} faster')
print(f'Compiled regexp run: {comp_per_regex:.3} seconds per regexp, x{raw_per_regex/comp_per_regex:.3f} faster')



Спасибо за отзыв! Времени, конечно, очень много ушло. Раза в 3 больше, чем изначально планировал…
Хорошее описание. За ссылку с тестом отдельное спасибо!
Очень давно искал толковое описание как работать с регулярными выражениями.
Огромное спасибо автору.

Хороший перевод + адаптация с примерами и иллюстрациями, отличная работа.


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


Например, re.split может добавлять тот кусок текста, по которому был разрез, в список частей. А в re.sub можно вместо шаблона для замены передать функцию. Это — реальные вещи, которые прямо очень нужны, но никто про это не пишет.

когда именно это описано в оффициальной документации в первых двух предложениях для этих функций

Ну, непосредственного перевода в статье примерно нет. Всё, кроме нескольких предложений, писалось «своими словами».
И да, в статье вообще нет ничего из «теории» такого, чего нет в документации. Документация у питона весьма приличная. И на английском вообще есть суперские ресурсы: www.regular-expressions.info и www.rexegg.com. На последнем так вообще есть такие штуки, что ого-го.

Но мне нужен был понятный последовательный cookbook с привязкой к питону на русском языке, в котором есть все «нужные» штуки.
Отличная статья, большое спасибо!
Извините, не сочтите за наглость, а вы не думали выложить ее в PDF (раз уж у вас есть опыт перегонки из обычного html в html хабра))?

Ну, вообще можно и pdf сделать. Правда теперь мне нужно перелить часть изменений из хабра в оригинальный html. Ещё от коллег была «заявка» на упрощение введения для тех, кто совсем не в теме.
Буду очень признателен!
А да, кмк лучше подождать пару дней — за это время добавятся комментарии/исправления/пожелания
Вот бы кто про рекурсивные шаблоны подробно рассказал, было бы очень здорово
А что такое рекурсивные шаблоны?
Ну… как бы это сказать. Это шаблоны поиска, которые ищутся рекурсивно. У PCRE это реализуется через модификатор (?R). Классическая задача — поиск содержимого внутри неограниченного количества некоторых парных символов, например скобок — например найти в строке {{{aaa}}} подстроку 'aaa'. Без рекурсии это сделать невозможно, насколько я помню. Первое что кажется подходящим [{]+[^}]+[}]+ — не подходит, т.к. здесь не будет соблюдаться требование парности открывающих и закрывающих скобок и шаблон совпадет со строками {aаа}}}, {{{{{{{{aаа}} и т.п. Рекурсивный шаблон решающий задачу выглядит так — {((?>[^{}]+)|(?R))*}

Тема непростая для понимания и не особо освещенная в рунете, хотя рекурсивные шаблоны появились в PCRE аж в 2000 году.

Интересная статейка на тему — www.rexegg.com/regex-recursion.html (поиск палиндромов прекрасный пример применения)
Скудная документация — perldoc.perl.org/perlretut.html#Recursive-patterns
А где можно увидеть все ответы на задачи?

Есть ли способ визуально разделить группы поиска?

# NEED MATCH IN THIS CASE
import re
print(re.fullmatch(r"\d\.   \d{2}\.    \d{3}", "1.12.123"))

мои мысли:

  1. должен быть флаг, позволяющий интерпретировать пробел только через \s или [ ], но такого нет

  2. можно конечно использовать конструкцию с квантификатором "( ){0}" но это забивает/усложняет сам паттерн(((

Визуально разделить группы можно. Вот пример из fractions.py:
_RATIONAL_FORMAT = re.compile(r"""
    \A\s*                      # optional whitespace at the start, then
    (?P<sign>[-+]?)            # an optional sign, then
    (?=\d|\.\d)                # lookahead for digit or .digit
    (?P<num>\d*)               # numerator (possibly empty)
    (?:                        # followed by
       (?:/(?P<denom>\d+))?    # an optional denominator
    |                          # or
       (?:\.(?P<decimal>\d*))? # an optional fractional part
       (?:E(?P<exp>[-+]?\d+))? # and optional exponent
    )
    \s*\Z                      # and optional whitespace to finish
""", re.VERBOSE | re.IGNORECASE)

m = _RATIONAL_FORMAT.match(numerator)
numerator = int(m.group('num') or '0')
denom = m.group('denom')
decimal = m.group('decimal')
exp = m.group('exp')

Тем самым как шаблон для регулярки '\\\\par' означает просто текст \par

Кажется, что тут подразумевалось \\par

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории