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

Автор:А. М. Кучлинг <amk@amk.ca>

Аннотация

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

Вступление

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

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

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

Простые шаблоны

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

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

Символы соответствия

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

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

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

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

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

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

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

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

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

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

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

\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') и т. д.

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

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

Шаг Соответствие Объяснение
1 a a в RE соответствии.
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'), но не будет соответствовать '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 приводит к бесконечности верхней границы.

Читатели, склонные к редукционизму, могут заметить, что все три других квалификатора могут быть выражены с использованием этой нотации. {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 с использованием этой нотации необработанных строк.

Кроме того, специальные escape-последовательности, которые допустимы в регулярных выражениях, но недействительны в качестве строковых литералов 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 и вернуть их как итератор.

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

Вы можете узнать об этом, экспериментируя в интерактивном режиме с модулем re. Если у вас есть tkinter, вы также можете посмотреть Tools/demo/redemo.py, демонстрационную программу, включенную в дистрибутив Python. Он позволяет вам вводить RE и строки и показывает, соответствует RE или нет. redemo.py может оказаться весьма полезным при отладке сложного 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() вернёт объект соответствия, поэтому вам следует сохранить результат в переменной для дальнейшего использования.

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

Теперь вы можете запросить объект соответствия информацию о совпадающей строке. Экземпляры объектов 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)

В реальных программах наиболее распространенный стиль — сохранить объект соответствия в переменной, а затем проверить, было ли это 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, делающий литерал необработанным строковым литералом, необходим в этом примере, потому что escape-последовательности в обычном «приготовленном» строковом литерале, которые не распознаются Python, в отличие от регулярных выражений, теперь приводят к DeprecationWarning и в конечном итоге станут SyntaxError. См. Чума обратной косой черты.

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

>>> 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, либо экземпляр объект соответствия.

>>> 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 не нуждаются в повторном синтаксическом анализе шаблона.

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

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

Флаги компиляции позволяют изменять некоторые аспекты работы регулярных выражений. Флаги доступны в модуле 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 (for „extended“) Включить подробные RE, которые могут быть более человекочитаемыми и понятными.
I
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' (последний соответствует только в режиме Юникод). Этот нижний регистр не учитывает текущий языковой стандарт; это будет, если вы также установите флаг LOCALE.

L
LOCALE

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

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

M
MULTILINE

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

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

S
DOTALL

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

A
ASCII

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

X
VERBOSE

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

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

charref = re.compile(r"""
 &[#]                # Начало ссылки на числовой объект
 (
     0[0-7]+         # Восьмеричная форма
   | [0-9]+          # Десятичная форма
   | x[0-9a-fA-F]+   # Шестнадцатеричная форма
 )
 ;                   # Завершающая точка с запятой
""", re.VERBOSE)

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

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

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

Более продвинутые шаблоны

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

Больше метасимволов

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

Некоторые из оставшихся метасимволов, которые следует обсудить, — это утверждения нулевой ширины. Они не заставляют двигатель двигаться по строке; вместо этого они вообще не потребляют символов, а просто преуспевают или проигрывают. Например, \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, поэтому все методы объект соответствия содержат группу 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 могут использовать множество групп как для захвата представляющих интерес подстрок, так и для группировки и структурирования самого RE. В сложных 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 — это, очевидно, название группы. Именованные группы ведут себя точно так же, как группы захвата, и дополнительно связывают имя с группой. Все методы объекта соответствия, которые имеют дело с группами захвата, принимают либо целые числа, которые относятся к группе по номеру, либо строки, содержащие имя желаемой группы. Именованным группам по-прежнему присваиваются номера, поэтому вы можете получить информацию о группе двумя способами:

>>> 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'

Утверждения с опережением

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

(?=...)
Утверждение положительного просмотра вперед. Это выполняется успешно, если содержащееся в нем регулярное выражение, представленное здесь как ..., успешно совпадает в текущем местоположении, и не работает в противном случае. Но после того, как содержащееся выражение было опробовано, механизм сопоставления вообще не продвигается; остальная часть шаблона проверяется прямо там, где началось утверждение.
(?!...)
Утверждение отрицательного просмотра вперед. Это противоположно положительному утверждению; оно считается успешным, если содержащееся выражение не совпадает с текущей позицией в строке.

Чтобы добавить понятности, давайте рассмотрим случай, когда полезен просмотр вперед. Рассмотрим простой шаблон для сопоставления имени файла и разделения его на базовое имя и расширение, разделенные .. Например, в 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])

Разделить string по совпадениям регулярного выражения. Если в 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-'

Если replacement является строкой, обрабатываются любые экранирующие символы обратной косой черты. Т. е. \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}'

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

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

>>> def hexrepl(match):
...     "Возвращает шестнадцатеричную строку для десятичного числа"
...     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, тогда полная мощность регулярных выражений может не потребоваться. У строк есть несколько методов для выполнения операций с фиксированными строками, и они обычно намного быстрее, потому что реализация представляет собой единственный небольшой цикл 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*                 # Пропустить ведущие пробелы
 (?P<header>[^:]+)   # Имя заголовка
 \s* :               # Пробел и двоеточие
 (?P<value>.*?)      # Значение заголовка - *? используется для
                     # потери следующего конечного пробела
 \s*$                # Конечный пробел до конца строки
""", re.VERBOSE)

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

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

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

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

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