Учебный пример: Адрес Улицы
Эта серия примеров основана на реальных проблемах, которые появились в моей работе несколько лет назад, когда мне пришлось обработать и стандартизировать адреса улиц, экспортированных из устаревшей системы до того, как произвести импорт в новую систему. (Обратите внимание: это не придуманный пример, им всё ещё можно пользоваться). Этот пример показывает как я подошёл к проблеме:
>>> 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.'
>>> UNIQae610d7ca506639d-nowiki-00000003-QINU ③
'100 NORTH BROAD RD.'
>>> import re ④
>>> re.sub('ROAD$', 'RD.', s) ⑤
'100 NORTH BROAD RD.'
- ① Моя задача стандартизировать адрес улицы, например 'ROAD' всегда выражается сокращением 'RD.'. На первый взгляд мне показалось, что это достаточно просто, и я могу использовать метод replace(). В конце концов, все данные уже в верхнем регистре и несовпадение регистра не составит проблемы. Строка поиска 'ROAD' являлась константой и обманчиво простой пример s.replace() вероятно работает.
- ② Жизнь же, напротив, полна противоречивых примеров, и я быстро обнаружил один из них. Проблема заключалась в том что 'ROAD' появилась в адресе дважды, один раз как 'ROAD', а во второй как часть названия улицы 'BROAD'. Метод replace() обнаруживал 2 вхождения и слепо заменял оба, разрушая таким образом правильный адрес.
- ③ Чтобы решить эту проблему вхождения более одной подстроки 'ROAD', вам необходимо прибегнуть к следующему: искать и заменять 'ROAD' в последних четырёх символах адреса (s[-4:]), оставляя строку отдельно (s[:-4]). Как вы могли заметить, это уже становится громоздким. К примеру, шаблон зависит от длины заменяемой строки. (Если вы заменяли 'STREET' на 'ST.', вам придется использовать s[:-6] и s[-6:].replace(...).) Не хотели бы вы вернуться к этому коду через полгода для отладки? Я не хотел бы.
- ④ Пришло время перейти к регулярным выражениям. В Python все функции, связанные с регулярными выражениями содержится в модуле re.
- ⑤ Взглянем на первый параметр: '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'
- ① В действительности я хотел совпадения с 'ROAD' когда оно на конце строки и является самостоятельным словом (а не частью большего). Чтобы описать это в регулярном выражении необходимо использовать '\b', что означает «слово должно оказаться прямо тут.» В Python это сложно, так как '\' знак в строке должен быть экранирован. Иногда это называют как «бедствие бэкслэша» и это одна из причин почему регулярные выражения проще в Perl чем в Python. Однако недостаток Perl в том что регулярные выражения смешиваются с другим синтаксисом, если у вас ошибка, достаточно сложно определить где она, в синтаксисе или в регулярном выражении.
- ② Чтобы обойти проблему «бедствие бэкслэша» вы можете использовать то, что называется неформатированная строка (raw string), путём применения префикса строки при помощи символа 'r'. Это скажет Python-у что ничего в этой строке не должно быть экранировано; '\t' это табулятор, но r'\t' это символ бэкслэша '\' , а следом за ним буква 't'. Я рекомендую всегда использовать неформатированную строку, когда вы имеете дело с регулярными выражениями; с другой стороны всё становится достаточно путанным (несмотря на то что наше регулярное выражения уже достаточно запутано).
- ③ *вздох* К неудаче я скоро обнаружил больше причин противоречащих моей логике. В этом случае адрес улицы содержал в себе цельное отдельное слово 'ROAD' и оно не было на конце строки, так как адрес содержал номер квартиры после определения улицы. Так как слово 'ROAD' не находится в конце строки, регулярное выражение re.sub() его пропускало и мы получали на выходе ту же строку что и на входе, а это то чего вы не хотите.
- ④ Чтобы решить эту проблему я удалил символ '$' и добавил ещё один '\b'. Теперь регулярное выражение совпадало с 'ROAD' если оно являлось цельным словом в любой части строки, на конце, в середине и в начале.
⁂