Глава 5. Регулярные выражения

Добавлено 14 апреля 2020 в 22:47

Некоторые люди, во время решения одной проблемы думают: «Я знаю, я буду использовать регулярные выражения». Теперь у них две проблемы…

Джейми Завински

Содержание главы

Погружение

Каждый новый язык программирования содержит встроенные функции для работы со строками. В Python, у строк есть методы для поиска и замены: index(), find(), split(), count(), replace() и т.д. Но эти методы ограничены для простейших случаев. Например метод index() ищет простую жёстко заданную часть строки и поиск всегда регистрозависимый. Чтобы выполнить регистронезависимый поиск по строке s, вы должны вызвать s.lower() или s.upper() для того чтобы быть уверенным что строка имеет соответствующий регистр для поиска. Методы replace() и split() имеют те же ограничения.

Если ваша задача может быть решена при помощи этих методов, лучше использовать их. Они простые и быстрые, легко читаемые; о быстром, простом и удобочитаемом коде можно много рассказывать. Но если вы обнаружите что вы используете большое количество строковых функций с условиями if для обработки специальных случаев, или используете множество последовательных вызовов split() и join() чтобы нарезать на кусочки ваши строки, значит вы нуждаетесь в регулярных выражениях.

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

Если вы пользовались регулярными выражениями в других языках (таких как Perl, JavaScript, или PHP), синтаксис Python'а будет для вас достаточно привычным. Прочитайте обзор модуля re, чтобы узнать о доступных функциях и их аргументах.

5.2 Учебный пример: адрес улицы

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

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')                ①
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')                ②
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')  ③
'100 NORTH BROAD RD.'
>>> import re                               ④
>>> re.sub('ROAD$', 'RD.', s)               ⑤
'100 NORTH BROAD RD.'
  1. Строка 2. Моя задача стандартизировать адрес улицы, например 'ROAD' всегда выражается сокращением 'RD.'. На первый взгляд мне показалось, что это достаточно просто, и я могу использовать метод replace(). В конце концов, все данные уже в верхнем регистре и несовпадение регистра не составит проблемы. Строка поиска 'ROAD' являлась константой, и обманчиво простой пример s.replace() вероятно работает.
  2. Строка 5. Жизнь же, напротив, полна противоречивых примеров, и я быстро обнаружил один из них. Проблема заключалась в том что 'ROAD' появилась в адресе дважды, один раз как 'ROAD', а во второй как часть названия улицы 'BROAD'. Метод replace() обнаруживал 2 вхождения и слепо заменял оба, разрушая таким образом правильный адрес.
  3. Строка 7. Чтобы решить эту проблему вхождения более одной подстроки 'ROAD', вам необходимо прибегнуть к следующему: искать и заменять 'ROAD' в последних четырёх символах адреса (s[-4:]), оставляя строку отдельно (s[:-4]). Как вы могли заметить, это уже становится громоздким. К примеру, шаблон зависит от длины заменяемой строки. (Если вы заменяли 'STREET' на 'ST.', вам придется использовать s[:-6] и s[-6:].replace(...).) Не хотели бы вы вернуться к этому коду через полгода для отладки? Я не хотел бы.
  4. Строка 9. Пришло время перейти к регулярным выражениям. В Python все функции, связанные с регулярными выражениями содержится в модуле re.
  5. Строка 10. Взглянем на первый параметр: 'ROAD$'. Это простое регулярное выражение которое находит 'ROAD' только в конце строки. Знак $ означает «конец строки». (Также существует символ ^, означающий «начало строки».) Используя функцию re.sub(), вы ищете в строке s регулярное выражение 'ROAD$' и заменяете на 'RD.'. Оно совпадает с 'ROAD' в конце строки s, но не совпадает с 'ROAD', являющимся частью названия 'BROAD', так как оно находится в середине строки s.

^ означает начало строки. $ означает конец строки.

