Spec-Zone.ru › Python 3.8

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

Автор

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

Аннотация

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

Введение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

\d

Сопоставляет любую десятичную цифру; это эквивалентно классу [0-9].

\D

Сопоставляет любой символ, не являющийся цифрой; это эквивалентно классу [^0-9].

\s

Сопоставляет любой пробельный символ; это эквивалентно классу [ \t\n\r\f\v].

\S

Сопоставляет любой не-пробельный символ; это эквивалентно классу [^ \t\n\r\f\v].

\w

Сопоставляет любой буквенно-цифровой символ; это эквивалентно классу [a-zA-Z0-9_].

\W

Сопоставляет любой не-буквенно-цифровой символ; это эквивалентно классу [^a-zA-Z0-9_].

Эти последовательности могут быть включены внутри класса символов. Например, [\s,.] — это класс символов, который сопоставится с любым пробельным символом, или ',' или '.'.

Последний метасимвол в этом разделе — .. Он сопоставляет любой символ, кроме символа новой строки, и существует альтернативный режим (re.DOTALL), где он будет сопоставляться даже с символом новой строки. . часто используется, когда вы хотите сопоставить «любой символ».

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

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

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

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

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

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

Шаг

Совпадение

Объяснение

1

a

Символ a в РЕ сопоставился.

2

abcbd

Движок сопоставил [bcd]*, дойдя до конца строки.

3

Ошибка

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

4

abcb

Возврат назад, чтобы [bcd]* сопоставлялся с одним символом меньше.

5

Ошибка

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

6

abc

Ещё раз возврат назад, так что [bcd]* сопоставляется только с bc.

6

abcb

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

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

Другой метасимвол для повторения — это +, который сопоставляется один или более раз. Обратите особое внимание на разницу между * и +; * сопоставляется ноль или более раз, поэтому то, что повторяется, может вообще отсутствовать, в то время как + требует по крайней мере одного вхождения. Используя аналогичный пример, ca+t сопоставит 'cat' (1 'a') и 'caaat' (3 'a'), но не сопоставит 'ct'.

Есть ещё два квалификатора повторения. Символ вопроса ? сопоставляется один раз или ноль раз; можно рассматривать его как указание на то, что что-то является необязательным. Например, home-?brew сопоставляется либо с 'homebrew', либо с 'home-brew'.

Наиболее сложным квалификатором повторения является {m,n}, где m и n — десятичные целые числа. Этот квалификатор означает, что должно быть по меньшей мере m повторений и не более n. Например, a/{1,3}b будет сопоставляться с 'a/b', 'a//b' и 'a///b'. Он не будет сопоставляться с 'ab', у которого нет косых черт, или с 'a////b', у которого четыре.

Можно опустить либо m, либо n; в этом случае для отсутствующего значения принимается разумное значение. Пропуск m интерпретируется как нижний предел 0, а пропуск n даёт верхний предел бесконечности.

Читатели, склонные к редукционизму, могут заметить, что три других квалификатора могут быть выражены с помощью этой нотации. {0,} эквивалентно *, {1,} эквивалентно +, и {0,1} эквивалентно ?. Лучше использовать *, + или ?, когда это возможно, просто потому, что они короче и легче читаются.

Использование регулярных выражений

Теперь, когда мы рассмотрели некоторые простые регулярные выражения, как их использовать в Python? Модуль re предоставляет интерфейс к движку регулярных выражений, позволяющий компилировать РЕ в объекты и затем выполнять с ними сопоставления.

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

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

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

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

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

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

Использование РЕ в виде строк упрощает язык Python, но имеет один недостаток, который будет рассмотрен в следующей секции.

Проблема обратной косой черты

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

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

Символы

Этап

\section

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

\\section

Экранированная обратная косая черта для re.compile()

"\\\\section"

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

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

