HOWTO по регулярным выражениям

Автор:

А.М. Кючлинг <amk@amk.ca>

Введение

Регулярные выражения (называемые RE, или regex, или regex patterns) - это, по сути, крошечный, узкоспециализированный язык программирования, встроенный в Python и доступный через модуль re. Используя этот маленький язык, вы задаете правила для набора возможных строк, которые вы хотите сопоставить; этот набор может содержать английские предложения, или адреса электронной почты, или команды TeX, или все, что вам угодно. Затем вы можете задавать вопросы типа «Соответствует ли эта строка шаблону?» или «Есть ли где-нибудь в этой строке совпадение с шаблоном?». Также с помощью RE можно изменять строку или разбивать ее на части различными способами.

Шаблоны регулярных выражений компилируются в серию байткодов, которые затем выполняются механизмом согласования, написанным на языке C. Для продвинутого использования может потребоваться обратить пристальное внимание на то, как механизм будет выполнять данный RE, и написать RE определенным образом, чтобы получить байткод, который выполняется быстрее. Оптимизация не рассматривается в этом документе, поскольку для этого необходимо хорошо понимать внутреннее устройство механизма согласования.

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

Простые узоры

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

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

Сопоставление символов

Большинство букв и символов будут просто совпадать сами с собой. Например, регулярное выражение test будет точно соответствовать строке test. (Вы можете включить режим, не учитывающий регистр, который позволит этому RE также соответствовать Test или TEST; подробнее об этом позже).

Из этого правила есть исключения: некоторые символы являются специальными metacharacters и сами по себе не совпадают. Вместо этого они сигнализируют о том, что нужно сопоставить что-то необычное, или влияют на другие части RE, повторяя их или изменяя их значение. Большая часть этого документа посвящена обсуждению различных метасимволов и того, что они делают.

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

. ^ $ * + ? { } [ ] \ | ( )

Первые метасимволы, которые мы рассмотрим, - это [ и ]. Они используются для указания класса символов, то есть набора символов, которые вы хотите сопоставить. Символы можно перечислять по отдельности, а можно указать диапазон символов, указав два символа и разделив их символом '-'. Например, [abc] будет соответствовать любому из символов a, b или c; это то же самое, что и [a-c], в котором используется диапазон для выражения того же набора символов. Если вы хотите, чтобы совпадали только строчные буквы, то RE будет [a-z].

Метасимволы (кроме \) не активны внутри классов. Например, [akm$] будет соответствовать любому из символов 'a', 'k', 'm' или '$'; '$' обычно является метасимволом, но внутри класса символов он лишен своей особой природы.

Вы можете подобрать символы, не перечисленные в классе, с помощью complementing набора. Это обозначается включением '^' в качестве первого символа класса. Например, [^5] будет соответствовать любому символу, кроме '5'. Если каретка появляется в другом месте класса символов, она не имеет специального значения. Например: [5^] будет соответствовать либо '5', либо '^'.

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

Некоторые специальные последовательности, начинающиеся с '\', представляют собой предопределенные наборы символов, которые часто бывают полезны, например, набор цифр, набор букв или набор всего, что не является пробелами.

Рассмотрим пример: \w соответствует любому буквенно-цифровому символу. Если шаблон regex выражен в байтах, это эквивалентно классу [a-zA-Z0-9_]. Если regex-шаблон представляет собой строку, то \w будет соответствовать всем символам, помеченным как буквы в базе данных Unicode, предоставляемой модулем unicodedata. Вы можете использовать более ограниченное определение \w в строковом шаблоне, установив флаг re.ASCII при компиляции регулярного выражения.

Приведенный ниже список специальных последовательностей не является полным. Полный список последовательностей и расширенные определения классов для строковых шаблонов Unicode см. в последней части Regular Expression Syntax в справочнике по стандартной библиотеке. В целом, версии Unicode соответствуют любому символу, который находится в соответствующей категории в базе данных Unicode.

\d

Сопоставляет любую десятичную цифру; это эквивалентно классу [0-9].

\D

Сопоставляет любой нецифровой символ; это эквивалентно классу [^0-9].

\s

Сопоставляет любой пробельный символ; это эквивалентно классу [ \t\n\r\f\v].

\S

Сопоставляет любой символ, не являющийся пробелом; это эквивалентно классу [^ \t\n\r\f\v].

\w

Сопоставляет любой буквенно-цифровой символ; это эквивалентно классу [a-zA-Z0-9_].

\W

Сопоставляет любой не буквенно-цифровой символ; это эквивалентно классу [^a-zA-Z0-9_].

Эти последовательности могут быть включены в символьный класс. Например, [\s,.] - это класс символов, который будет соответствовать любому пробельному символу, или ',', или '.'.

Последний метасимвол в этом разделе - .. Он соответствует любому символу, кроме символа новой строки, и есть альтернативный режим (re.DOTALL), в котором он соответствует даже новой строке. . часто используется в тех случаях, когда нужно сопоставить «любой символ».

Повторяющиеся вещи

Возможность сопоставлять различные наборы символов - это первое, что могут регулярные выражения, чего нельзя сделать с помощью методов, доступных для строк. Однако если бы это была единственная дополнительная возможность регексов, они не были бы большим достижением. Другая возможность заключается в том, что вы можете указать, что части RE должны повторяться определенное количество раз.