Продолжая историю про обработку адресов, я скоро обнаружил, что предыдущий пример совпадения 'ROAD' на конце адреса был недостаточно хорош, так как не все адреса включали в себя определение улицы. Некоторые адреса просто оканчивались названием улицы. Я избегал этого в большинстве случаев, но если название улицы было 'BROAD', тогда регулярное выражение совпадало с 'ROAD' на конце строки 'BROAD', чего я совершенно не хотел.

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)   ①
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)   ②
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)   ③
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s)  ④
'100 BROAD RD. APT 3'
  1. Строка 4. В действительности я хотел совпадения с 'ROAD', когда оно на конце строки и является самостоятельным словом (а не частью большего). Чтобы описать это в регулярном выражении необходимо использовать '\b', что означает «слово должно оказаться прямо тут.» В Python это сложно, так как знак '\' в строке должен быть экранирован. Иногда это называют как «бедствие обратного слеша», и это одна из причин, почему регулярные выражения проще в Perl, чем в Python. Однако недостаток Perl в том, что регулярные выражения смешиваются с другим синтаксисом, если у вас появилась ошибка, достаточно сложно определить, где она находится, в синтаксисе или в регулярном выражении.
  2. Строка 6. Чтобы обойти проблему «бедствия обратного слеша», вы можете использовать то, что называется «неформатированной строкой» (raw string) с помощью использования префикса строки с символом 'r'. Это скажет Python'у, что в этой строке ничего не должно быть экранировано; '\t' – это табулятор, но r'\t' – это символ обратного слеша '\' , а следом за ним буква 't'. Я рекомендую всегда использовать неформатированную строку при работе с регулярными выражениями; иначе всё становится достаточно запутанным (несмотря на то, что наше регулярное выражения уже достаточно запутано).
  3. Строка 9. *вздох* К сожалению, я скоро обнаружил еще больше случаев противоречащих моей логике. В этом случае адрес улицы содержал в себе цельное отдельное слово 'ROAD' и оно не было на конце строки, так как после определения улицы адрес содержал номер квартиры. Поскольку слово 'ROAD' не находится в конце строки, регулярное выражение re.sub() его пропускало, и мы получали на выходе ту же строку, что и на входе, а это не то, что было нужно.
  4. Строка 11. Чтобы решить эту проблему я удалил символ '$' и добавил ещё один '\b'. Теперь регулярное выражение совпадало с 'ROAD', если оно являлось цельным словом в любой части строки, на конце, в середине и в начале.

5.3 Учебный пример: римские цифры

Скорее всего вы видели римские цифры, даже если вы в них не разбираетесь. Вы могли видеть их на копирайтах старых фильмов и ТВ-шоу («Copyright MCMXLVI» вместо «Copyright 1946»), или на стенах в библиотеках университетов («учреждено MDCCCLXXXVIII» вместо « учреждено 1888»). Вы могли видеть их в структуре библиографических ссылок. Эта система отображения цифр относится к древней Римской империи (отсюда и название).

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

  • I = 1
  • V = 5
  • X = 10
  • L = 50
  • C = 100
  • D = 500
  • M = 1000

Нижеследующие правила позволяют конструировать римские цифры:

  • Иногда символы складываются. I это 1, II это 2, и III это 3. VI это 6 (посимвольно, «5 и 1»), VII это 7, и VIII это 8.
  • Десятичные символы (I, X, C, и M) могут быть повторены до 3 раз. Для образования 4 вам необходимо отнять от следующего высшего символа пятёрки. Нельзя писать 4 как IIII; вместо этого, она записывается как IV («на 1 меньше 5»). 40 записывается как XL («на 10 меньше 50»), 41 как XLI, 42 как XLII, 43 как XLIII, и 44 как XLIV («на 10 меньше 50, и на 1 меньше 5»).
  • Иногда символы… обратны сложению. Разместив определённые символы до других, вы вычитаете их от конечного значения. Например 9, вам необходимо отнять от следующего высшего символа десять: 8 это VIII, но 9 это IX («на 1 меньше 10»), не VIIII (так как символ I не может быть повторён 4 раза). 90 это XC, 900 это CM.
  • Пятёрки не могут повторяться. 10 всегда отображается как X, никогда как VV. 100 всегда C, никогда LL.
  • Римские цифры читаются слева направо, поэтому положение символа имеет большое значение. DC это 600; CD это совершенно другая цифра (400, «на 100 меньше 500»). CI это 101; IC это даже не является допустимым римским числом (так как вы не можете вычитать 1 прямо из 100; вам необходимо записать это как XCIX, «на 10 меньше 100, и на 1 меньше 10»).

