Рассматриваемые
ранее примеры регулярных выражений имеют один существенный недостаток: они
способны находить соответствия там, где это не предполагалось. Например,
выражение вида:
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
|
Конец
текста
|
(?=exp)
|
Проверка
на совпадение с выражением exp продолжения строки. При этом позиция
поиска не смещается на выражение exp (опережающая
проверка).
|
(?!exp)
|
Проверка
на несовпадение с выражением exp продолжения строки. (Также
опережающая проверка).
|
(?<=exp)
|
Проверка
на совпадение с выражением exp хвоста уже обработанной (проверенной)
строки. Она также называется позитивной ретроспективной проверкой.
|
(?<!exp)
|
Проверка
на несовпадение с выражением exp хвоста уже обработанной (проверенной)
строки. Еще она называется негативной ретроспективной проверкой.
|
Рассмотрим
примеры использования этих проверок. Предположим, у нас имеется вот такой
многострочный текст:
text = """<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Уроки по Python</title>
</head>
<body>
<script type="text/javascript">
let o = document.getElementById('id_div');
console.log(obj);
</script>
</body>
</html>"""
И мы хотим
выделить содержимое тега script. Запишем регулярное выражение в
таком виде:
match = re.findall(r"^<script.*?>([\w\W]+)(?=</script>)", text, re.MULTILINE)
Здесь сначала
проверяется, что тег script записан вначале строки (флаг MULTILINE указывает,
чтобы проверка ^ соответствовала началу каждой строки). Далее, проверяется
запись тега script: он может быть
с параметрами, а может идти и без параметров. Затем, выбираются все символы
(любые), пока не встретится закрывающий тег script. В результате,
получим:
["\nlet o =
document.getElementById('id_div');\nconsole.log(obj);\n"]
Здесь может
возникнуть вопрос: почему мы используем именно такой символьный класс [\w\W] для выбора
всех символов? Почему бы здесь не использовать точку, которая соответствует
любому символу. Дело в том, что точка соответствует любому символу, но не
символу перевода строки \n. Поэтому вот такое выражение:
match = re.findall(r"^<script.*?>(.+)(?=</script>)", text, re.MULTILINE)
даст пустую
коллекцию, т.к. символ \n встречается сразу же после тега script.
Теперь посмотрим
на первый символ ^ - начало строки. Если поставить хотя бы один пробел перед
открывающим тегом script, то шаблон не сработает и получим
пустую коллекцию. Это, как раз, из-за этой первой проверки.
Далее, если
убрать проверку (?=</script>), то будут выбраны все
символы до конца текста:
["\nlet o =
document.getElementById('id_div');\nconsole.log(obj);\n</script>\n</body>\n</html>"]
Вернем ее и
смотрите, эта проверка опережающая и закрывающий тег не входит в результат
обработки. А
вот если написать так:
match = re.findall(r"^<script.*?>([\w\W]+)(?<=</script>)", text, re.MULTILINE)
то тег </script> будет
находиться в итоговой строке:
["\nlet
o =
document.getElementById('id_div');\nconsole.log(obj);\n</script>"]
По аналогии
работают и обратные проверки:
(?!exp) (?<!exp)
Предположим, мы
хотим выбрать все пары:
атрибут=значение
Это можно
сделать с помощью такого правила:
match = re.findall(r"([-\w]+)[ \t]*=[ \t]*[\"']([^\"']+)(?<![ \t])", text, re.MULTILINE)
Первая скобка
выделяет имя атрибута, после него должно идти равно (с пробелами или
табуляциями или без них), далее, кавычка (двойная или одинарная) и в кавычках
мы выбираем все символы не кавычки. В конце стоит негативная ретроспективная
проверка на отсутствие в конце проверяемой строки пробела или табуляции. Она
будет отсекать эти символы, если они будут идти непосредственно перед
закрывающей кавычкой. Результат работы данного правила, следующий:
[('http-equiv',
'Content-Type'), ('content', 'text/html; charset=windows-1251'), ('name',
'viewport'), ('content', 'width=device-width, initial-scale=1.0'), ('type',
'text/javascript')]
Как видите,
получили список кортежей для пар ключ=значение. Мало того, если перед
закрывающей кавычкой у значения поставить пробелы или табуляции, то они будут
проигнорированы.
Но здесь есть
тонкий момент. Если поставить символы кавычек после ретроспективной проверки:
match = re.findall(r"([-\w]+)[ \t]*=[ \t]*[\"'](.+?)(?<![ \t])[\"']", text, re.MULTILINE)
то мы получим не то, что ожидаем:
[('http-equiv',
'Content-Type " content='), ('name', 'viewport'), ('content',
'width=device-width, initial-scale=1.0'), ('type', 'text/javascript')]
У нас второе
значение будет захватывать следующую пару ключ=значение, так как мы здесь
попросту нарушили шаблон. В первом случае правило могло не учитывать последние
пробелы и завершить шаблон при закрывающей кавычке, т.к. мы явно не требовали
ее наличия в конце. Здесь же они явно указаны и при остановке на пробелах
кавычек уже не будет в конце и шаблон становится некорректным.
Подобные моменты
особенно часто встречаются на начальном этапе использования регулярных
выражений. В них нужно стараться вникать, разбираться и в последующем не
совершать похожих ошибок. И, как всегда, здесь все приходит с опытом. Благо на
просторах Интернета много примеров использования регулярных выражений и на всем
этом богатстве материала можно быстро освоить хотя бы начальный минимум.
Давайте теперь
немного усложним задачу и определим правило для выделения пар:
ключ="значение"
или ключ=значение
Например, в
тексте ниже есть ключ align=center, значение
которого записано без кавычек.
text = """<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type " content="text/html; charset=windows-1251">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Уроки по Python</title>
</head>
<body>
<p align=center>Hello World!</p>
</body>
</html>"""
Для решения
данной задачи воспользуемся еще одним типом проверки. В Python можно проверять
наличие группирующего выражения, например:
(?P<q>[\"'])
Мы здесь находим
и сохраняем кавычки с именем группы q. Затем, в зависимости от наличия
или отсутствия этой группы, выполнять заданный шаблон. Для этого используется
такой синтаксис:
(?(id|name)yes_pattern)
или так:
(?(id|name)yes_pattern|no_pattern)
Здесь yes_pattern – шаблон,
выполняемый при наличии группы; no_pattern – шаблон,
выполняемый при отсутствии группы.
Возвращаясь к
нашей задаче, определим такое регулярное выражение:
match = re.findall(r"([-\w]+)[ \t]*=[ \t]*(?P<q>[\"'])?(?(q)([^\"']+(?<![ \t]))|([^ \t>]+))", text, re.MULTILINE)
Смотрите, мы
здесь проверяем: если кавычки присутствуют, то выполняем прежний шаблон, а
иначе, новый с выбором всех символов, пока не встретится пробел, табуляция или
закрывающаяся скобка. При
запуске увидим следующий результат:
[('http-equiv',
'"', 'Content-Type', ''), ('content', '"', 'text/html;
charset=windows-1251', ''), ('name', '"', 'viewport', ''), ('content',
'"', 'width=device-width, initial-scale=1.0', ''), ('align', '', '',
'center')]
У нас здесь был
успешно выделен атрибут align со значением center благодаря
использованию проверки группы с кавычками.
В заключение
этого занятия я приведу набор флагов, которые можно назначать к регулярным
выражениям:
Флаг
|
Описание
|
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
|
Включает
режим отладки при компиляции регулярного выражения
|
Все эти флаги
достаточно очевидны, кроме, может быть, последнего. Давайте воспользуемся им и
запишем наше последнее правило с набором комментариев:
match = re.findall(r"""([-\w]+) #выделяем атрибут
[ \t]*=[ \t]* #далее, должно идти равно и кавычки
(?P<q>[\"'])? #проверяем наличие кавычки
(?(q)([^\"']+(?<![ \t]))|([^ \t>]+)) #выделяем значение атрибута
""",
text, re.MULTILINE|re.VERBOSE)
Смотрите, мы
здесь записали правило в несколько строк, добавили комментарии и объединили два
флага: MULTILINE и VERBOSE, используя
операцию логического ИЛИ.
Флаги можно
указывать и непосредственно внутри выражения, используя синтаксис:
(?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']
Вот так
реализуются проверки и флаги в регулярных выражениях на Python. И мы с вами
охватили весь материал по основам построения регулярных выражений. Используя
эти знания, можно создавать правила для обработки строк в самых разных задачах.
Но как это реализовать, используя методы модуля re, мы с вами
подробно поговорим на следующем занятии.