Первый метасимвол для повторения, который мы рассмотрим, - это *. * не совпадает с литеральным символом '*'; вместо этого он указывает, что предыдущий символ может быть повторен не один, а ноль или более раз.

Например, ca*t будет соответствовать 'ct' (0 'a' символов), 'cat' (1 'a'), 'caaat' (3 'a' символа) и так далее.

Повторы, такие как *, становятся greedy; при повторении RE система поиска попытается повторить его как можно больше раз. Если последующие части шаблона не совпадут, механизм поиска вернется назад и повторит попытку с меньшим количеством повторений.

Пошаговый пример сделает это более наглядным. Рассмотрим выражение a[bcd]*b. Оно соответствует букве 'a', нулю или более букв из класса [bcd] и, наконец, заканчивается буквой 'b'. Теперь представьте, что это RE сопоставляется со строкой 'abcbd'.

Шаг

Подборка

Пояснение

1

a

В RE совпадает a.

2

abcbd

Движок подбирает [bcd]*, доходя до конца строки.

3

Неудача

Движок пытается найти соответствие b, но текущая позиция находится в конце строки, поэтому он терпит неудачу.

4

abcb

Вернитесь назад, чтобы [bcd]* соответствовал на один символ меньше.

5

Неудача

Снова попробуйте b, но текущая позиция находится на последнем символе, который является 'd'.

6

abc

Вернитесь назад, чтобы [bcd]* совпадал только с bc.

6

abcb

Попробуйте снова использовать b. На этот раз символ в текущей позиции - 'b', поэтому попытка удается.

Теперь достигнут конец RE, и в нем есть совпадение с 'abcb'. Это демонстрирует, как механизм поиска сначала проходит так далеко, как только может, а если совпадение не найдено, он постепенно возвращается назад и снова и снова перебирает оставшуюся часть RE. Он будет возвращаться назад до тех пор, пока не перепробует ноль совпадений для [bcd]*, и если и это не удастся, механизм придет к выводу, что строка вообще не соответствует RE.

Другим повторяющимся метасимволом является +, который встречается один или несколько раз. Обратите внимание на разницу между * и +; * совпадает ноль или более раз, поэтому то, что повторяется, может вообще не присутствовать, в то время как + требует хотя бы одного вхождения. Если использовать аналогичный пример, то ca+t будет соответствовать 'cat' (1 'a'), 'caaat' (3 'a's), но не будет соответствовать 'ct'.

Есть еще два повторяющихся оператора, или квантификатора. Символ вопросительного знака, ?, встречается либо один, либо ноль раз; можно считать, что он обозначает что-то необязательное. Например, home-?brew соответствует либо 'homebrew', либо 'home-brew'.

Самый сложный квантификатор - {m,n}, где m и n - десятичные целые числа. Этот квантификатор означает, что должно быть не менее m повторений и не более n. Например, a/{1,3}b будет соответствовать 'a/b', 'a//b' и 'a///b'. Он не будет соответствовать 'ab', в котором нет косых черт, или 'a////b', в котором их четыре.

Вы можете опустить либо m, либо n; в этом случае за недостающее значение принимается разумное. Опущение m интерпретируется как нижняя граница 0, а опущение n приводит к верхней границе бесконечности.

В простейшем случае {m} совпадает с предыдущим элементом ровно m раз. Например, a/{2}b будет соответствовать только 'a//b'.

Читатели, склонные к редукционизму, могут заметить, что все три других квантора могут быть выражены с помощью этой нотации. {0,} - то же самое, что *, {1,} эквивалентно +, а {0,1} - то же самое, что ?. Лучше использовать *, + или ?, когда это возможно, просто потому, что они короче и легче читаются.

Использование регулярных выражений

Теперь, когда мы рассмотрели несколько простых регулярных выражений, как на самом деле использовать их в Python? Модуль re предоставляет интерфейс к механизму регулярных выражений, позволяя вам компилировать RE в объекты, а затем выполнять совпадения с ними.

Компиляция регулярных выражений

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

>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')

re.compile() также принимает необязательный аргумент flags, который используется для включения различных специальных функций и вариаций синтаксиса. Мы рассмотрим доступные настройки позже, а пока достаточно одного примера:

>>> p = re.compile('ab*', re.IGNORECASE)

RE передается в re.compile() в виде строки. RE обрабатываются как строки, потому что регулярные выражения не являются частью основного языка Python, и для их выражения не было создано специального синтаксиса. (Есть приложения, которым RE вообще не нужны, поэтому нет необходимости раздувать спецификацию языка их включением). Вместо этого модуль re - это просто модуль расширения языка C, входящий в состав Python, как и модули socket или zlib.

Размещение RE в строках упрощает язык Python, но имеет один недостаток, о котором пойдет речь в следующем разделе.

Чума обратного удара