5.3.1 Проверка на тысячи

Что необходимо сделать чтобы проверить что произвольная строка является допустимым римским числом? Давайте будем брать по одному символу за один раз. Так как римские числа всегда записываются от высшего к низшему, начнём с высшего: с тысячной позиции. Для чисел от 1000 и выше, используются символы M.

>>> import re
>>> pattern = '^M?M?M?$'        ①
>>> re.search(pattern, 'M')     ②
<_sre.SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')    ③
<_sre.SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')   ④
<_sre.SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM')  ⑤
>>> re.search(pattern, '')      ⑥
<_sre.SRE_Match object at 0106F4A8>
  1. Строка 2. Этот патерн состоит из трёх частей. ^ совпадает с началом строки. Если его не указать, патерн будет совпадать с М без учёта положения в строке, а это не то что нам надо. Вы должны быть уверены что символы М, если присутствуют, то находятся в начале строки. M? опционально совпадает с одним символом M. Так как это повторяется три раза, то патерн соответствует от нуля до трёх символам М в строке. И символ $ совпадёт с концом строки. Когда он комбинируется с символом ^ в начале, это означает, что патерн должен совпасть с полной строкой, без других символов до и после символов М.
  2. Строка 3. Сущность модуля re – это функция search(), которая использует патерн регулярного выражения (pattern) и строку ('M') и ищет совпадения в соответствии с регулярным выражением. Если совпадение обнаружено, search() возвращает объект который имеет различные методы описания совпадения; если совпадения не обнаружено, search() возвращает None, нулевое значение в Python. Всё, о чём мы заботимся в данный момент, совпадёт ли патерн, а это можно сказать глянув на значение возвращаемое функцией search(). 'M' совпадает с этим регулярным выражением, так как первое опциональное M совпадает, а вторая и третья необязательные М игнорируются.
  3. Строка 5.'MM' совпадает так как первая и вторая необязательные М совпадают, а третья игнорируется
  4. Строка 7.'MMM' совпадает полностью, так как все три символа М совпадают
  5. Строка 9.'MMMM' не совпадает. Все три М совпадают, но регулярное выражение настаивает на конце строки, (из-за наличия символа $), а строка ещё не кончилась (из за четвёртой М). Поэтому search() возвращает None.
  6. Строка 10. Занимательно то, что пустая строка также совпадает с регулярным выражением, так как все символы М необязательны.

5.3.2 Проверка на сотни

? делает патерн необязательным

Расположение сотен более сложное, чем тысяч, так как существует несколько взаимно исключающих путей записи и зависит от значения.

  • 100 = C
  • 200 = CC
  • 300 = CCC
  • 400 = CD
  • 500 = D
  • 600 = DC
  • 700 = DCC
  • 800 = DCCC
  • 900 = CM

Таким образом есть четыре возможных патерна:

  • CM
  • CD
  • от нуля до трёх символов C (ноль, если место сотен пустое)
  • D и последующие от нуля до трёх символов C

Два последних патерна могут быть объединены:

  • необязательное D, а за ним от нуля до трёх символов C

Данный пример показывает, как проверить позицию сотен в римском числе.

