Spec-Zone.ru › Python 3.10

Как использовать регулярные выражения

Автор

A.M. Kuchling <amk@amk.ca>

Аннотация

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

Введение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Следующий список специальных последовательностей неполный. Полный список последовательностей и расширенные определения классов для шаблонов строк Unicode см. в последней части Синтаксис регулярных выражений в справочнике по стандартной библиотеке. В общем случае версии 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), где он будет сопоставлять даже символ новой строки. . часто используется, когда вы хотите сопоставить «любой символ».

Повторяющиеся элементы

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

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

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

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

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

Шаг

Совпадение

Объяснение

1

a

a в регулярном выражении совпадает.

2

abcbd

Механизм сопоставляет [bcd]*, доходя до конца строки.

3

Ошибка

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

4

abcb

Откат, чтобы [bcd]* совпадало с на один символ меньше.

5

Ошибка

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

6

abc

Еще один откат, чтобы [bcd]* сопоставляло только bc.

6

abcb

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

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

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

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

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

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

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

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

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

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

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

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

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

Символы

Этап

\section

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

\\section

Экранированный обратный слэш для re.compile()

"\\\\section"

Экранированные обратные слэши для строкового литерала

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

Решение заключается в использовании необработанного строкового обозначения 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()

Определить, соответствует ли регулярное выражение началу строки.

search()

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

findall()

Найти все подстроки, где соответствует регулярное выражение, и вернуть их как список.

finditer()

Найти все подстроки, где соответствует регулярное выражение, и вернуть их как итератор.

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

Вы можете узнать об этом, проведя интерактивные эксперименты с модулем re. Если у вас есть tkinter, вы также можете посмотреть на Tools/demo/redemo.py, демонстрационную программу, входящую в дистрибутив Python. Она позволяет вводить регулярные выражения и строки и отображает, соответствует ли регулярное выражение или нет. redemo.py может быть очень полезным при отладке сложного регулярного выражения.

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

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

Теперь вы можете попробовать сопоставить различные строки с регулярным выражением [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'>

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

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

Назначение

group()

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

start()

Вернуть начальную позицию совпадения.

end()

Вернуть конечную позицию совпадения.

span()

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

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

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

group() возвращает подстроку, соответствующую регулярному выражению. start() и end() возвращают начальный и конечный индексы совпадения. span() возвращает оба индекса начала и конца в одном кортеже. Поскольку метод match() только проверяет, соответствует ли регулярное выражение началу строки, 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, делающий литерал сырой строкой, необходим в этом примере, потому что последовательности обратной косой черты в обычном «готовом» литерале строки, которые не распознаются 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() и так далее. Эти функции принимают те же аргументы, что и соответствующий метод шаблона, с добавлением строки регулярного выражения в качестве первого аргумента, и по-прежнему возвращают либо 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 под двумя именами, например, длинным именем, таким как 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 (для «расширенного»)

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

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"""
 &[#]                # 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.

Более мощные шаблоны

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

Дополнительные метасимволы

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

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

|

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

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

^

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

Например, если вы хотите сопоставить слово From только в начале строки, используемое регулярное выражение — ^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 в возврат, и ваше регулярное выражение не будет соответствовать ожидаемому.

>>> 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, которое соответствует только тогда, когда текущая позиция не находится на границе слова.

Группирование

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

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

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

Обратные ссылки такого рода не часто полезны просто для поиска в строке — в нем мало текстовых форматов, которые повторяют данные таким способом — но вы скоро поймете, что они очень полезны при выполнении подстановок строк.

Незахватывающие и именованные группы

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

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

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

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

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

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

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

Именованные группы полезны, потому что они позволяют использовать легко запоминающиеся имена вместо необходимости запоминать номера. Вот пример регулярного выражения из модуля 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()

Разбить строку на список, разделив её там, где совпадает регулярное выражение

sub()

Найти все подстроки, где совпадает регулярное выражение, и заменить их другой строкой

subn()

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

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

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

.split(string[, maxsplit=0])

Разделяет строку string по соответствиям регулярного выражения. Если в регулярном выражении используются группы захвата, то их содержимое также будет возвращено как часть результирующего списка. Если 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().']

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

>>> 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.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])

Возвращает строку, полученную путём замены всех левых неперекрывающихся вхождений регулярного выражения в 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, заменяются подстрокой, соответствующей группе в регулярном выражении. Это позволяет вам включать части исходного текста в результирующую строку замены.

Этот пример ищет слово 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):
...     "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, то полная мощь регулярных выражений может не потребоваться. У строк есть несколько методов для выполнения операций с фиксированными строками, и они обычно гораздо быстрее, потому что реализация представляет собой один небольшой цикл C, оптимизированный для этой цели, вместо большого, более обобщённого движка регулярных выражений.

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

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

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

match() против search()

Функция match() проверяет только, соответствует ли регулярное выражение началу строки, а функция search() ищет совпадение в строке вперёд. Важно помнить об этом различии. Помните, что match() сообщит только об успешном совпадении, которое начнётся с 0; если совпадение не начнётся с нуля, match() его не сообщит.

>>> print(re.match('super', 'superstition').span())
(0, 5)
>>> print(re.match('super', 'insuperable'))
None

С другой стороны, search() будет просматривать строку вперёд, сообщая о первом найденном совпадении.

>>> print(re.search('super', 'superstition').span())
(0, 5)
>>> print(re.search('super', 'insuperable').span())
(2, 7)

Иногда вы захотите продолжать использовать re.match() и просто добавить .* в начало вашего регулярного выражения. Сопротивляйтесь этому желанию и используйте re.search() вместо этого. Компилятор регулярных выражений выполняет некоторый анализ регулярных выражений для ускорения поиска совпадения. Один такой анализ определяет, каким должен быть первый символ совпадения; например, шаблон, начинающийся с Crow , должен совпадать, начиная с 'C'. Анализ позволяет движку быстро просматривать строку, ища начальный символ, и пытаться выполнить полное совпадение только в случае, если найден 'C'.

Добавление .* блокирует эту оптимизацию, требуя прохода до конца строки и возврата назад для поиска совпадения для остальной части регулярного выражения. Используйте re.search() вместо этого.

Жадные и нежадные

При повторении регулярного выражения, как в 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>

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

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

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

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

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

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

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

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

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*$")

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

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

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

© 2001–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.10/howto/regex.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API