Как уже говорилось ранее, в регулярных выражениях символ обратной косой черты ('\') используется для обозначения специальных форм или для того, чтобы позволить использовать специальные символы, не вызывая их специального значения. Это противоречит тому, что в Python этот же символ используется для той же цели в строковых литералах.

Допустим, вы хотите написать программу RE, которая сопоставляет строку \section, которая может быть найдена в файле LaTeX. Чтобы понять, что писать в коде программы, начните с нужной строки, которую нужно сопоставить. Далее необходимо экранировать все обратные слеши и другие метасимволы, поставив перед ними обратную косую черту, в результате чего получится строка \\section. Результирующая строка, которая должна быть передана в re.compile(), должна быть \\section. Однако, чтобы выразить это в виде строкового литерала Python, оба обратных слеша должны быть экранированы еще раз.

Персонажи

Сцена

\section

Текстовая строка для сопоставления

\\section

Снятый обратный слеш для re.compile()

"\\\\section"

Обратные косые черты для строкового литерала

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

Решение состоит в том, чтобы использовать для регулярных выражений сырую строковую нотацию Python; обратные косые черты не обрабатываются особым образом в строковом литерале с префиксом 'r', поэтому r"\n" - это двухсимвольная строка, содержащая '\' и 'n', а "\n" - односимвольная строка, содержащая новую строку. Регулярные выражения часто записываются в коде Python с использованием этой необработанной строковой нотации.

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

Обычная строка

Необработанная строка

"ab*"

r"ab*"

"\\\\section"

r"\\section"

"\\w+\\s+\\1"

r"\w+\s+\1"

Выполнение матчей

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

Метод/атрибут

Назначение

match()

Определите, совпадает ли RE с началом строки.

search()

Просканируйте строку в поисках любого места, где встречается этот RE.

findall()

Находит все подстроки, в которых совпадает RE, и возвращает их в виде списка.

finditer()

Находит все подстроки, в которых совпадает RE, и возвращает их в виде iterator.

match() и search() возвращают None, если совпадение не найдено. В случае успеха возвращается экземпляр match object, содержащий информацию о совпадении: где оно начинается и заканчивается, с какой подстрокой совпало и т. д.

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

В этом HOWTO для примеров используется стандартный интерпретатор Python. Сначала запустите интерпретатор Python, импортируйте модуль re и скомпилируйте файл RE:

>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')

Теперь вы можете попробовать сопоставить различные строки с RE [a-z]+. Пустая строка не должна совпадать вообще, поскольку + означает «одно или несколько повторений». В этом случае match() должен вернуть None, что приведет к тому, что интерпретатор не выведет никакого результата. Для наглядности можно явно вывести результат match().

>>> p.match("")
>>> print(p.match(""))
None

Теперь попробуем его на строке, с которой он должен совпасть, например tempo. В этом случае match() вернет match object, поэтому результат следует сохранить в переменной для последующего использования.

>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>

Теперь вы можете запросить у match object информацию о совпавшей строке. Экземпляры объектов Match также имеют несколько методов и атрибутов; наиболее важными из них являются:

Метод/атрибут

Назначение

group()

Верните строку, с которой совпадает RE

start()

Возвращает начальную позицию матча

end()

Возвращает конечную позицию матча

span()

Возвращает кортеж, содержащий (начальную, конечную) позиции совпадения

Попробовав эти методы, вы вскоре проясните их смысл:

>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() возвращает подстроку, с которой совпал RE. start() и end() возвращают начальный и конечный индекс совпадения. span() возвращает начальный и конечный индексы в одном кортеже. Поскольку метод match() проверяет, совпадает ли RE только с началом строки, start() всегда будет нулем. Однако метод search() шаблонов сканирует строку, поэтому в этом случае совпадение может начинаться не с нуля.

>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)

В реальных программах наиболее распространенным стилем является сохранение match object в переменной, а затем проверка, было ли это None. Обычно это выглядит так:

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print('Match found: ', m.group())
else:
    print('No match')

Два метода шаблона возвращают все совпадения с шаблоном. findall() возвращает список совпадающих строк:

>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

Префикс r, превращающий литерал в необработанный строковый литерал, необходим в этом примере, поскольку экранирующие последовательности в обычном «приготовленном» строковом литерале, не распознаваемые Python, в отличие от регулярных выражений, теперь приводят к DeprecationWarning и в конечном итоге превращаются в SyntaxError. См. Чума обратного удара.

Метод findall() должен создать весь список, прежде чем он будет возвращен в качестве результата. Метод finditer() возвращает последовательность экземпляров match object в виде iterator:

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator  
<callable_iterator object at 0x...>
>>> for match in iterator:
...     print(match.span())
...
(0, 2)
(22, 24)
(29, 31)

Функции уровня модуля

Не обязательно создавать объект паттерна и вызывать его методы; модуль re также предоставляет функции верхнего уровня, называемые match(), search(), findall(), sub() и так далее. Эти функции принимают те же аргументы, что и соответствующий метод шаблона, с добавлением строки RE в качестве первого аргумента, и возвращают либо экземпляр None, либо экземпляр match object.

>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')  
<re.Match object; span=(0, 5), match='From '>

Под капотом эти функции просто создают для вас объект паттерна и вызывают соответствующий метод на нем. Они также сохраняют скомпилированный объект в кэше, поэтому при последующих вызовах, использующих тот же RE, не нужно будет снова и снова разбирать шаблон.

Следует ли вам использовать эти функции на уровне модуля или получить шаблон и вызвать его методы самостоятельно? Если вы обращаетесь к regex в цикле, предварительная компиляция сэкономит несколько вызовов функций. За пределами циклов разница невелика благодаря внутреннему кэшу.