>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'  ①
>>> re.search(pattern, 'MCM')             ②
<_sre.SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')              ③
<_sre.SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')          ④
<_sre.SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')            ⑤
>>> re.search(pattern, '')                ⑥
<_sre.SRE_Match object at 01071D98>
  1. Строка 2. Этот патерн начинается также как и предыдущий, проверяя начало строки (^), потом тысячи (M?M?M?). Следом идёт новая часть в скобках, которая описывает три взаимоисключающих патерна, разделённых вертикальными линиями: CM, CD и D?C?C?C? (который соответствует необязательной D и следующими за ней от нуля до трёх необязательными символам C). Парсер регулярного выражения проверяет каждый из этих патернов от левого к правому, выбирая первый подходящий и игнорируя последующие.
  2. Строка 3. 'MCM' совпадает так как первый символ M совпадает, второй и третий символы M игнорируются, символы CM совпадают (и патерны CD и D?C?C?C? после этого не анализируются). MCM – это римское представление числа 1900.
  3. Строка 5. 'MD' совпадает, так как первый символ M совпадает, второй и третий символы M игнорируются, и патерн D?C?C?C? совпадает с D (три символа C необязательны и игнорируются). MD – это римское представление числа 1500.
  4. Строка 7. 'MMMCCC' совпадает так как первый символ M совпадает, и патерн D?C?C?C? сопадает с CCC (символ D необязателен и игнорируются). MMMCCC i это римское представление числа 3300.
  5. Строка 9. 'MCMC' не совпадает. Первый символ M совпадает, второй и третий символы M игнорируются, также совпадает CM, но патерн $ не совпадает, так как вы ещё не в конце строки (у вас есть еще несовпадающий символ C). Символ C не совпадает как часть патерна D?C?C?C?, так как исключающий патерн CM уже совпал.
  6. Строка 10. Занимательно то, что пустая строка всё ещё совпадает с регулярным выражением, так как все символы М необязательны и игнорируются, и пустая строка совпадает с патерном D?C?C?C?, где все символы необязательны и игнорируются.

Вот так так! Вы заметили, как быстро регулярные выражения становятся отвратительными? А мы обработали в римском представлении чисел пока только позиции тысяч и сотен. Но если последуете далее, вы обнаружите, что десятки и единицы описать будет легче, так как они имеют такой же патерн. А пока давайте рассмотрим другой способ описать этот же патерн.

5.4 Использование синтаксиса {n, m}

модификатор {1,4} совпадает с от 1 до 4 вхождениями патерна

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

>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')     ①
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MM')    ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMM')   ③
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM')  ④
>>> 
  1. Строка 3. Тут патерн совпадает с началом строки и первым необязательным символом М, но не со вторым и третьим (но это нормально так как они необязательны), а также с концом строки.
  2. Строка 5. Тут патерн совпадает с началом строки, с первым и вторым необязательными символами М, но не с третьим (это нормально так как он необязателен) и с концом строки.
  3. Строка 7. Тут патерн совпадает с началом строки и со всеми тремя необязательными символами М, а также с концом строки.
  4. Строка 9. Тут патерн совпадает с началом строки и со всеми тремя необязательными символами М, но не совпадает с концом строки (так как присутствует ещё один символ М), таким образом патерн не совпадает и возвращает None.
>>> pattern = '^M{0,3}$'        ①
>>> re.search(pattern, 'M')     ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')    ③
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')   ④
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM')  ⑤
>>> 
  1. Строка 1. Этот патерн говорит: «совпасть с началом строки, потом с от нуля до трёх символов М, и потом с концом строки». Символы 0 и 3 могут быть любыми цифрами, если вам необходимо совпадение минимум с одним и не более чем с тремя символами М, патерн необходимо записать как М{1,3}.
  2. Строка 2. Тут патерн совпадает с началом строки, потом с одним из возможных трёх символов М, потом с концом строки.
  3. Строка 4. Тут патерн совпадает с началом строки, потом с двумя из возможных трёх символов М, потом с концом строки.
  4. Строка 6. Тут патерн совпадает с началом строки, потом с тремя из возможных трёх символов М, потом с концом строки.
  5. Строка 8. Тут патерн совпадает с началом строки, потом с двумя из возможных трёх символов М, но не совпадает с концом строки. Регулярное выражение допускает до трёх символов М до конца строки, но у вас четыре, и патерн возвращает None.

5.4.1 Проверка на десятки и единицы

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

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')     ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')      ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')     ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')   ④
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX')  ⑤
>>> 
  1. Строка 2. Тут патерн совпадает с началом строки, потом с первым необязательным символом М, потом CM, потом XL, потом с концом строки. Вспомните что синтаксис (A|B|C) означает «совпасть только с одним из символов A, B или C» У нас совпадает XL, и мы игнорируем XC и L?X?X?X?, и после этого переходим к концу строки. MCMXL – это римское представление числа 1940.
  2. Строка 4. Тут патерн совпадает с началом строки, потом с первым необязательным символом М, потом CM, потом с L?X?X?X?. Из L?X?X?X? совпадает L, и пропускаются три необязательных символа X. После этого переходим к концу строки. MCML – это римское представление числа 1950.
  3. Строка 6. Тут патерн совпадает с началом строки, потом с первым необязательным символом М, потом CM, потом с необязательным L и первым необязательным X, пропуская второй и третий необязательные символы X, после этого переходит к концу строки. MCMLX – это римское представление числа 1960.
  4. Строка 8. Тут патерн совпадает с началом строки, потом с первым необязательным символом М, потом CM, потом с необязательным L и всеми тремя необязательными символами X, после этого переходит к концу строки. MCMLXXX – это римское представление числа 1980.
  5. Строка 10. Тут патерн совпадает с началом строки, потом с первым необязательным символом М, потом CM, потом с необязательным L и всеми тремя необязательными символами X, после этого не совпадает с концом строки, так как есть ещё один символ X, таким образом патерн не срабатывает и возвращает None. MCMLXXXX – это недопустимое римское число.

