Руководство по регулярным выражениям
- Автор:
-
A.M. Kuchling <amk@amk.ca>
Введение
Регулярные выражения (RE, regex или шаблоны regex) — это, по сути, небольшой специализированный язык программирования, встроенный в Python и доступный через модуль re. С помощью этого небольшого языка вы задаёте правила для множества возможных строк, которые хотите найти; в это множество могут входить предложения на английском языке, адреса электронной почты, команды TeX или любые другие строки. Затем можно задать вопросы вроде «Соответствует ли эта строка шаблону?» или «Есть ли в этой строке где-нибудь совпадение с шаблоном?». Кроме того, с помощью RE можно изменять строку или разделять её на части разными способами.
Шаблоны регулярных выражений компилируются в последовательность байт-кодов, которые затем выполняются написанным на C механизмом сопоставления. При использовании расширенных возможностей может понадобиться внимательно изучить, как механизм будет выполнять заданное RE, и записать RE определённым образом, чтобы получить более быстрый байт-код. В этом документе оптимизация не рассматривается, поскольку для неё необходимо хорошо понимать внутреннее устройство механизма сопоставления.
Язык регулярных выражений относительно мал и ограничен, поэтому с его помощью нельзя выполнить все возможные задачи обработки строк. Есть и такие задачи, которые можно решить регулярными выражениями, но сами выражения при этом оказываются очень сложными. В таких случаях лучше написать код на 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), в котором он соответствует и символу новой строки. . часто используется, когда нужно сопоставить «любой символ».
Повторение элементов
Возможность сопоставлять различные наборы символов — первое преимущество регулярных выражений, недоступное при использовании методов строк. Однако если бы это было единственным дополнительным преимуществом regex, они не представляли бы большого прогресса. Ещё одна возможность — указывать, что части RE должны повторяться заданное количество раз.
Первый метасимвол для повторения элементов, который мы рассмотрим, — *. * не соответствует самому символу '*', а указывает, что предыдущий символ может соответствовать нулю или более раз, а не ровно один раз.
Например, ca*t будет соответствовать 'ct' (0 символов 'a'), 'cat' (1 символ 'a'), 'caaat' (3 символа 'a') и так далее.
Повторы, такие как *, являются жадными: повторяя RE, механизм сопоставления будет пытаться повторить его как можно больше раз. Если последующие части шаблона не совпадут, механизм сопоставления вернётся назад и попробует ещё раз, но с меньшим количеством повторов.
Пошаговый пример поможет лучше это понять. Рассмотрим выражение a[bcd]*b. Оно соответствует букве 'a', нулю или более букв из класса [bcd] и, наконец, заканчивается символом 'b'. Теперь представим, что мы сопоставляем это RE со строкой 'abcbd'.
Шаг | Сопоставлено | Пояснение |
|---|---|---|
1 |
| Сопоставляется символ |
2 |
| Механизм сопоставляет |
3 | Несовпадение | Механизм пытается сопоставить |
4 |
| Механизм возвращается назад, чтобы |
5 | Несовпадение | Механизм снова пытается сопоставить |
6 |
| Механизм снова возвращается назад, чтобы |
6 |
| Механизм снова пытается сопоставить |
Теперь достигнут конец 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, верхняя граница считается равной бесконечности.
Простейший вариант {m} означает, что предшествующий элемент должен повторяться ровно m раз. Например, a/{2}b будет соответствовать только 'a//b'.
Читатели, склонные к редукционизму, могут заметить, что все три других квантификатора можно выразить с помощью этой записи. {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, обе обратные косые черты необходимо экранировать ещё раз.
Символы | Этап |
|---|---|
| Текстовая строка, которой нужно найти соответствие |
| Экранированная обратная косая черта для |
| Экранированные обратные косые черты для строкового литерала |
Короче говоря, чтобы найти соответствие буквальной обратной косой черте, нужно записать '\\\\' как строку регулярного выражения, поскольку само регулярное выражение должно иметь вид \\, а в обычном строковом литерале Python каждая обратная косая черта должна быть записана как \\. В регулярных выражениях, где обратные косые черты встречаются часто, это приводит к множеству повторяющихся обратных косых черт и затрудняет понимание результирующих строк.
Решение — использовать для регулярных выражений обозначение необработанных строк Python: в строковом литерале с префиксом 'r' обратные косые черты не обрабатываются особым образом, поэтому r"\n" — это строка из двух символов, содержащая '\' и 'n', а "\n" — строка из одного символа, содержащая перевод строки. В коде Python регулярные выражения часто записывают именно в таком виде.
Кроме того, специальные escape-последовательности, допустимые в регулярных выражениях, но недопустимые в строковых литералах Python, теперь вызывают предупреждение DeprecationWarning, а со временем будут вызывать ошибку SyntaxError. Это означает, что такие последовательности станут недопустимыми, если не использовать обозначение необработанных строк или не экранировать обратные косые черты.
Обычная строка | Необработанная строка |
|---|---|
|
|
|
|
|
|
Поиск совпадений
Что делать после того, как у вас появился объект, представляющий скомпилированное регулярное выражение? У объектов шаблонов есть несколько методов и атрибутов. Здесь мы рассмотрим только самые важные; полный список см. в документации модуля re.
Метод/атрибут | Назначение |
|---|---|
| Определяет, соответствует ли регулярное выражение началу строки. |
| Просматривает строку в поисках позиции, в которой этому регулярному выражению соответствует фрагмент. |
| Находит все подстроки, соответствующие регулярному выражению, и возвращает их в виде списка. |
| Находит все подстроки, соответствующие регулярному выражению, и возвращает их в виде итератора. |
match() и search() возвращают None, если совпадение не найдено. Если поиск успешен, возвращается экземпляр объекта совпадения, содержащий сведения о совпадении: его начальную и конечную позиции, соответствующую подстроку и другую информацию.
Изучить это можно, экспериментируя в интерактивном режиме с модулем re.
В этом руководстве для примеров используется стандартный интерпретатор 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'>
Теперь можно запросить у объекта совпадения сведения о строке, соответствующей шаблону. У экземпляров объекта совпадения также есть несколько методов и атрибутов; самые важные из них:
Метод/атрибут | Назначение |
|---|---|
| Возвращает строку, соответствующую регулярному выражению |
| Возвращает начальную позицию совпадения |
| Возвращает конечную позицию совпадения |
| Возвращает кортеж с позициями начала и конца совпадения |
Попробуйте вызвать эти методы — так вы быстро поймёте, что они означают:
>>> 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, превращающий литерал в литерал необработанной строки, поскольку 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() и так далее. Эти функции принимают те же аргументы, что и соответствующий метод шаблона, но строка регулярного выражения указывается первым аргументом. Они по-прежнему возвращают либо 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.
Ниже приведена таблица доступных флагов, а затем — более подробное описание каждого из них.
Флаг | Значение |
|---|---|
Заставляет такие escape-последовательности, как | |
Заставляет | |
Выполняет поиск без учёта регистра. | |
Выполняет поиск с учётом локали. | |
Включает многострочный поиск, влияя на | |
Включает подробный режим для регулярных выражений, позволяющий записывать их более чётко и понятно. |
- re.I
- re.IGNORECASE
-
Выполняет поиск без учёта регистра: классы символов и строковые литералы находят совпадения с буквами независимо от регистра. Например,
[A-Z]будет соответствовать и строчным буквам. Полное сопоставление по Unicode также работает, если только флагASCIIне используется для отключения сопоставления с символами не из ASCII. Если шаблоны Unicode[a-z]или[A-Z]используются вместе с флагомIGNORECASE, они будут соответствовать 52 буквам ASCII и ещё четырём буквам не из ASCII: «İ» (U+0130, заглавная латинская буква I с точкой сверху), «ı» (U+0131, строчная латинская буква i без точки), «ſ» (U+017F, строчная латинская буква s длинная) и «K» (U+212A, знак кельвина).Spamбудет соответствовать'Spam','spam','spAM'или'ſpam'(последний вариант соответствует только в режиме Unicode). При преобразовании к нижнему регистру текущая локаль не учитывается; она будет учитываться, если также задать флагLOCALE.
- re.L
- re.LOCALE
-
Заставляет
\w,\W,\b,\Bи поиск без учёта регистра зависеть от текущей локали, а не от базы данных Unicode.Локали — это функция библиотеки C, предназначенная для написания программ, учитывающих различия между языками. Например, при обработке текста на французском языке в кодировке вам может понадобиться написать
\w+для поиска слов, но\wсоответствует классу символов[A-Za-z]только в байтовых шаблонах; ему не соответствуют байты, представляющиеéилиç. Если система настроена правильно и выбрана французская локаль, некоторые функции C сообщат программе, что байт, соответствующийé, также следует считать буквой. Если при компиляции регулярного выражения задать флагLOCALE, скомпилированный объект будет использовать эти функции C для\w. Это работает медленнее, но также позволяет\w+находить французские слова, как и ожидалось. В Python 3 этот флаг не рекомендуется использовать, поскольку механизм локалей очень ненадёжен: он обрабатывает только одну «культуру» за раз и работает только с 8-битными локалями. В Python 3 сопоставление по Unicode для шаблонов Unicode (str) включено по умолчанию и умеет работать с разными локалями и языками.
- re.M
- re.MULTILINE
-
(Пока ещё не объяснялось, что такое
^и$; они будут представлены в разделе Другие метасимволы.)Обычно
^соответствует только началу строки, а$— только концу строки и непосредственно позиции перед символом перевода строки (если он есть) в конце строки. Если задан этот флаг,^соответствует началу строки и началу каждой строки внутри неё — сразу после каждого символа перевода строки. Аналогично метасимвол$соответствует концу строки и концу каждой строки (непосредственно перед каждым символом перевода строки).
- re.S
- re.DOTALL
-
Заставляет специальный символ
'.'соответствовать любому символу, включая символ перевода строки; без этого флага'.'соответствует чему угодно, кроме символа перевода строки.
- re.A
- re.ASCII
-
Заставляет
\w,\W,\b,\B,\sи\Sвыполнять поиск только по ASCII, а не по всему Unicode. Это имеет смысл только для шаблонов Unicode и игнорируется для байтовых шаблонов.
- re.X
- re.VERBOSE
-
Этот флаг позволяет писать более понятные регулярные выражения, предоставляя больше возможностей для их форматирования. Если флаг задан, пробельные символы в строке регулярного выражения игнорируются, кроме случаев, когда пробел находится внутри класса символов или перед ним стоит неэкранированная обратная косая черта. Это позволяет нагляднее структурировать регулярное выражение и добавлять отступы. Флаг также позволяет включать в регулярное выражение комментарии, которые будут игнорироваться движком; комментарий обозначается символом
'#', который находится не внутри класса символов и перед которым не стоит неэкранированная обратная косая черта.Например, вот регулярное выражение с флагом
re.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)Без подробного режима регулярное выражение выглядело бы так:
charref = re.compile("&#(0[0-7]+" "|[0-9]+" "|x[0-9a-fA-F]+);")В примере выше для разбиения регулярного выражения на небольшие части использована автоматическая конкатенация строковых литералов в Python, но всё равно его сложнее понять, чем вариант с
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в символ возврата на одну позицию, и ваше регулярное выражение не будет работать так, как вы ожидаете. Следующий пример выглядит так же, как предыдущее регулярное выражение, но перед строкой регулярного выражения опущен'r'.>>> 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() возвращает кортеж со строками всех подгрупп — от первой до последней.
>>> 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() разбивает строку в местах совпадения с регулярным выражением и возвращает список частей. Он похож на строковый метод 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*$")
Обратная связь
Регулярные выражения — сложная тема. Помог ли этот документ вам разобраться в них? Были ли в нём непонятные места или проблемы, с которыми вы столкнулись, но которые здесь не рассмотрены? Если да, отправьте автору предложения по улучшению.
Самой полной книгой о регулярных выражениях, скорее всего, является «Mastering Regular Expressions» Джеффри Фридла, опубликованная издательством O’Reilly. К сожалению, в ней рассматриваются исключительно варианты регулярных выражений Perl и Java, и совсем нет материалов по Python, поэтому она не будет полезна в качестве справочника по программированию на Python. (В первом издании рассматривался удалённый ныне модуль regex, что вряд ли вам поможет.) Попробуйте взять эту книгу в библиотеке.
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/howto/regex.html