Флаги компиляции

Флаги компиляции позволяют изменять некоторые аспекты работы регулярных выражений. Флаги доступны в модуле re под двумя именами: длинным, например IGNORECASE, и коротким, однобуквенным, например I. (Если вы знакомы с модификаторами шаблонов Perl, однобуквенные формы используют те же буквы; короткая форма re.VERBOSE - это re.X, например). Несколько флагов можно указать путем побитового ИЛИ; например, re.I | re.M устанавливает флаги I и M.

Вот таблица доступных флагов, за которой следует более подробное объяснение каждого из них.

Флаг

Значение

ASCII, A

Заставляет некоторые эскейпы, такие как \w, \b, \s и \d, соответствовать только символам ASCII с соответствующим свойством.

DOTALL, S

Заставьте . соответствовать любому символу, включая новые строки.

IGNORECASE, I

Выполняйте совпадения без учета регистра.

LOCALE, L

Выполните сопоставление с учетом локализации.

MULTILINE, M

Многострочное соответствие, затрагивающее ^ и $.

VERBOSE, X (для „extended“)

Включите подробные RE, которые могут быть организованы более чисто и понятно.

re.I
re.IGNORECASE

Выполняйте сопоставление без учета регистра; символьный класс и литеральные строки будут сопоставлять буквы, игнорируя регистр. Например, [A-Z] будет соответствовать и строчным буквам. Полное совпадение с Юникодом также работает, если только флаг ASCII не используется для отключения совпадений, отличных от ASCII. Если шаблоны Юникода [a-z] или [A-Z] используются в сочетании с флагом IGNORECASE, они будут соответствовать 52 буквам ASCII и 4 дополнительным не-ASCII буквам: „İ“ (U+0130, латинская заглавная буква I с точкой над), „ı“ (U+0131, латинская строчная буква без точки i), „ſ“ (U+017F, латинская строчная буква длинная s) и „K“ (U+212A, знак Кельвина). Spam будет соответствовать 'Spam', 'spam', 'spAM' или 'ſpam' (последнее совпадает только в режиме Unicode). При этом не учитывается текущая локаль; она будет учитываться, если вы также установите флаг LOCALE.

re.L
re.LOCALE

Сделайте соответствие \w, \W, \b, \B и нечувствительное к регистру зависит от текущей локали, а не от базы данных Unicode.

Locales - это функция библиотеки C, предназначенная для помощи в написании программ, учитывающих языковые различия. Например, если вы обрабатываете закодированный французский текст, вы хотите иметь возможность написать \w+ для соответствия словам, но \w соответствует только классу символов [A-Za-z] в шаблонах байтов; он не будет соответствовать байтам, соответствующим é или ç. Если ваша система настроена правильно и выбрана французская локаль, некоторые функции языка C сообщат программе, что байт, соответствующий é, также должен считаться буквой. Установка флага LOCALE при компиляции регулярного выражения приведет к тому, что результирующий скомпилированный объект будет использовать эти функции C для \w; это медленнее, но также позволяет \w+ соответствовать французским словам, как вы и ожидали. В Python 3 использовать этот флаг не рекомендуется, так как механизм локалей очень ненадежен, он обрабатывает только одну «культуру» за раз и работает только с 8-битными локалями. В Python 3 по умолчанию уже включено сопоставление с Юникодом для шаблонов Юникода (str), и оно способно работать с различными локалями/языками.

re.M
re.MULTILINE

(^ и $ еще не объяснены; они будут представлены в разделе Больше метаперсонажей).

Обычно ^ совпадает только с началом строки, а $ - только с концом строки и непосредственно перед новой строкой (если она есть) в конце строки. Когда этот флаг указан, ^ совпадает с началом строки и с началом каждой строки внутри строки, сразу после каждой новой строки. Аналогично, метасимвол $ встречается в конце строки и в конце каждой строки (непосредственно перед каждой новой строкой).

re.S
re.DOTALL

Заставляет специальный символ '.' соответствовать любому символу, включая новую строку; без этого флага '.' будет соответствовать всему, за исключением новой строки.

re.A
re.ASCII

Заставьте \w, \W, \b, \B, \s и \S выполнять сопоставление только ASCII вместо полного сопоставления Unicode. Это имеет смысл только для шаблонов Unicode и игнорируется для байтовых шаблонов.

re.X
re.VERBOSE

Этот флаг позволяет писать более читабельные регулярные выражения, предоставляя вам большую гибкость в их форматировании. Когда этот флаг установлен, пробельные символы в строке RE игнорируются, за исключением случаев, когда пробельные символы находятся в классе символов или перед ними стоит неэкранированный обратный слеш; это позволяет более четко организовать и сделать отступы в RE. Этот флаг также позволяет помещать в RE комментарии, которые будут игнорироваться движком; комментарии помечаются символом '#', который не входит в символьный класс и не предшествует обратной косой черте.

Например, вот RE, в котором используется re.VERBOSE; видите, насколько легче читать?

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

Без настройки verbose сообщение RE выглядело бы так:

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

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

Больше силы узора

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

Больше метаперсонажей

Есть несколько метасимволов, которые мы еще не рассмотрели. Большинство из них будет рассмотрено в этом разделе.