(A|B) совпадает либо с A, либо с B.

Для описания единиц подходит тот же патерн. Я пропущу подробности и покажу конечный результат.

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

Итак, как это будет выглядеть при использовании альтернативного синтаксиса {n,m}? Этот пример показывает новый синтаксис.

>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')              ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')          ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII')   ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')                 ④
<_sre.SRE_Match object at 0x008EEB48>
  1. Строка 2. Тут патерн совпадает с началом строки, потом с одним из трёх возможных символов М, потом D?C{0,3}. Из него совпадает только необязательный символ D и ни один из необязательных C. Далее совпадает необязательный символ L из L?X{0,3} и ни один из трёх необязательных символов X. После совпадает с V из V?I{0,3} и ни с одним из трёх необязательных символов I и, наконец, с концом строки. MDLV – это римское представление числа 1555.
  2. Строка 4. Тут патерн совпадает с началом строки, потом с двумя из трёх возможных символов М, потом D и один необязательный символ C из D?C{0,3}. Потом L?X{0,3} с L и один из трёх возможных символов X, потом V?I{0,3} с V и одним из трёх символов I, потом с концом строки. MMDCLXVI – это римское представление числа 2666.
  3. Строка 6. Тут патерн совпадает с началом строки, потом с тремя из трёх M, потом D и C из D?C{0,3}, потом L?X{0,3} с L и три из трёх X, потом V?I{0,3} с V и тремя из трёх I, потом конец строки. MMMDCCCLXXXVIII – это римское представление числа 3888, и это максимально длинное римское число которое можно записать без расширенного синтаксиса.
  4. Строка 8. Смотрите внимательно. (я чувствую себя магом: «Смотрите, дети, внимательно, сейчас кролик вылезет из моей шляпы...»). Тут совпадает начало строки, ни один из трёх М, потом D?C{0,3} пропускает необязательный символ D и три необязательных символа C, потом L?X{0,3} пропускает необязательный символ L и три необязательных символа X, потом V?I{0,3} пропускает необязательный символ V и один из трёх необязательных символов I. Потом конец строки. Всё!

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

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

5.5 Подробные регулярные выражения

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

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

  • Пробельные символы игнорируются. Пробелы, табуляции и возвраты каретки соответственно не совпадают с пробелами, табуляциями и возвратами каретки. Они вообще не совпадают ни с чем. (Если вы хотите совпадения с пробелом в подробном регулярном выражении, вам необходимо поставить перед ним обратный слеш.)
  • Комментарии игнорируются. Комментарий в подробном регулярном выражении похож на комментарий в коде Python: он начинается с символа # и действует до конца строки. В этом случае комментарий находится внутри многострочной строки, но он работает как обычно.

Пример сделает всё более понятным. Давайте вернемся к компактному регулярному выражению, с которым мы работали, и создадим на его основе подробное регулярное выражение. Пример показан ниже.

>>> pattern = '''
    ^                   # начало строки
    M{0,3}              # тысячи - от 0 до 3-х символом M
    (CM|CD|D?C{0,3})    # сотни - 900 (CM), 400 (CD), 0-300 (от 0 до 3 C),
                        #         или 500-800 (D, а следом от 0 до 3 C)
    (XC|XL|L?X{0,3})    # десятки - 90 (XC), 40 (XL), 0-30 (от 0 до 3 X),
                        #           или 50-80 (L, а следом от 0 до 3 X)
    (IX|IV|V?I{0,3})    # единицы - 9 (IX), 4 (IV), 0-3 (от 0 до 3 I),
                        #           или 5-8 (V, а следом от 0 до 3 I)
    $                   # конец строки
    '''
