Как использовать регулярные выражения
- Автор
-
А.М. Кучлинг <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], которая использует диапазон для выражения того же набора символов. Если вы хотели сопоставить только строчные буквы, ваше регулярное выражение было бы [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 |
|
|
2 |
| Движок сопоставляет |
3 | Ошибка | Движок пытается сопоставить |
4 |
| Движок откатывается назад, чтобы |
5 | Ошибка | Попытка снова сопоставить |
6 |
| Ещё раз откатывается назад, так что |
6 |
| Повторная попытка сопоставления |
Теперь достигнут конец регулярного выражения, и он сопоставился с '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, оба обратных слэша необходимо экранировать ещё раз.
Символы | Этап |
|---|---|
| Строка для сопоставления |
| Экранированный обратный слэш для |
| Экранированные обратные слэши для строкового литерала |
Короче говоря, чтобы сопоставить буквенный обратный слэш, необходимо написать '\\\\' в качестве строки регулярного выражения, потому что регулярное выражение должно быть \\, а каждый обратный слэш должен быть выражен как \\ внутри обычного строкового литерала Python. В регулярных выражениях, которые содержат обратные слэши повторно, это приводит к многочисленным повторяющимся обратным слэшам и затрудняет понимание результирующих строк.
Решение заключается в использовании синтаксиса «сырых строк» Python для регулярных выражений; обратные слэши не обрабатываются каким-либо специальным образом в строковых литералах, предваряемых префиксом 'r', поэтому r"\n" является строкой из двух символов, содержащей '\' и 'n', в то время как "\n" — это строка из одного символа, содержащая перевод строки. Регулярные выражения часто будут записываться в коде Python с использованием этого синтаксиса «сырых строк».
Кроме того, специальные последовательности экранирования, которые допустимы в регулярных выражениях, но не допустимы в строковых литералах Python, теперь приводят к DeprecationWarning и, в конечном итоге, к SyntaxError, что означает, что последовательности будут недопустимы, если не используется синтаксис «сырых строк» или экранирование обратных слэшей.
Обычная строка | Строка без обработки |
|---|---|
|
|
|
|
|
|
Выполнение сопоставлений
После того, как у вас есть объект, представляющий скомпилированное регулярное выражение, что с ним делать? Объекты шаблонов имеют несколько методов и атрибутов. Здесь будут рассмотрены только самые важные из них; обратитесь к документации re за полным списком.
Метод/Атрибут | Назначение |
|---|---|
| Определяет, соответствует ли регулярное выражение началу строки. |
| Просматривает строку, ища местоположения, где это регулярное выражение соответствует. |
| Находит все подстроки, где соответствует регулярное выражение, и возвращает их в виде списка. |
| Находит все подстроки, где соответствует регулярное выражение, и возвращает их как итератор. |
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'>
Теперь вы можете запросить у объекта совпадения информацию о совпавшей строке. Экземпляры объектов совпадения также имеют несколько методов и атрибутов; самые важные из них:
Метод/Атрибут | Назначение |
|---|---|
| Возвращает строку, соответствующую регулярному выражению |
| Возвращает начальную позицию совпадения |
| Возвращает конечную позицию совпадения |
| Возвращает кортеж, содержащий позиции начала и конца совпадения |
Попытка использования этих методов скоро прояснит их значение:
>>> 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.
Ниже представлена таблица доступных флагов, за которой следует более подробное объяснение каждого из них.
Флаг | Значение |
|---|---|
| Заставляет несколько экранированных последовательностей, таких как |
| Заставляет |
| Выполняет сопоставление без учета регистра. |
| Выполняет сопоставление с учетом текущей локали. |
| Сопоставление по нескольким строкам, влияющее на |
| Включает подробные регулярные выражения, которые могут быть организованы более четко и понятнее. |
-
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в символ удаления, и ваше регулярное выражение не будет совпадать так, как вы ожидаете. Следующий пример выглядит так же, как наше предыдущее регулярное выражение, но опускает'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() возвращает кортеж, содержащий строки для всех подгрупп, начиная с 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'), вместо того, чтобы помнить, как получить девятую группу.
Синтаксис обратных ссылок в выражении, таком как (...)\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$)[^.]*$
Изменение строк
До этого момента мы просто выполняли поиск по статической строке. Регулярные выражения также часто используются для изменения строк различными способами, используя следующие методы шаблонов:
Метод/Атрибут | Назначение |
|---|---|
| Разделить строку на список, разделяя ее там, где совпадает RE |
| Найти все подстроки, где совпадает RE, и заменить их другой строкой |
| Делает то же, что и |
Разделение строк
Метод split() шаблона разделяет строку там, где совпадает RE, возвращая список фрагментов. Он похож на метод split() строк, но предоставляет гораздо большую общность в разделителях, по которым можно разделять; строка split() поддерживает разделение только по пробелам или по фиксированной строке. Как и ожидалось, есть также функция уровня модуля re.split().
-
.split(string[, maxsplit=0]) -
Разделить строку по совпадениям регулярного выражения. Если используются группирующие скобки в RE, то их содержимое также будет возвращено как часть результирующего списка. Если maxsplit не равно нулю, выполняется не более maxsplit разделений.
Вы можете ограничить количество произведённых разделений, передав значение для maxsplit. Когда maxsplit не равно нулю, выполняется не более maxsplit разделений, а остальная часть строки возвращается как последний элемент списка. В следующем примере разделителем является любая последовательность небуквенно-цифровых символов.
>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']
Иногда вы заинтересованы не только в том, что находится между разделителями, но и в том, что это за разделители. Если используются группирующие скобки в RE, то их значения также возвращаются как часть списка. Сравните следующие вызовы:
>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']
Функция уровня модуля re.split() добавляет RE в качестве первого аргумента, но в остальном она такая же.
>>> re.split(r'[\W]+', 'Words, words, words.') ['Words', 'words', 'words', ''] >>> re.split(r'([\W]+)', 'Words, words, words.') ['Words', ', ', 'words', ', ', 'words', '.', ''] >>> re.split(r'[\W]+', 'Words, words, words.', 1) ['Words', 'words, words.']
Поиск и замена
Другой распространённой задачей является нахождение всех совпадений с шаблоном и замена их другой строкой. Метод sub() принимает значение замены, которое может быть строкой или функцией, и строку для обработки.
-
.sub(replacement, string[, count=0]) -
Возвращает строку, полученную заменой самых левых неперекрывающихся вхождений RE в строке на замену replacement. Если шаблон не найден, возвращается строка без изменений.
Необязательный аргумент 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() выполняет ту же работу, но возвращает кортеж из 2 элементов: новую строку и количество выполненных замен:
>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)
Пустые совпадения заменяются только тогда, когда они не прилегают к предыдущему пустому совпадению.
>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'
Если replacement является строкой, любые обратные слэши в ней обрабатываются. То есть, \n преобразуется в один символ новой строки, \r преобразуется в символ возврата каретки и так далее. Неизвестные эскейпы, такие как \& оставляются без изменений. Обратные ссылки, такие как \6, заменяются подстрокой, соответствующей соответствующей группе в RE. Это позволяет включить части исходного текста в результирующую строку замены.
Этот пример находит слово section за которым следует строка, заключённая в {, }, и изменяет section на subsection:
>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'
Также существует синтаксис для ссылки на именованные группы, определённые синтаксисом (?P<name>...). \g<name> будет использовать подстроку, соответствующую группе с именем name, а \g<number> использует соответствующий номер группы. \g<2> поэтому эквивалентен \2, но не является неоднозначным в строке замены, такой как \g<2>0. (\20 будет интерпретироваться как ссылка на группу 20, а не как ссылка на группу 2, за которой следует буква '0'.) Следующие замены все эквивалентны, но используют все три варианта строки замены.
>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'
replacement также может быть функцией, что даёт вам ещё больший контроль. Если replacement является функцией, функция вызывается для каждого неперекрывающегося вхождения pattern. При каждом вызове функция получает аргумент объект совпадения для совпадения и может использовать эту информацию для вычисления желаемой строки замены и возврата её.
В следующем примере функция замены переводит десятичные числа в шестнадцатеричные:
>>> def hexrepl(match): ... "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. (В первом издании рассматривался модуль Python regex — теперь удаленный — который вам вряд ли поможет.) Попробуйте найти её в вашей библиотеке.
© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.9/howto/regex.html