Среди оставшихся метасимволов, о которых пойдет речь, - zero-width assertions. Они не заставляют движок продвигаться по строке; вместо этого они вообще не потребляют никаких символов, а просто приводят к успеху или провалу. Например, \b - это утверждение, что текущая позиция находится на границе слова; позиция никак не меняется от \b. Это означает, что утверждения нулевой ширины никогда не должны повторяться, потому что если они совпадают один раз в данном месте, то, очевидно, могут совпадать бесконечное число раз.

|

Чередование, или оператор «или». Если A и B являются регулярными выражениями, то A|B будет соответствовать любой строке, которая соответствует либо A, либо B. Оператор | имеет очень низкий приоритет, чтобы обеспечить разумную работу при чередовании многосимвольных строк. Crow|Servo будет соответствовать либо 'Crow', либо 'Servo', но не 'Cro', 'w' или 'S', а также 'ervo'.

Чтобы сопоставить литерал '|', используйте \| или заключите его внутри символьного класса, как в [|].

^

Сопоставляет с началом строк. Если не установлен флаг MULTILINE, то совпадение будет только в начале строки. В режиме MULTILINE он также будет совпадать сразу после каждой новой строки в строке.

Например, если вы хотите, чтобы слово From совпадало только с началом строки, используйте RE ^From.

>>> print(re.search('^From', 'From Here to Eternity'))  
<re.Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None

Чтобы сопоставить литерал '^', используйте \^.

$

Сопоставляет с концом строки, который определяется как конец строки или любое место, за которым следует символ новой строки.

>>> print(re.search('}$', '{block}'))  
<re.Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))  
<re.Match object; span=(6, 7), match='}'>

Чтобы сопоставить литерал '$', используйте \$ или заключите его внутри символьного класса, как в [$].

\A

Сопоставляет только начало строки. В режиме MULTILINE \A и ^ фактически одинаковы. В режиме MULTILINE они отличаются: \A по-прежнему совпадает только с началом строки, но ^ может совпадать с любым местом в строке, следующим за символом новой строки.

\Z

Сопоставляет только с концом строки.

\b

Граница слова. Это утверждение нулевой ширины, которое совпадает только с началом или концом слова. Слово определяется как последовательность алфавитно-цифровых символов, поэтому конец слова обозначается пробелом или неалфавитно-цифровым символом.

В следующем примере class совпадает только с полным словом; оно не совпадает, если содержится внутри другого слова.

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None

При использовании этой специальной последовательности следует помнить о двух тонкостях. Во-первых, это худшее столкновение между строковыми литералами Python и последовательностями регулярных выражений. В строковых литералах Python \b - это символ обратного пробела, ASCII-значение 8. Если вы не используете необработанные строки, то Python преобразует \b в пробел, и ваше RE не будет соответствовать ожидаемому. Следующий пример выглядит так же, как и предыдущий RE, но в нем опущен 'r' перед строкой RE.

>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))
<re.Match object; span=(0, 7), match='\x08class\x08'>

Во-вторых, внутри символьного класса, где это утверждение не используется, \b представляет символ обратного пробела для совместимости со строковыми литералами Python.

\B

Еще одно утверждение нулевой ширины, противоположное \b, совпадающее только тогда, когда текущая позиция не находится на границе слова.

Группировка

Часто требуется получить больше информации, чем просто о том, совпал RE или нет. Регулярные выражения часто используются для расчленения строк путем записи RE, разделенного на несколько подгрупп, которые соответствуют различным компонентам, представляющим интерес. Например, строка заголовка RFC-822 делится на имя заголовка и значение, разделенные символом ':', следующим образом:

From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com

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

Группы обозначаются метасимволами '(', ')'. Метасимволы '(' и ')' имеют то же значение, что и в математических выражениях; они объединяют содержащиеся в них выражения, и вы можете повторить содержимое группы с помощью квантификатора, например *, +, ? или {m,n}. Например, (ab)* будет соответствовать нулю или более повторений ab.

>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)

Группы, обозначенные '(', ')', также содержат начальный и конечный индекс текста, которому они соответствуют; его можно получить, передав аргумент в group(), start(), end() и span(). Группы нумеруются, начиная с 0. Группа 0 присутствует всегда; это весь RE, поэтому все методы match object имеют группу 0 в качестве аргумента по умолчанию. Позже мы увидим, как выразить группы, которые не охватывают участок текста, которому они соответствуют:

>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'

Подгруппы нумеруются слева направо, от 1 и выше. Группы могут быть вложенными; чтобы определить их количество, просто посчитайте символы открывающей круглой скобки, двигаясь слева направо.

>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'

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

>>> m.group(2,1,2)
('b', 'abc', 'b')

Метод groups() возвращает кортеж, содержащий строки для всех подгрупп, начиная с 1 и до сколь угодно большого их количества.

>>> m.groups()
('abc', 'b')

Обратные ссылки в шаблоне позволяют указать, что содержимое предыдущей группы захвата также должно быть найдено в текущем месте строки. Например, \1 сработает, если в текущей позиции будет найдено точное содержимое группы 1, и не сработает в противном случае. Помните, что в строковых литералах Python также используется обратная косая черта, за которой следуют цифры, что позволяет включать в строку произвольные символы, поэтому при включении обратных ссылок в RE обязательно используйте необработанную строку.