>>> re.search(pattern, 'M', re.VERBOSE)                 ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)         ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)   ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'M')                             ④
  1. Строка 12. Главное, что надо запомнить, это то, что при испольвании подробных регулярных выражений необходимо добавлять дополнительные аргументы: re.VERBOSE – это константа, определённая в модуле re, которая служит сигналом, что патерн должен быть использован, как подробное регулярное выражение. Как вы можете видеть, этот патерн содержит большое количество пробельных символов (и все они игнорируются), а также несколько комментариев (которые игнорируются также). Если мы игнорируем комментарии и пробельные символы, то получается то же самое регулярное выражение, что и в предыдущем примере, но в гораздо более читабельном виде.
  2. Строка 14. Здесь совпадает начало строки, потом одно и трёх возможных M, потом CM, потом L и три из возможных X, потом IX, потом конец строки.
  3. Строка 16. Здесь совпадает начало строки, потом три из трёх возможных M, потом D и три из возможных трёх C, потом L и три из трёх возможных X, потом V и три из трёх возможных I, потом конец строки.
  4. Строка 18. Тут не совпадает. Почему? Так как отсутствует флаг re.VERBOSE, и функция re.search() рассматривает патерн, как компактное регулярное выражение, с значащими пробельными символами и символами #. Python не может автоматически определить, является ли регулярное выражение подробным или нет. Python рассматривает каждое регулярное выражение как компактное до тех пор, пока вы не укажете, что оно подробное.

5.6 Учебный пример: обработка телефонных номеров

\d совпадает с любыми цифрами (0–9). \D совпадает со всем, кроме цифр

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

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

Вот телефонные номера, которые я должен был обработать:

  • 800-555-1212
  • 800 555 1212
  • 800.555.1212
  • (800) 555-1212
  • 1-800-555-1212
  • 800-555-1212-1234
  • 800-555-1212x1234
  • 800-555-1212 ext. 1234
  • work 1-(800) 555.1212 #1234

Достаточно разнообразно! В каждом из этих примеров мне необходимо было знать, что код территории был равен 800, код оператора был равен 555, и остаток телефонного номера был 1212. Для тех, что с добавочным номером, мне необходимо было знать, что добавочный номер был равен 1234.

Давайте займёмся разработкой решения для обработки телефонного номера. Код ниже показывает первый шаг:

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')  ①
>>> phonePattern.search('800-555-1212').groups()             ②
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')                 ③
>>> phonePattern.search('800-555-1212-1234').groups()        ④
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'groups'
  1. Строка 1. Всегда читайте регулярное выражение слева направо. Выражение совпадает с началом строки и потом с (\d{3}). Что такое \d{3}? Итак , \d значит «любая цифра» (от 0 до 9). {3} значит «совпадение точно с тремя цифрами»; это вариация на тему синтаксиса {n, m}, который мы рассмотрели ранее. Если заключить это выражение в круглые скобки, то это будет означать «совпасть должно точно три цифры, и потом запомнить их как группу, которую я запрошу позже». Затем должен идти дефис. Далее должно идти совпадение с другой группой из трёх цифр. Потом опять дефис. Потом ещё одна группа из четырёх цифр. И в конце совпадение с концом строки.
  2. Строка 2. Чтобы получить доступ к группам, которые запомнил парсер регулярного выражения, используйте метод groups() объекта, который возвращается методом search(). Он должен вернуть кортеж такого количества групп, которое было определено в регулярном выражении. В нашем случае определены три группы, одна с тремя цифрами, вторая с тремя цифрами и третья с четырьмя цифрами.
  3. Строка 4. Это регулярное выражение не окончательный вариант, так как оно не обрабатывает добавочный номер после основного телефонного номера. Для этого вы должны расширить регулярное выражение.
  4. Строка 5. Вот почему вы не должны использовать «цепочку» из методов search() и groups() в продакшн коде. Если метод search() не вернёт совпадения, то он вернёт None, это не стандартный объект регулярного выражения. Вызов None.groups() генерирует очевидное исключение: None не имеет метода groups(). (Конечно же это немного менее очевидно, когда вы получаете это исключение из глубин вашего кода. Да, сейчас это говорит мой опыт.)
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')  ①
>>> phonePattern.search('800-555-1212-1234').groups()              ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')                       ③
>>> 
>>> phonePattern.search('800-555-1212')                            ④
>>> 
  1. Строка 1. Это регулярное выражение почти идентично предыдущему. Так же как и предыдущее, оно совпадает с началом строки, потом с запоминаемой группой из трёх цифр, потом дефис, потом запоминаемая группа из трёх цифр, потом дефис, потом запоминаемая группа из четырёх цифр. Что же нового? Это совпадение с еще одним дефисом и запоминаемой группой из одной и более цифр.
  2. Строка 2. Метод groups() теперь возвращает кортеж из четырёх элементов, а регулярное выражение теперь запоминает четыре группы.
  3. Строка 4. К сожалению, это регулярное выражение не является финальным решением, так как оно подразумевает, что различные фрагменты номера разделены дефисом. Что случится, если они будут разделены пробелами, запятыми или точками? Вам необходимо более общее решение для совпадения с различными типами разделителей.
  4. Строка 6. Упс! Это не единственное, что это регулярное выражение делает не так, как мы хотим. В действительности это даже шаг назад, так как теперь вы не можете обрабатывать телефонные номера без добавочных номеров. Это совершенно не то, что нам было нужно; если добавочный номер есть, вы бы хотели знать его, но если его нет, вы всё равно хотите знать оставшиеся фрагменты телефонного номера.

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