Решение заключается в использовании в Python обозначения «сырой» строки для регулярных выражений; обратные косые черты не обрабатываются каким-либо специальным образом в строковом литерале, предваряемом 'r', поэтому r"\n" — это строка из двух символов, содержащая '\' и 'n', а "\n" — это строка из одного символа, содержащая символ новой строки. Регулярные выражения часто записываются в коде Python с использованием этого обозначения «сырых» строк.

Кроме того, специальные escape-последовательности, которые допустимы в регулярных выражениях, но не допустимы как строковые литералы Python, теперь приводят к предупреждению DeprecationWarning и в конечном итоге к ошибке SyntaxError, что означает, что последовательности будут недопустимы, если не используется обозначение «сырой» строки или экранирование обратных косых черт.

Обычная строка

Строка ссырой

"ab*"

r"ab*"

"\\\\section"

r"\\section"

"\\w+\\s+\\1"

r"\w+\s+\1"

Выполнение сопоставлений

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

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

Назначение

match()

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

search()

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

findall()

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

finditer()

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

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

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

Этот 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'>

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

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

Назначение

group()

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

start()

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

end()

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

span()

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

Пробы этих методов вскоре прояснят их смысл:

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

group() возвращает подстроку, которая соответствовала регулярному выражению. start() и end() возвращают начальный и конечный индексы совпадения. span() возвращает оба индекса начала и конца в одном кортеже. Поскольку метод match() проверяет только, соответствует ли регулярное выражение началу строки, start() всегда будет равно нулю. Однако метод search() шаблонов просматривает строку, поэтому совпадение может не начинаться с нуля в этом случае.

>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)

В реальных программах наиболее распространенный стиль заключается в хранении объекта совпадения в переменной и проверке, было ли оно None. Это обычно выглядит так:

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print('Match found: ', m.group())
else:
    print('No match')

Два метода шаблона возвращают все совпадения для шаблона. findall() возвращает список сопоставленных строк:

>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

Префикс r, делающий литерал сырой строкой, необходим в этом примере, потому что последовательности 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, например.

Ниже приведена таблица доступных флагов, за которой следует более подробное объяснение каждого из них.

Флаг

Значение

ASCII, A

Заставляет несколько эскейпов, таких как \w, \b, \s и \d соответствовать только ASCII-символам с соответствующим свойством.

DOTALL, S

Заставляет . соответствовать любому символу, включая символы новой строки.

IGNORECASE, I

Производит сопоставление без учета регистра.

LOCALE, L

Производит сопоставление с учетом локали.

MULTILINE, M

Сопоставление по нескольким строкам, влияющее на ^ и $.

VERBOSE, X (для «расширенного»)

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

I
IGNORECASE

Выполняет сопоставление без учёта регистра; классы символов и литеральные строки будут сопоставлять буквы, игнорируя регистр. Например, [A-Z] также будет соответствовать строчным буквам. Полное сопоставление по Юникоду также работает, если не используется флаг ASCII для отключения сопоставлений, не относящихся к ASCII. Когда шаблоны Юникода [a-z] или [A-Z] используются в сочетании с флагом IGNORECASE, они будут соответствовать 52 ASCII буквам и 4 дополнительным не-ASCII буквам: ‘İ’ (U+0130, заглавная буква I с точкой сверху), ‘ı’ (U+0131, строчная буква i без точки), ‘ſ’ (U+017F, строчная буква длинное s) и ‘K’ (U+212A, знак Кельвина). Spam будет соответствовать 'Spam', 'spam', 'spAM', или 'ſpam' (последнее сопоставляется только в режиме Юникода). Эта приведение к нижнему регистру не учитывает текущую локаль; это будет учитывать, если вы также установите флаг LOCALE.

L
LOCALE

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

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

M
MULTILINE

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

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

S
DOTALL

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

A
ASCII

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

X
VERBOSE

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

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

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

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

Больше возможностей шаблонов

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

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

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

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

|

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

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

^

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

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

>>> print(re.search('^From', 'From Here to Eternity'))  
<re.Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None

Чтобы сопоставить литеру '^', используйте \^.

$

