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

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

  • ① Всегда читайте регулярное выражение слева направо. Выражение совпадает с началом строки и потом с (\d{3}). Что такое \d{3}? Итак , \d значит «любая цифра» (от 0 до 9). {3} значит «совпадение с конкретно тремя цифрами»; это вариации на тему {n, m} синтаксиса который вы наблюдали ранее. Если заключить это выражение в круглые скобки, то это значит «совпасть должно точно три цифры и потом запомнить их как группу которую я запрошу позже». Потом выражение должно совпасть с дефисом. Потом совпасть с другой группой из трёх цифр, потм опять дефис. Потом ещё одна группа из четырёх цифр. И в конце совпадение с концом строки.
  • ② Чтобы получить доступ к группам которые запомнил обработчик регулярного выражения, используйте метод groups() на объекте который возвращает метод search(). Он должен вернуть кортеж такого количества групп, которое было определено в регулярном выражении. В нашем случае определены три группы, одна с тремя цифрами, другая с тремя цифрами и третья с четырьмя цифрами.
  • ③ Это регулярное выражение не окончательный ответ, так как оно не обрабатывает расширение после телефонного номера. Для этого вы должны расширить регулярное выражение.
  • ④ Вот почему вы не должны использовать «цепочку» из методов 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') ④
>>>

  • ① Это регулярное выражение почти идентично предыдущему. Так же как и до этого оно совпадает с началом строки, потом с запомненной группой из трёх цифр, потом дефис, потом запомненная группа из трёх цифр, потом дефис, потом запомненная группа из четырёх цифр. Что же нового? Это совпадение с другим дефисом и запоминаемая группа из одной и более цифры.
  • ② Метод groups() теперь возвращает кортеж из четырёх элементов, а регулярное выражение теперь запоминает четыре группы.
  • ③ К неудаче это регулярное выражение не является финальным ответом, так как оно подразумевает что различные части номера разделены дефисом. Что случится если они будут разделены пробелами, запятыми или точками?
  • ④ Вам необходимо более общее решение для совпадения с различными типами разделителей.

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


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

>>> 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') ⑤
>>>

  • ① Держите свою шляпу. У вас совпадает начало строки, потом группа из трёх цифр, потом \D+. Что это за чертовщина? Ок, \D совпадает с любым символом кроме цифр и также «+» означает «1 или более». Итак \D+ означает один или более символом не являющихся цифрами. Это то что вы используете вместо символа дефиса «-» чтобы совпадало с любыми разделителями.
  • ② Использование \D+ вместо «-» значит, что теперь регулярное выражение совпадает с телефонным номером разделённым пробелами вместо дефисов.
  • ③ Конечно телефонные номера разделенные дефисами тоже срабатывают.
  • ④ К неудаче это ещё не окончательный ответ, так как он подразумевает наличие разделителя. Что если номер введён без всяких разделителей?
  • ⑤ ОпцаЁ! И до сих пор не решена проблема расширения. Теперь у вас две проблемы, но вы можете справится с ними используя ту же технику.


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

>>> 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') ⑤
>>>

  • ① Только одно изменение, замена «+» на «*». Вместо \D+ между частями номера, теперь используется \D*. Помните что «+» означает «1 или более»? Ок, «*» означает «ноль или более». Итак теперь вы можете обработать номер даже если он не содержит разделителей.
  • ② Подумать только, это действительно работает. Почему? У вас совпадает начало строки, потом запоминается группа из трёх цифр (800), потом ноль или более нецифровых символов, потом запоминается группа из трёх цифр (555), потом ноль или более нецифровых символов, потом запоминается группа из четырёх цифр (1212), потом ноль или более нецифровых символов, потом запоминается группа из произвольного количества цифр (1234), потом конец строки.
  • ③ Различные вариации также работают: точки вместо дефисов, и также пробелы или «x» перед расширением.
  • ④ Наконец вы решили давнюю проблему: расширение снова опционально. Если не найдено расширения метод groups() всё ещё возвращает четыре элемента, но четвёртый элемент просто пустая строка.
  • ⑤ Я ненавижу быть вестником плохих новостей, но вы ещё не закончили. Что же тут за проблема? Существуют дополнительные символы до «area» кода, но регулярное выражение думает что код города это первое что находится в начале строки. Нет проблем, вы можете использовать ту же технику «ноль или более нецифровых символов» чтобы пропустить начальные символы до кода города.


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