>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')  ①
>>> phonePattern.search('800 555 1212 1234').groups()  ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()  ③
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')              ④
>>> 
>>> phonePattern.search('800-555-1212')                ⑤
>>> 
  1. Строка 1. Посмотрим. У вас совпадает начало строки, потом группа из трёх цифр, потом \D+. Что это еще такое? Ок, \D совпадает с любым символом кроме цифр и также "+" означает «1 или более». Итак, \D+ означает один или более символов, не являющихся цифрами. Это то, что мы используем, вместо символа дефиса "-", чтобы было совпадение с любыми разделителями.
  2. Строка 2. Использование \D+ вместо "-" приводит к тому, что теперь регулярное выражение совпадает с телефонным номером разделённым пробелами, вместо дефисов.
  3. Строка 4. Конечно телефонные номера разделенные дефисами тоже срабатывают.
  4. Строка 6. К сожалению, это ещё не окончательный вариант, так как он подразумевает наличие разделителя. Что если номер введён без всяких разделителей?
  5. Строка 8. Упс! И до сих пор не решена проблема добавочного номера. Теперь у нас две проблемы, но мы можем справится с ними, используя ту же технику.

Следующий пример показывает регулярное выражение для обработки телефонных номеров без разделителей.

>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('80055512121234').groups()      ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()  ③
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()        ④
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')           ⑤
>>> 
  1. Строка 1. Только одно изменение, замена "+" на "*". Вместо \D+ между частями номера, теперь используется \D*. Помните, что "+" означает «1 или более»? Ок, "*" означает «ноль или более». Итак теперь вы можете обработать номер, даже если он не содержит разделителей.
  2. Строка 2. Подумать только, это действительно работает. Почему? У вас совпадает начало строки, потом запоминается группа из трёх цифр (800), потом ноль или более нецифровых символов, потом запоминается группа из трёх цифр (555), потом ноль или более нецифровых символов, потом запоминается группа из четырёх цифр (1212), потом ноль или более нецифровых символов, потом запоминается группа из произвольного количества цифр (1234), потом конец строки.
  3. Строка 4. Различные вариации также работают: точки вместо дефисов, и также пробелы или «x» перед добавочным номером.
  4. Строка 6. Наконец, мы решили давнюю проблему: добавочный номер снова необязателен. Если добавочный номер не найден, метод groups() всё равно вернет четыре элемента, но четвёртый элемент будет просто пустой строкой.
  5. Строка 8. Я ненавижу приносить плохие новости, но мы ещё не закончили. Что же тут за проблема? Существуют дополнительные символы до кода территории, но регулярное выражение думает, что код территории – это первое, что находится в начале строки. Нет проблем, вы можете использовать ту же технику, «ноль или более нецифровых символов», чтобы пропустить начальные символы до кода территории.

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