Например, следующий RE обнаруживает удвоенные слова в строке.

>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'

Подобные обратные ссылки не часто полезны для простого поиска по строке - существует мало текстовых форматов, которые повторяют данные таким образом - но вскоре вы обнаружите, что они очень полезны при выполнении подстановки строк.

Группы невовлечения и именованные группы

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

Perl 5 хорошо известен своими мощными дополнениями к стандартным регулярным выражениям. Для этих новых возможностей разработчики Perl не могли выбрать новые однотактные метасимволы или новые специальные последовательности, начинающиеся с \, без того, чтобы регулярные выражения Perl не отличались от стандартных RE. Если бы они выбрали & в качестве нового метасимвола, например, старые выражения предполагали бы, что & - это регулярный символ, и не стали бы его экранировать, написав \& или [&].

Решение, выбранное разработчиками Perl, заключалось в использовании (?...) в качестве синтаксиса расширения. ? сразу после круглой скобки - синтаксическая ошибка, потому что ? не будет ничего повторять, так что это не создавало проблем с совместимостью. Символы сразу после ? указывают, какое расширение используется, поэтому (?=foo) - это одно (утверждение с положительным опережением), а (?:foo) - совсем другое (группа без захвата, содержащая подвыражение foo).

Python поддерживает несколько расширений Perl и добавляет синтаксис расширений к синтаксису расширений Perl. Если первым символом после вопросительного знака является P, вы знаете, что это расширение, специфичное для Python.

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

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

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

За исключением того факта, что вы не можете получить содержимое того, чему соответствовала группа, не захватывающая группа ведет себя точно так же, как захватывающая группа; вы можете поместить в нее что угодно, повторить ее с помощью метасимвола повторения, например *, и вложить ее в другие группы (захватывающие или не захватывающие). (?:...) особенно полезен при изменении существующего шаблона, поскольку вы можете добавлять новые группы, не меняя нумерацию остальных групп. Следует отметить, что нет никакой разницы в производительности при поиске между группами с захватом и без захвата; ни одна из этих форм не быстрее другой.

Более важной особенностью являются именованные группы: вместо того чтобы ссылаться на них по номерам, на группы можно ссылаться по имени.

Синтаксис именованной группы - это одно из специфических для Python расширений: (?P<name>...). name - это, очевидно, имя группы. Именованные группы ведут себя точно так же, как группы захвата, и дополнительно связывают имя с группой. Все методы match object, работающие с группами захвата, принимают либо целые числа, обозначающие группу по номеру, либо строки, содержащие имя нужной группы. Именованным группам по-прежнему присваиваются номера, поэтому информацию о группе можно получить двумя способами:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

Кроме того, вы можете получить именованные группы в виде словаря с помощью groupdict():

>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe')
>>> m.groupdict()
{'first': 'Jane', 'last': 'Doe'}

Именованные группы удобны тем, что позволяют использовать легко запоминающиеся имена, вместо того чтобы запоминать номера. Вот пример RE из модуля imaplib:

InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

Очевидно, что гораздо проще получить m.group('zonem'), а не вспоминать, что нужно получить группу 9.

Синтаксис обратных ссылок в таком выражении, как (...)\1, указывает на номер группы. Естественно, существует вариант, в котором вместо номера используется имя группы. Это еще одно расширение Python: (?P=name) указывает, что содержимое группы с именем name должно быть снова сопоставлено в текущей точке. Регулярное выражение для поиска удвоенных слов, \b(\w+)\s+\1\b, также можно записать как \b(?P<word>\w+)\s+(?P=word)\b:

>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'

Утверждения на будущее

Еще одним утверждением с нулевой шириной является утверждение lookahead. Утверждения Lookahead доступны как в положительной, так и в отрицательной форме и выглядят следующим образом:

(?=...)

Утверждение положительного опережения. Оно выполняется, если содержащееся в нем регулярное выражение, представленное здесь ..., успешно совпадает в текущем месте, и терпит неудачу в противном случае. Но после того, как содержащееся выражение было опробовано, механизм поиска не продвигается вперед; остальная часть шаблона будет опробована прямо там, где началось утверждение.

(?!...)

Отрицательное утверждение с опережением. Это утверждение противоположно положительному; оно успешно, если содержащееся в нем выражение не совпадает в текущей позиции в строке.

Чтобы конкретизировать это, давайте рассмотрим случай, в котором заблаговременный просмотр будет полезен. Рассмотрим простой шаблон для поиска имени файла и разделения его на базовое имя и расширение, разделенные символом .. Например, в news.rc, news - это базовое имя, а rc - расширение имени файла.

Шаблон, соответствующий этому, довольно прост:

.*[.].*$

Обратите внимание, что . требует особого обращения, поскольку это метасимвол, поэтому он находится внутри класса символов, чтобы соответствовать только этому конкретному символу. Также обратите внимание на концевой $; он добавляется для того, чтобы убедиться, что вся остальная часть строки должна быть включена в расширение. Это регулярное выражение соответствует foo.bar и autoexec.bat, sendmail.cf и printers.conf.

Теперь немного усложним задачу: что если вам нужно сопоставить имена файлов, в которых расширение не bat? Некоторые неправильные попытки:

.*[.][^b].*$ В первой попытке исключить bat требуется, чтобы первый символ расширения не был b. Это неверно, поскольку шаблон также не соответствует foo.bar.

