Опережающие (lookahead) проверки. Флаги

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

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

import re
 
text = "подоходный налог"
match = re.findall(r"прибыль|обретение|доход", text)
print(match)

найдет подстроку «доход» в слове «подоходный». Подобные моменты как раз и решаются с помощью проверок. В данном случае для выделения слова «доход» целиком можно воспользоваться проверкой \b – граница слова. Фактически, это набор небуквенных и нецифровых символов. Запишем регулярное выражение в виде:

match = re.findall(r"прибыль|обретение|\bдоход\b", text)

Теперь слово «подоходный» пропущено и на выходе получаем пустую коллекцию. Но, если добавим в текст это слово:

text = "подоходный налог, доход"

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

match = re.findall(r"\bприбыль\b|\bобретение\b|\bдоход\b", text)

или проще, с помощью группировки вариантов:

match = re.findall(r"\b(?:прибыль|обретение|доход)\b", text)

Во втором случае мы используем несохраняющую группировку и для найденных совпадений выполняем проверку на соответствие границы слова.

Обратите внимание, проверки не являются частью совпадения строки по шаблону, они лишь проверяют определенные условия, поэтому сам по себе символ \b в строке text не ищется, а определяется граница слова в шаблоне, где он записан.

В общем случае, для регулярных выражений доступны следующие проверки:

Символ

Описание

^

Начало текста (с флагом re.MULTILINE – начало строки)

$

Конец текста (с флагом re.MULTILINE – позиция перед символом переноса строки \n)

\A

Начало текста

\b

Граница слова (внутри символьных классов [] соответствует символу BACKSPACE)

\B

Граница не слова (зависим от флага re.ASCII)

\Z

Конец текста

A(?=B)

Позитивная опережающая проверка (positive lookahead). Находит такое текущее совпадение A, за которым обязательно следует шаблон B.

A(?!B)

Негативная опережающая проверка (negative lookahead). Находит такое текущее совпадение A, за которым не следует шаблон B.

(?<=B)A

Позитивная ретроспективная проверка (positive lookbehind). Находит такое текущее совпадение A, перед которым обязательно следует шаблон B.

(?<!B)A

Негативная ретроспективная проверка (negative lookbehind). Находит такое текущее совпадение A, перед которым не следует шаблон B.

С простыми символами, в целом, все понятно, а вот опережающие (lookahead) и ретроспективные (lookbehind) проверки требуют отдельного рассмотрения. Начнем с опережающих проверок.

Пусть, например, из строки вида:

msg = "5 ручек стоит 450 руб."

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

match = re.findall(r"\d+(?=[ ]*руб[.])", msg)
print(match)

Увидим в консоли результат:

['450']

Обратите внимание, что опережающие проверки (?=…) и (?!...) не включаются в итоговый результат, и мы выделяем только число 450. Также текущая позиция обработки текста будет находиться на пробеле после числа 450. Поэтому, если после опережающей проверки в шаблоне выбрать все оставшиеся символы до конца строки:

match = re.findall(r"\d+(?=[ ]*руб[.]).*", msg)

то в итоговом результате увидим:

['450 руб.']

Этот шаблон можно расширить на разные слова, записанные после какого-либо числа. Например, так:

msg = "5 ручек стоит 450 рублей"
 
match = re.findall(r"\d+(?=[ ]*(?:руб[.]|рубл[яи]|рублей|рублями))", msg)
print(match)

Теперь после цены можно прописывать разные слова: руб., рубля, рубли, рублей, рублями.

Давайте теперь воспользуемся противоположной проверкой и для примера выделим все email вне зоны ru:

emails = ["test@example.ser_bal.com", "info@test.ru", "admin@domain.org"]

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

for email in emails:
    match = re.findall(r"[\w\W]+@[\w\W]+[.](?!ru)", email)
    if match:
        print(match)

Мы здесь не проверяем корректность email-адресов, а лишь оставляем те, что не заканчиваются на «ru». Результатом будет вывод:

['test@example.ser_bal.']
['admin@domain.']