>>> 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') ④
>>>

  • ① Это то же самое что и в предыдущем примере, кроме \D*, ноль или более нецифровых символов, до первой запомненной группы (код города). Заметьте что вы не запоминаете те нецифровые символы до кода города (они не в скобках). Если вы обнаружите их, вы просто пропустите их и запомните код города.
  • ② Вы можете успешно обработать телефонный номер, даже со скобками до кода города. (Правая скобка также обрабатывается; как нецифровой символ и совпадает с \D* после первой запоминаемой группы.)
  • ③ Простая проверка не поломали ли мы чего-то, что должно было работать. Так как лидирующие символы полностью опциональны, совпадает начало строки, ноль нецифровых символов, потом запоминается группа из трёх цифр (800), потом один нецифровой символ (дефис), потом группа из трёх цифр (555), потом один нецифровой (дефис), потом запоминается группа из четырёх цифр (1212), потом ноль нецифровых символов, потом группа цифр из нуля символов, потом конец строки.
  • ④ Вот где регулярное выражение выколупывает мне глаза тупым предметом. Почему этот номер не совпал? Потому что 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') ③
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234') ④
('800', '555', '1212', '1234')

  • ① Заметьте отсутствие ^ в регулярном выражении. Вы больше не совпадаете с началом строки. Ничего теперь не подсказывает как следует поступать с введёнными данными вашему регулярному выражению. Обработчик регулярного выражения будет выполнять тяжелую работу чтобы разобраться где же введённая строка начнёт совпадать.
  • ② Теперь вы можете успешно обработать телефонный номер который включает лидирующие символы и цифры, плюс разделители любого типа между частями номера.
  • ③ Простая проверка. Всё работает.
  • ④ И даже это работает тоже.

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

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

>>> phonePattern = re.compile(r'''
# don't match beginning of string, number can start anywhere
(\d{3}) # area code is 3 digits (e.g. '800')
\D* # optional separator is any number of non-digits
(\d{3}) # trunk is 3 digits (e.g. '555')
\D* # optional separator
(\d{4}) # rest of number is 4 digits (e.g. '1212')
\D* # optional separator
(\d*) # extension is optional and can be any number of digits
$ # end of string
''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() ①
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212') ②
('800', '555', '1212', '')

  • ① Кроме того что оно разбито на множество строк, это совершенно такое же регулярное выражение как было в последнем шаге, и не будет сюрпризом что оно обрабатывает такие же входные данные.
  • ② Финальная простая проверка. Да, всё ещё работает. Вы сделали это.

Итоги

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

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

^ совпадение с началом строки.
$ совпадение с концом строки.
\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.

Учебный пример: Обработка телефонных номеров - student2.ru  

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

Замыкания и генераторы

Погружение

По причинам, превосходящим всяческое понимание, я всегда восхищался языками. Не языками программирования. Хотя да, ими, а также языками естественными. Возьмем, к примеру, английский. Английский язык — это шизофренический язык, который заимствует слова из немецкого, французского, испанского и латинского языков (не говоря уже об остальных). Откровенно говоря, «заимствует» — неуместное слово; он их скорее «ворует». Или, возможно, «ассимилирует» — как Борги. Да, хороший вариант.

« Мы Борги. Ваши лингвистические и этимологические особенности станут нашими. Сопротивление бесполезно. »

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

Если вы выросли в англоязычной стране или изучали английский в формальной школьной обстановке, вы, вероятно, знакомы с основными правилами:

  1. Если слово заканчивается буквами S, X или Z, следует добавить ES. Bass становится basses, fax становится faxes, а waltz — waltzes.
  2. Если слово заканчивается звонкой H, следует добавить ES; если заканчивается глухой H, то нужно просто добавить S. Что такое звонкая H? Это такая, которая вместе с другими буквами объединяется в звук, который вы можете расслышать. Соответственно, coach становится coaches, и rash становится rashes, потому что вы слышите звуки CH и SH, когда произносите эти слова. Но cheetah становится cheetahs, потому что H здесь глухая.
  3. Если слово заканчивается на Y, которая читается как I, то замените Y на IES; если Y объединена с гласной и звучит как-то по-другому, то просто добавьте S. Так что vacancy становится vacancies, но day становится days.
  4. Если ни одно из правил не подходит, просто добавьте S и надейтесь на лучшее.

(Я знаю, существует множество исключений. Man становится men, а woman — women, но human становится humans. Mouse — mice, а louse — lice, но house во множественной числе — houses. Knife становится knives, а wife становится wives, но lowlife становится lowlifes. И не заставляйте меня вглядываться в слова, которые не изменяются во множественном числе, как например sheep, deer или haiku).

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

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

Наши рекомендации