.*[.]([^b]..|.[^a].|..[^t])$

Выражение становится еще более запутанным, когда вы пытаетесь исправить первое решение, требуя соответствия одному из следующих случаев: первый символ расширения не является b; второй символ не является a; или третий символ не является t. Это позволяет принять foo.bar и отвергнуть autoexec.bat, но требует трехбуквенного расширения и не примет имя файла с двухбуквенным расширением, например sendmail.cf. Мы снова усложним шаблон, пытаясь исправить ситуацию.

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

В третьей попытке вторая и третья буквы становятся необязательными, чтобы обеспечить соответствие расширениям короче трех символов, например sendmail.cf.

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

Негативный прогноз пробивается сквозь всю эту путаницу:

.*[.](?!bat$)[^.]*$ Отрицательное опережение означает: если выражение bat не совпадает в этой точке, попробуйте остальные части шаблона; если bat$ совпадает, весь шаблон будет провален. Скользящий $ нужен для того, чтобы гарантировать, что что-то вроде sample.batch, где расширение начинается только с bat, будет разрешено. С помощью [^.]* можно убедиться, что шаблон работает, если в имени файла есть несколько точек.

Исключить другое расширение имени файла теперь легко: просто добавьте его в качестве альтернативы внутри утверждения. Следующий шаблон исключает имена файлов, которые заканчиваются на bat или exe:

.*[.](?!bat$|exe$)[^.]*$

Изменение строк

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

Метод/атрибут

Назначение

split()

Разделите строку на список, разбивая его везде, где встречается RE

sub()

Найдите все подстроки, в которых встречается RE, и замените их на другую строку

subn()

Делает то же самое, что и sub(), но возвращает новую строку и количество замен

Разделение строк

Метод split() шаблона разбивает строку на части везде, где встречается RE, возвращая список частей. Он похож на метод split() для строк, но предоставляет гораздо больше возможностей для разделения по разделителям; строка split() поддерживает только разделение по пробельным символам или по фиксированной строке. Как и следовало ожидать, есть и функция re.split() на уровне модуля.

.split(string[, maxsplit=0])

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

Вы можете ограничить количество выполняемых разбиений, передав значение maxsplit. Если maxsplit ненулевое, будет сделано не более maxsplit разбиений, а остаток строки будет возвращен в качестве последнего элемента списка. В следующем примере разделителем является любая последовательность не алфавитно-цифровых символов:

>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']

Иногда вам не только интересно, что представляет собой текст между разделителями, но и необходимо знать, что было разделителем. Если в RE используются разделительные скобки, то их значения также возвращаются как часть списка. Сравните следующие вызовы:

>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']

Функция уровня модуля re.split() добавляет RE, который будет использоваться в качестве первого аргумента, но в остальном она такая же.