Действительно остались только нужные email, но без окончаний. Мы уже знаем, как это можно поправить. После опережающей проверки выберем все оставшиеся символы. Получим шаблон:

match = re.findall(r"[\w\W]+@[\w\W]+[.](?!ru).*", email)

со следующим результатом его работы:

['test@example.ser_bal.com']
['admin@domain.org']

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

for email in emails:
    if not email.endswith(".ru"):
        print(email)

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

Давайте рассмотрим несколько более сложный пример. Пусть имеется текст, из которого нужно выделить email-адреса вне зоны «ru»:

text = '''
Напишите Сергею на email: sergey.balakirev@bagoogle.com. Возможно, он ответит вам,
если ваш email guest89@mail.ru! Но для надежности связи лучше укажите
альтернативный admin.test@gmail.com в зоне com.
С наилучшими пожеланиями, ваш верный друг!
'''

Шаблон можно прописать следующим образом:

match = re.findall(r"\b[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+[.](?!ru)[a-zA-Z]{2,}\b", text, re.MULTILINE)
print(match)

Здесь мы в пределах границ слова «\b» выделяем сначала фрагмент до символа «@», затем фрагмент после символа «@» до последней точки. И если после нее не записано «ru», то фрагмент считается верным и продолжается выделение домена шаблоном [a-zA-Z]{2,}. Получаем следующий результат выделения email-адресов:

['sergey.balakirev@bagoogle.com', 'admin.test@gmail.com']

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

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

match = re.findall(r"\b[a-zA-Z0-9._-]+"     # выделение фрагмента до символа @ 
                   r"@"                     # символ @
                   r"[a-zA-Z0-9._-]+"       # выделение фрагмента после символа @
                   r"[.]"                   # проверка наличия последней точки
                   r"(?!ru)"                # после не должно быть фрагмента "ru"
                   r"[a-zA-Z]{2,}\b",       # выделение доменной зоны
                   text, re.MULTILINE)      # флаг обработки многострочного текста

Или можно записать, как обычную многострочную строку с указанием дополнительного флага re.VERBOSE:

match = re.findall(r'''\b[a-zA-Z0-9._-]+     # выделение фрагмента до символа @ 
                   @                         # символ @
                   [a-zA-Z0-9._-]+           # выделение фрагмента после символа @
                   [.]                       # проверка наличия последней точки
                   (?!ru)                    # после не должно быть фрагмента "ru"
                   [a-zA-Z]{2,}\b            # выделение доменной зоны
                   ''',
                   text, re.MULTILINE|re.VERBOSE)      # флаг обработки многострочного текста

Флаги регулярных выражений

Вообще регулярные выражения модуля re поддерживают следующие флаги:

Флаг

Описание

re.A или re.ASCII

При этом флаге проверки \b, \B, \s, \S, \w и \W действуют так, как если бы они применялись к тексту, содержащему только символы ASCII (по умолчанию используется Юникод re.U / re.UNICODE и лучше оставаться в этом режиме)

re.I или re.IGNORECASE

Проверка без учета регистра символов

re.M или

re.MULTILINE

Влияет на проверки ^ и $. Начало ^ считается началом строки (сразу после символа \n или начало текста). Конец $ считается в позиции перед \n (или конец строки)

re.S или

re.DOTALL

При установке этого флага символ . также включает символ перевода строки \n.

re.X или

re.VERBOSE

Позволяет включать в регулярные выражения пробелы и комментарии

re.DEBUG

Включает режим отладки при компиляции регулярного выражения

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

(?flags),

где flags – один или несколько флагов. Причем, их имена, следующие:

  • a – то же самое, что и re.ASCII;
  • i – соответствует re.IGNORECASE;
  • m – для re.MULTILINE;
  • s – для re.DOTALL;
  • x – для re.VERBOSE.

и записываются в самом начале, например, так:

text = "Python, python, PYTHON"
match = re.findall(r"(?im)python", text)
print(match)

Мы здесь включили два флага: re.IGNORECASE и re.MULTILINE. Благодаря первому, в строке находятся все три совпадения со словом python:

['Python', 'python', 'PYTHON']

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

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме