HOWTO по регулярным выражениям

Автор

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

Аннотация

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

Введение

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

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

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

Простые узоры

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

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

Соответствие символов

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

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

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

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

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

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

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

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

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

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

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

Повторяющиеся вещи

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

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

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

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

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

Шаг

Соответствует

Пояснение

1

a

В RE совпадает a.

2

abcbd

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

3

Неудача

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

4

abcb

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

5

Неудача

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

6

abc

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

6

abcb

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Чума обратного удара

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

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

Персонажи

Сцена

\section

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

\\section

Обратный слеш для re.compile()

"\\\\section"

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

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

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

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

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

Необработанная строка

"ab*"

r"ab*"

"\\\\section"

r"\\section"

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

r"\w+\s+\1"

Выполнение матчей

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

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

Назначение

match()

Определите, совпадает ли RE с началом строки.

search()

Сканирование строки в поисках любого места, где встречается этот RE.

findall()

Находит все подстроки, в которых RE совпадает, и возвращает их в виде списка.

finditer()

Находит все подстроки, в которых RE совпадает, и возвращает их в виде iterator.

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

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

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

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

Теперь вы можете попробовать сопоставить различные строки с RE [a-z]+. Пустая строка не должна соответствовать вообще, поскольку + означает «одно или несколько повторений». В этом случае match() должен вернуть None, что заставит интерпретатор не выводить никакого результата. Вы можете явно вывести результат match(), чтобы это было понятно.

>>> p.match("")
>>> print(p.match(""))
None

Теперь попробуем его на строке, которой он должен соответствовать, например tempo. В этом случае match() вернет match object, поэтому результат следует сохранить в переменной для дальнейшего использования.

>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>

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

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

Назначение

group()

Возвращает строку, совпадающую с RE

start()

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

end()

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

span()

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

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

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

group() возвращает подстроку, с которой совпал RE. start() и end() возвращают начальный и конечный индекс совпадения. span() возвращает начальный и конечный индексы в одном кортеже. Поскольку метод match() проверяет только совпадение RE в начале строки, 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)

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

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

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

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

Префикс r, делающий литерал необработанным строковым литералом, необходим в этом примере, потому что экранирующие последовательности в обычном «приготовленном» строковом литерале, не распознаваемые Python, в отличие от регулярных выражений, теперь приводят к DeprecationWarning и в конечном итоге станут SyntaxError. См. Чума обратного удара.

findall() должен создать весь список, прежде чем он будет возвращен в качестве результата. Метод finditer() возвращает последовательность экземпляров match object в виде iterator:

>>> 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() и так далее. Эти функции принимают те же аргументы, что и соответствующий метод шаблона, с добавлением строки RE в качестве первого аргумента, и возвращают либо None, либо экземпляр match object.

>>> 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, не придется снова и снова разбирать шаблон.

Следует ли вам использовать эти функции на уровне модуля или лучше получить шаблон и вызвать его методы самостоятельно? Если вы обращаетесь к regex в цикле, предварительная компиляция сэкономит несколько вызовов функций. За пределами циклов разница невелика благодаря внутреннему кэшу.

Флаги компиляции

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

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

I
IGNORECASE

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

L
LOCALE

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

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

M
MULTILINE

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

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

S
DOTALL

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

A
ASCII

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

X
VERBOSE

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

Например, вот RE, в котором используется re.VERBOSE; видите, насколько легче читать?

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

Без настройки verbose, RE будет выглядеть следующим образом:

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

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

Больше силы узора

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

Больше метахарактеров

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

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

|

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

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

^

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

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

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

Для соответствия литералу '^' используйте \^.

$

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

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

Для соответствия литералу '$' используйте \$ или заключите его внутри класса символов, как в [$].

\A

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

\Z

Выполняет поиск только в конце строки.

\b

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

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

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

Есть две тонкости, которые следует помнить при использовании этой специальной последовательности. Во-первых, это худшее столкновение между строковыми литералами Python и последовательностями регулярных выражений. В строковых литералах Python символ \b - это символ обратного пробела, ASCII значение 8. Если вы не используете необработанные строки, то Python преобразует \b в пробел, и ваш RE не будет соответствовать ожидаемому. Следующий пример выглядит так же, как и наш предыдущий RE, но опускает 'r' перед строкой RE.

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

Группировка

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

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

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

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

Группы без захвата и именные группы

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

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

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

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

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

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

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

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

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

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

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

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

Утверждения на будущее

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

(?=...)

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

(?!...)

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

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

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

Необязательный аргумент 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 преобразуется в возврат каретки, и так далее. Неизвестные символы, такие как \&, остаются без внимания. Обратные ссылки, такие как \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}'

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

В следующем примере функция замены переводит десятичные цифры в шестнадцатеричные:

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

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

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

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

Жадные против нежадных

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

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

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

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

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

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

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

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

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

pat = re.compile(r"""
 \s*                 # Skip leading whitespace
 (?P<header>[^:]+)   # Header name
 \s* :               # Whitespace, and a colon
 (?P<value>.*?)      # The header's value -- *? used to
                     # lose the following trailing whitespace
 \s*$                # Trailing whitespace to end-of-line
""", re.VERBOSE)

Это гораздо более читабельно, чем:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

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

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

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

Вернуться на верх