>>> re.split(r'[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split(r'([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split(r'[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

Поиск и замена

Другая распространенная задача - найти все совпадения с шаблоном и заменить их на другую строку. Метод sub() принимает значение замены, которое может быть либо строкой, либо функцией, и строку, которую нужно обработать.

.sub(replacement, string[, count=0])

Возвращает строку, полученную путем замены крайних левых непересекающихся вхождений RE в string на замену replacement. Если шаблон не найден, string возвращается без изменений.

Необязательный аргумент count - это максимальное количество вхождений шаблона, которое должно быть заменено; count должно быть неотрицательным целым числом. Значение по умолчанию 0 означает замену всех вхождений.

Вот простой пример использования метода sub(). Он заменяет названия цветов словом colour:

>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'

Метод subn() выполняет ту же работу, но возвращает кортеж, содержащий новое значение строки и количество произведенных замен:

>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)

Пустые совпадения заменяются только в том случае, если они не примыкают к предыдущему пустому совпадению.

>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'

Если замена - это строка, то обрабатываются все содержащиеся в ней обратные косые черты. То есть \n преобразуется в один символ новой строки, \r преобразуется в возврат каретки и так далее. Неизвестные эскейпы, такие как \&, оставляются без внимания. Обратные ссылки, такие как \6, заменяются на подстроку, соответствующую группе в RE. Это позволяет включать части оригинального текста в результирующую строку замены.

В этом примере совпадает слово section, за которым следует строка, заключенная в {, }, и изменяет section на subsection:

>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'

Существует также синтаксис для обращения к именованным группам, как определено в синтаксисе (?P<name>...). В \g<name> будет использоваться подстрока, сопоставленная с группой, названной name, а в \g<number> - соответствующий номер группы. Таким образом, \g<2> эквивалентен \2, но не является двусмысленным в такой строке замены, как \g<2>0. (\20 будет интерпретирован как ссылка на группу 20, а не как ссылка на группу 2, за которой следует литеральный символ '0'). Следующие замены эквивалентны, но используют все три варианта строки замены.

>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'

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

В следующем примере функция замены переводит десятичные числа в шестнадцатеричные:

>>> def hexrepl(match):
...     "Return the hex string for a decimal number"
...     value = int(match.group())
...     return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'

При использовании функции re.sub() на уровне модуля шаблон передается в качестве первого аргумента. Шаблон может быть предоставлен как объект или как строка; если вам нужно указать флаги регулярного выражения, вы должны либо использовать объект шаблона в качестве первого параметра, либо использовать встроенные модификаторы в строке шаблона, например, sub("(?i)b+", "x", "bbbb BBBB") возвращает 'x x'.

Общие проблемы

Регулярные выражения - мощный инструмент для некоторых приложений, но в некоторых случаях их поведение не является интуитивно понятным, а иногда они ведут себя не так, как вы ожидаете. В этом разделе мы расскажем о некоторых наиболее распространенных «подводных камнях».

Используйте строковые методы

Иногда использование модуля re является ошибкой. Если вы сопоставляете фиксированную строку или класс одиночных символов и не используете никаких возможностей re, таких как флаг IGNORECASE, то вся мощь регулярных выражений может и не понадобиться. В Strings есть несколько методов для выполнения операций с фиксированными строками, и они обычно намного быстрее, потому что реализация представляет собой один небольшой цикл на C, оптимизированный для этой цели, а не большой, более обобщенный механизм регулярных выражений.

Одним из примеров может быть замена одной фиксированной строки на другую; например, вы можете заменить word на deed. Функция re.sub() кажется наиболее подходящей для этого, но рассмотрите метод replace(). Обратите внимание, что replace() также заменит word внутри слов, превратив swordfish в sdeedfish, но наивная RE word сделала бы и это. (Чтобы не выполнять замену на части слов, шаблон должен быть \bword\b, чтобы потребовать, чтобы word имел границу слова по обе стороны. Это выходит за рамки возможностей replace()).

Другая распространенная задача - удаление из строки всех вхождений одного символа или замена его другим. Для этого можно использовать что-то вроде re.sub('\n', ' ', S), но translate() способен выполнить обе задачи и будет быстрее, чем любая операция с регулярным выражением.

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

Жадные против нежадных

При повторении регулярного выражения, как, например, в a*, результирующим действием является поглощение как можно большего количества шаблона. Этот факт часто подводит вас, когда вы пытаетесь сопоставить пару сбалансированных разделителей, таких как угловые скобки, окружающие HTML-тег. Наивный шаблон для сопоставления одного HTML-тега не работает из-за жадности .*.

>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span())
(0, 32)
>>> print(re.match('<.*>', s).group())
<html><head><title>Title</title>

RE совпадает с '<' в '<html>', а .* поглощает остальную часть строки. Однако в RE остается еще много места, и > не может совпасть с концом строки, поэтому механизм регулярных выражений вынужден перебирать символ за символом, пока не найдет совпадение для >. Окончательное совпадение простирается от '<' в '<html>' до '>' в '</title>', а это не то, что вам нужно.

В этом случае решением будет использование нежадных квантификаторов *?, +?, ?? или {m,n}?, которые соответствуют как можно меньшему количеству текста. В приведенном выше примере квантификатор '>' пробуют сразу после первого совпадения с '<', а когда это не удается, движок продвигается вперед на один символ за раз, повторяя попытку '>' на каждом шаге. Это приводит к правильному результату:

>>> print(re.match('<.*?>', s).group())
<html>

(Обратите внимание, что разбор HTML или XML с помощью регулярных выражений очень мучителен. Быстрые и грязные шаблоны справятся с общими случаями, но в HTML и XML есть особые случаи, которые сломают очевидное регулярное выражение; к тому времени, когда вы напишете регулярное выражение, которое обрабатывает все возможные случаи, шаблоны станут очень сложными. Для таких задач используйте модуль парсера HTML или XML).

Использование re.VERBOSE

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

Для таких RE указание флага re.VERBOSE при компиляции регулярного выражения может быть полезным, поскольку позволяет более четко отформатировать регулярное выражение.

Флаг re.VERBOSE имеет несколько эффектов. Пробелы в регулярном выражении, которые не находятся внутри символьного класса, игнорируются. Это означает, что выражение dog | cat эквивалентно менее читаемому dog|cat, но [a b] все равно будет соответствовать символам 'a', 'b' или пробелу. Кроме того, внутри RE можно помещать комментарии; комментарии простираются от символа # до следующей новой строки. При использовании строк с тройными кавычками это позволяет форматировать RE более аккуратно:

pat = re.compile(r"""
 \s*                 # Skip leading whitespace
 (?P<header>[^:]+)   # Header name
 \s* :               # Whitespace, and a colon
 (?P<value>.*?)      # The header's value -- *? used to
                     # lose the following trailing whitespace
 \s*$                # Trailing whitespace to end-of-line
""", re.VERBOSE)

Это гораздо удобнее для чтения, чем:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

Обратная связь

Регулярные выражения - сложная тема. Помог ли вам этот документ понять их? Были ли в нем непонятные моменты или проблемы, которые вы встретили и которые здесь не рассматривались? Если да, пожалуйста, отправьте автору предложения по улучшению.

Самой полной книгой по регулярным выражениям, безусловно, является книга Джеффри Фридла «Mastering Regular Expressions», изданная O’Reilly. К сожалению, она сосредоточена исключительно на разновидностях регулярных выражений в Perl и Java и совсем не содержит материала по Python, поэтому не будет полезна в качестве справочника по программированию на Python. (В первом издании рассматривался ныне удаленный модуль Python regex, который вам не очень поможет). Подумайте о том, чтобы взять ее в библиотеке.