>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('(800)5551212 ext. 1234').groups()                  ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                            ③
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')                      ④
>>> 
  1. Строка 1. Это то же самое, что и в предыдущем примере, кроме \D*, ноль или более нецифровых символов, перед первой запоминаемой группой (код территории). Заметьте, что мы не запоминаем эти нецифровые символы до кода территории (они не в скобках). Если мы обнаружим их, то просто пропустим их и запомним код территории.
  2. Строка 2. Вы можете успешно обработать телефонный номер, даже со скобками до кода города. (Правая скобка также обрабатывается; как нецифровой символ и совпадает с \D* после первой запоминаемой группы.)
  3. Строка 4. Простая проверка, не поломали ли мы чего-то, что должно было работать. Так как лидирующие символы полностью опциональны, совпадает начало строки, ноль нецифровых символов, потом запоминается группа из трёх цифр (800), потом один нецифровой символ (дефис), потом группа из трёх цифр (555), потом один нецифровой (дефис), потом запоминается группа из четырёх цифр (1212), потом ноль нецифровых символов, потом группа цифр из нуля символов, потом конец строки.
  4. Строка 6. Вот, где регулярное выражение выколупывает мне глаза тупым предметом. Почему этот номер не совпал? Потому что 1 находится до кода территории, а мы предполагали, что все лидирующие символы до кода территории не цифры (\D*).

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

>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()         ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                        ③
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234').groups()                      ④
('800', '555', '1212', '1234')
  1. Строка 1. Заметьте отсутствие ^ в регулярном выражении. Вы больше не ищете совпадения с началом строки. Вашему регулярному выражению теперь ничего не подсказывает, как следует поступать с введёнными данными. Обработчик регулярного выражения будет выполнять тяжелую работу, чтобы разобраться, где же введённая строка начнёт совпадать.
  2. Строка 2. Теперь вы можете успешно обработать телефонный номер, который включает лидирующие символы и цифры, плюс разделители любого типа между частями номера.
  3. Строка 4. Простая проверка. Всё работает.
  4. Строка 6. И даже это работает.

Видите, как быстро регулярное выражение выходит из-под контроля? Бросим взгляд на предыдущие итерации. Можете ли вы объяснить разницу между одним и другим выражением?

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

>>> phonePattern = re.compile(r'''
                # не искать совпадение с началом строки, номер может начинаться, где угодно
    (\d{3})     # код территории - это 3 цифры (например, '800')
    \D*         # необязательный разделитель - это любое количество не-цифр
    (\d{3})     # код оператора - это 3 цифры (например, '555')
    \D*         # необязательный разделитель
    (\d{4})     # оставшийся номер - это 4 цифры (например, '1212')
    \D*         # необязательный разделитель
    (\d*)       # добавочный номер необязателен и может быть любым количеством цифр
    $           # конец строки
    ''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()  ①
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                          ②
('800', '555', '1212', '')
  1. Строка 12. Кроме того, что оно разбито на множество строк, это точно такое же регулярное выражение, какое было в последнем шаге, и не будет сюрпризом что оно обрабатывает такие же входные данные.
  2. Строка 14. Финальная простая проверка. Да, всё ещё работает. Вы сделали это.

5.7 Заключение

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

Вы должны быть сейчас быть знакомы со следующими обозначениями:

  • ^ соответствует началу строки;
  • $ соответствует концу строки;
  • \b соответствует границе слова;
  • \d соответствует цифре;
  • \D соответствует не-цифре;
  • x? соответствует необязательному символу x (другими словами ноль или один символов x);
  • x* соответствует нулю или более символам x;
  • x+ соответствует одному или более символам x;
  • x{n, m} соответствует символам x, повторяющимся не менее n раз, но не более m раз;
  • (a|b|c) соответствует a или b или c;
  • (x) группа для запоминания. Вы можете получить это значение, используя метод groups() на объекте, который возвращается методом re.search.

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

Источник:

  • Mark Pilgrim. Dive Into Python 3

Теги

PythonОбучениеПрограммированиеРегулярные выраженияЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.