Флаги и проверки в регулярных выражениях

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

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