Руководство по регулярным выражениям
- Автор
-
A.M. Kuchling <amk@amk.ca>
Аннотация
Данный документ является вводным руководством по использованию регулярных выражений в Python с помощью модуля re. Он предоставляет более лёгкое в понимании введение, чем соответствующий раздел Справочника по библиотеке.
Введение
Регулярные выражения (сокращенно RE, или regex, или regex-шаблоны) представляют собой небольшой, специализированный язык программирования, встроенный в Python и доступный через модуль re. С помощью этого языка вы можете задать правила для множества возможных строк, которые хотите сопоставить; это множество может содержать английские предложения, адреса электронной почты, команды TeX или что угодно. Вы можете задавать вопросы, такие как «Соответствует ли эта строка шаблону?» или «Есть ли совпадение с шаблоном где-либо в этой строке?». Вы также можете использовать RE для изменения строки или ее разделения различными способами.
Шаблоны регулярных выражений компилируются в серию байткодов, которые затем выполняются движком сопоставления, написанным на языке C. Для продвинутого использования может потребоваться уделить особое внимание тому, как движок будет выполнять данное RE, и записать RE определенным образом, чтобы получить байткоды, выполняющиеся быстрее. Оптимизация не рассматривается в данном документе, так как она требует хорошего понимания внутренних механизмов движка сопоставления.
Язык регулярных выражений относительно невелик и ограничен, поэтому не все возможные задачи по обработке строк можно выполнить с помощью регулярных выражений. Также есть задачи, которые можно выполнить с помощью регулярных выражений, но выражения оказываются очень сложными. В таких случаях может быть лучше написать код Python для обработки; хотя код Python будет медленнее, чем сложное регулярное выражение, он, вероятно, будет более понятным.
Простые шаблоны
Мы начнем с изучения самых простых регулярных выражений. Поскольку регулярные выражения используются для работы со строками, мы начнем с наиболее распространенной задачи: сопоставления символов.
Для подробного объяснения компьютерных наук, лежащих в основе регулярных выражений (детерминированные и недетерминированные конечные автоматы), вы можете обратиться к любой книге по написанию компиляторов.
Сопоставление символов
Большинство букв и символов просто сопоставляются самим собой. Например, регулярное выражение test точно сопоставится со строкой test. (Вы можете включить режим, не учитывающий регистр, который позволит этому регулярному выражению сопоставиться и с Test, и с TEST; об этом позже.)
Есть исключения из этого правила; некоторые символы являются специальными метасимволами и не сопоставляются сами по себе. Вместо этого они сигнализируют о необходимости сопоставления чего-то необычного или влияют на другие части регулярного выражения, повторяя их или изменяя их значение. Большая часть этого документа посвящена обсуждению различных метасимволов и их функций.
Вот полный список метасимволов; их значения будут обсуждаться в остальной части этого руководства.
. ^ $ * + ? { } [ ] \ | ( )
Первые метасимволы, которые мы рассмотрим, — это [ и ]. Они используются для указания класса символов, который представляет собой набор символов, которые вы хотите сопоставить. Символы могут быть перечислены индивидуально или диапазон символов может быть указан с помощью двух символов, разделенных '-'. Например, [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 — как верхний предел бесконечность.
Простейший случай {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 с использованием этого обозначения «сырых строк».
Кроме того, специальные последовательности экранирования, допустимые в регулярных выражениях, но недопустимые в строковых литералах Python, теперь приводят к предупреждению DeprecationWarning и в конечном итоге станут SyntaxError, что означает, что последовательности будут недопустимы, если не используется обозначение «сырой строки» или не экранируются обратные слэши.
Обычная строка | Строка «сырого» формата |
|---|---|
|
|
|
|
|
|
Выполнение соответствий
После того, как у вас есть объект, представляющий скомпилированное регулярное выражение, что вы с ним делаете? Объекты шаблонов имеют несколько методов и атрибутов. Здесь будут рассмотрены только самые важные; обратитесь к документации re для полного списка.
Метод/Атрибут | Назначение |
|---|---|
| Определить, соответствует ли РЕ началу строки. |
| Просматривает строку, ища любое место, где это РЕ соответствует. |
| Найти все подстроки, где соответствует РЕ, и вернуть их в виде списка. |
| Найти все подстроки, где соответствует РЕ, и вернуть их как итератор. |
match() и search() возвращают None в случае отсутствия совпадения. Если они успешно, возвращается экземпляр объекта совпадения, содержащий информацию о совпадении: где оно начинается и заканчивается, подстрока, которой оно соответствует, и многое другое.
Вы можете узнать об этом, взаимодействуя с модулем re. Если у вас есть tkinter, вы также можете посмотреть на Tools/demo/redemo.py, демонстрационную программу, включенную в дистрибутив Python. Она позволяет вводить РЕ и строки и отображает, соответствует ли РЕ или нет. redemo.py может быть очень полезным при отладке сложного РЕ.
В этом HOWTO используются стандартный интерпретатор 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 '>
Внутри этих функций просто создается объект шаблона для вас и вызывается соответствующий метод. Они также сохраняют скомпилированный объект в кэше, поэтому последующие вызовы с тем же РЕ не будут анализировать шаблон снова и снова.
Стоит ли использовать эти функции уровня модуля или получать шаблон и вызывать его методы самостоятельно? Если вы обращаетесь к regex внутри цикла, предварительная компиляция сэкономит несколько вызовов функций. Вне циклов особой разницы нет благодаря внутреннему кэшу.
Флаги компиляции
Флаги компиляции позволяют изменять некоторые аспекты работы регулярных выражений. Флаги доступны в модуле re под двумя именами, длинным именем, например, IGNORECASE, и коротким, однобуквенным, например, I. (Если вы знакомы с модификаторами шаблонов Perl, однобуквенные формы используют те же буквы; короткая форма re.VERBOSE — re.X, например.) Несколько флагов можно указать, используя побитовое ИЛИ; re.I | re.M устанавливает флаги I и M, например.
Вот таблица доступных флагов, за которой следует более подробное объяснение каждого из них.
Флаг | Значение |
|---|---|
Заставляет несколько экранированных символов, например | |
Заставляет символ | |
Производить сопоставление без учёта регистра. | |
Производить сопоставление с учётом текущего языка. | |
Многострочное сопоставление, влияющее на | |
Включает расширенные регулярные выражения, которые можно организовать более чисто и понятно. |
- re.I
- re.IGNORECASE
-
Выполнять сопоставление без учёта регистра; классы символов и литерные строки будут сопоставлять буквы, игнорируя регистр. Например,
[A-Z]также будет сопоставлять строчные буквы. Полное соответствие Юникоду также работает, если флагASCIIне используется для отключения соответствия не-ASCII символов. При использовании юникод шаблонов[a-z]или[A-Z]в сочетании с флагомIGNORECASEони будут сопоставлять 52 буквы ASCII и 4 дополнительные не-ASCII буквы: ‘İ’ (U+0130, заглавная буква I с точкой сверху), ‘ı’ (U+0131, строчная буква i без точки), ‘ſ’ (U+017F, строчная буква с длинным s) и ‘K’ (U+212A, символ Кельвина).Spamбудет сопоставлять'Spam','spam','spAM', или'ſpam'(последнее сопоставляется только в режиме Юникода). Это приведение к нижнему регистру не учитывает текущий язык; это будет учитываться, если вы также установите флагLOCALE.
- re.L
- re.LOCALE
-
Заставляет
\w,\W,\b,\Bи сопоставление без учёта регистра зависеть от текущего языка вместо базы данных Юникода.Локали — это функция библиотеки C, предназначенная для написания программ, учитывающих различия языков. Например, если вы обрабатываете закодированный французский текст, вы захотите написать
\w+для сопоставления слов, но\wсоответствует только классу символов[A-Za-z]в шаблонах байт; он не будет сопоставлять байты, соответствующиеéилиç. Если ваша система настроена должным образом и выбрана французская локаль, определённые функции C скажут программе, что байт, соответствующийé, также должен рассматриваться как буква. Установка флагаLOCALEпри компиляции регулярного выражения заставит полученный скомпилированный объект использовать эти функции C для\w; это медленнее, но также позволяет\w+сопоставлять французские слова так, как ожидается. Использование этого флага не рекомендуется в Python 3, поскольку механизм локали очень ненадежный, он обрабатывает только одну «культуру» за раз и работает только с 8-битными локалями. Сопоставление Юникода уже включено по умолчанию в Python 3 для шаблонов Юникода (str), и он может обрабатывать различные локали/языки.
- re.M
- re.MULTILINE
-
(
^и$ещё не объяснены; они будут представлены в разделе Дополнительные метасимволы.)Обычно
^соответствует только началу строки, а$соответствует только концу строки и сразу перед переводом строки (если он есть) в конце строки. При указании этого флага^соответствует началу строки и началу каждой строки в строке сразу после каждого перевода строки. Аналогично, метасимвол$соответствует либо концу строки, либо концу каждой строки (непосредственно перед каждым переводом строки).
- re.S
- re.DOTALL
-
Делает специальный символ
'.'сопоставляющим любой символ, включая перевод строки; без этого флага'.'будет сопоставлять всё, *кроме* перевода строки.
- re.A
- re.ASCII
-
Заставляет
\w,\W,\b,\B,\sи\Sвыполнять сопоставление только с символами ASCII вместо полного сопоставления Юникода. Это имеет смысл только для шаблонов Юникода и игнорируется для шаблонов байтов.
- re.X
- re.VERBOSE
-
Этот флаг позволяет писать регулярные выражения, которые легче читать, предоставляя большую гибкость в том, как вы можете их форматировать. Когда этот флаг установлен, пробелы в строке RE игнорируются, за исключением случаев, когда пробелы находятся в классе символов или предшествуют неэкранированной обратной косой черте; это позволяет организовать и отступать RE более четко. Этот флаг также позволяет помещать комментарии в RE, которые будут игнорироваться движком; комментарии отмечаются
'#', которые не находятся в классе символов или не предшествуют неэкранированной обратной косой черте.Например, вот RE, использующий
re.VERBOSE; как стало легче читать?charref = re.compile(r""" &[#] # Start of a numeric entity reference ( 0[0-7]+ # Octal form | [0-9]+ # Decimal form | x[0-9a-fA-F]+ # Hexadecimal form ) ; # Trailing semicolon """, re.VERBOSE)Без установки verbose RE выглядел бы так:
charref = re.compile("&#(0[0-7]+" "|[0-9]+" "|x[0-9a-fA-F]+);")В приведенном выше примере использована автоматическая конкатенация строковых литералов Python для разделения RE на меньшие части, но он всё ещё сложнее для понимания, чем версия, использующая
re.VERBOSE.
Дополнительная мощь шаблонов
До сих пор мы рассмотрели лишь часть возможностей регулярных выражений. В этом разделе мы рассмотрим новые метасимволы и как использовать группы для извлечения фрагментов текста, которые были сопоставлены.
Дополнительные метасимволы
Есть некоторые метасимволы, которые мы еще не рассматривали. Большинство из них будет рассмотрено в этом разделе.
Некоторые из оставшихся метасимволов, которые будут обсуждаться, — это утверждения нулевой длины. Они не заставляют движок продвигаться по строке; вместо этого они вообще не потребляют символы и просто выполняют или не выполняют условие. Например, \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. Если вы не используете строковые литералы raw, 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 также используют обратную косую черту, за которой следуют числа, чтобы разрешить включение произвольных символов в строку, поэтому обязательно используйте строку raw при включении обратных ссылок в регулярное выражение.
Например, следующее регулярное выражение обнаруживает удвоенные слова в строке.
>>> 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$)[^.]*$
Изменение строк
До этого момента мы просто выполняли поиск в статической строке. Регулярные выражения также часто используются для изменения строк различными способами, используя следующие методы шаблонов:
Метод/Атрибут | Цель |
|---|---|
| Разделить строку на список, разделяя её там, где совпадает 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 в строке на замену замена. Если шаблон не найден, строка возвращается без изменений.
Необязательный аргумент count — максимальное количество вхождений шаблона, которые должны быть заменены; count должно быть целым неотрицательным числом. Значение по умолчанию 0 означает замену всех вхождений.
Вот простой пример использования метода sub(). Он заменяет имена цветов словом colour:
>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'
Метод subn() выполняет ту же работу, но возвращает пару из двух элементов, содержащую новую строку и количество выполненных замен:
>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)
Пустые совпадения заменяются только тогда, когда они не прилегают к предыдущему пустому совпадению.
>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'
Если замена является строкой, все обратные слэши в ней обрабатываются. То есть, \n преобразуется в один символ новой строки, \r преобразуется в возврат каретки и так далее. Неизвестные escape-последовательности, такие как \& оставляются без изменений. Ссылки на обратные ссылки, такие как \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}'
замена также может быть функцией, что даёт вам ещё больше контроля. Если замена является функцией, функция вызывается для каждого неперекрывающегося вхождения 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–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/howto/regex.html