Соответствует концу строки, которая определяется как либо конец строки, либо любое место, за которым следует символ новой строки.

>>> print(re.search('}$', '{block}'))  
<re.Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))  
<re.Match object; span=(6, 7), match='}'>

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

\A

Соответствует только началу строки. Когда не используется режим MULTILINE, \A и ^ фактически одинаковы. В режиме MULTILINE они отличаются: \A все равно соответствует только началу строки, но ^ может соответствовать любому месту внутри строки, следующему за символом новой строки.

\Z

Соответствует только концу строки.

\b

Граница слова. Это утверждение нулевой ширины, которое соответствует только началу или концу слова. Слово определяется как последовательность буквенно-цифровых символов, поэтому конец слова обозначается пробелом или небуквенно-цифровым символом.

Следующий пример сопоставляет class только тогда, когда это целое слово; он не будет сопоставляться, если он находится внутри другого слова.

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None

Есть два нюанса, которые следует помнить при использовании этой специальной последовательности. Во-первых, это худшее столкновение между строковыми литералами Python и последовательностями регулярных выражений. В строковых литералах Python \b — это символ удаления обратного слеша, ASCII-значение 8. Если вы не используете строковые литералы с префиксом «r», 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 также используют обратный слэш, за которым следуют цифры, для включения произвольных символов в строку, поэтому обязательно используйте строку с префиксом «r» при включении обратных ссылок в регулярное выражение.

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

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

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

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

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

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

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

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

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

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

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

За исключением того, что вы не можете получить содержимое того, что группа соответствовала, незахватывающая группа ведет себя точно так же, как захватывающая группа; вы можете поместить в неё что угодно, повторить её с метасимволом повторения, таким как *, и вложить её в другие группы (захватывающие или незахватывающие). (?:...) особенно полезно при модификации существующего шаблона, так как вы можете добавлять новые группы, не изменяя нумерацию всех остальных групп. Следует упомянуть, что в поиске нет разницы в производительности между захватывающими и незахватывающими группами; ни одна форма не быстрее другой.

Более важной особенностью являются именованные группы: вместо того, чтобы ссылаться на них по номерам, группы можно ссылаться по имени.

Синтаксис именованной группы — это одно из расширений, специфичных для Python: (?P<name>...). name, очевидно, имя группы. Именованные группы ведут себя точно так же, как захватывающие группы, и дополнительно связывают имя с группой. Методы объекта сопоставления, которые работают с захватывающими группами, принимают либо целые числа, которые ссылаются на группу по номеру, либо строки, содержащие имя нужной группы. Именованные группы всё равно получают номера, поэтому вы можете получить информацию о группе двумя способами:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

Кроме того, вы можете получить именованные группы в виде словаря с помощью groupdict():

>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe')
>>> m.groupdict()
{'first': 'Jane', 'last': 'Doe'}

Именованные группы удобны, так как позволяют использовать легко запоминающиеся имена вместо запоминания номеров. Вот пример регулярного выражения из модуля imaplib:

InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

Очевидно, гораздо проще получить m.group('zonem'), вместо того, чтобы помнить, как получить группу 9.

Синтаксис обратных ссылок в выражении, таком как (...)\1, относится к номеру группы. Естественно, есть вариант, который использует имя группы вместо номера. Это ещё одно расширение Python: (?P=name) указывает, что содержимое группы с именем name должно снова соответствовать текущей позиции. Регулярное выражение для поиска слов, повторяющихся дважды, \b(\w+)\s+\1\b также можно записать как \b(?P<word>\w+)\s+(?P=word)\b:

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

Утверждения поиска вперёд

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

(?=...)

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

(?!...)

Утверждение отрицательного поиска вперёд. Это противоположность положительному утверждению; оно выполняется, если содержащееся выражение не совпадает в текущей позиции в строке.

Чтобы это стало понятнее, давайте рассмотрим случай, когда утверждение поиска вперёд полезно. Рассмотрим простой шаблон для сопоставления имени файла и разделения его на имя базы и расширение, разделённые .. Например, в news.rc, news — это имя базы, а rc — расширение файла.

