Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs
Рассматриваемые
ранее примеры регулярных выражений имеют один существенный недостаток: они
способны находить соответствия там, где это не предполагалось. Например,
выражение вида:
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