Шаблон для сопоставления этого достаточно прост:

.*[.].*$

Обратите внимание, что . нужно обрабатывать специально, так как это метасимвол, поэтому он находится внутри класса символов, чтобы соответствовать только этому конкретному символу. Также обратите внимание на конечный $; это добавлено для того, чтобы убедиться, что вся остальная часть строки должна быть включена в расширение. Это регулярное выражение соответствует foo.bar и autoexec.bat и sendmail.cf и printers.conf.

Теперь давайте немного усложним задачу; что если вы хотите сопоставить имена файлов, где расширение не bat? Некоторые неправильные попытки:

.*[.][^b].*$ Первая попытка выше пытается исключить bat, потребовав, чтобы первый символ расширения не был b. Это неправильно, так как шаблон также не соответствует foo.bar.

.*[.]([^b]..|.[^a].|..[^t])$

Выражение становится более запутанным, когда вы пытаетесь исправить первое решение, потребовав соответствия одному из следующих случаев: первый символ расширения не b; второй символ не a; или третий символ не t. Это принимает foo.bar и отвергает autoexec.bat, но требует расширение из трёх символов и не будет принимать имя файла с расширением из двух символов, такое как sendmail.cf. Мы снова усложним шаблон, пытаясь исправить его.

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

В третьей попытке второй и третий символы сделаны необязательными, чтобы разрешить соответствие расширениям короче трёх символов, таким как sendmail.cf.

Теперь шаблон становится действительно сложным, что затрудняет его чтение и понимание. Хуже того, если проблема изменится, и вы захотите исключить оба bat и exe в качестве расширений, шаблон станет ещё более сложным и запутанным.

Отрицательное утверждение поиска вперёд прорывается через всю эту путаницу:

.*[.](?!bat$)[^.]*$ Отрицательное утверждение поиска вперёд означает: если выражение bat не соответствует в этой точке, попробуйте остальную часть шаблона; если bat$ соответствует, весь шаблон потерпит неудачу. Конечный $ необходим для того, чтобы такие имена файлов, как sample.batch, где расширение начинается только с bat, были разрешены. [^.]* гарантирует, что шаблон будет работать, когда в имени файла есть несколько точек.

Исключение другого расширения имени файла теперь просто; достаточно добавить его как альтернативу внутри утверждения. Следующий шаблон исключает имена файлов, заканчивающиеся либо bat , либо exe:

.*[.](?!bat$|exe$)[^.]*$

Изменение строк

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

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

Назначение

split()

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

sub()

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

subn()

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

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

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

.split(string[, maxsplit=0])

Разделить строку string по совпадениям регулярного выражения. Если в RE используются группирующие скобки, то их содержимое также будет возвращено как часть результирующего списка. Если maxsplit не равно нулю, выполняется не более maxsplit разделений.

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

>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']

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

>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']

Функция модуля re.split() добавляет используемое RE в качестве первого аргумента, но в остальном она такая же.

>>> re.split(r'[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split(r'([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split(r'[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

Поиск и замена

Еще одна распространенная задача — найти все совпадения с шаблоном и заменить их другой строкой. Метод sub() принимает значение замены, которое может быть либо строкой, либо функцией, и строку, которую нужно обработать.

.sub(replacement, string[, count=0])

Возвращает строку, полученную заменой левых неперекрывающихся вхождений RE в string заменой replacement. Если шаблон не найден, возвращается string без изменений.

Необязательный аргумент count — максимальное количество вхождений шаблона, которые нужно заменить; count должен быть неотрицательным целым числом. Значение по умолчанию 0 означает замену всех вхождений.

Вот простой пример использования метода sub(). Он заменяет имена цветов словом colour:

>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'

Метод subn() выполняет ту же работу, но возвращает кортеж из 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 преобразуется в возврат каретки и так далее. Неизвестные 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}'

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.8/howto/regex.html

Spec-Zone.ru

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