Zanurkuj w Pythonie/Analiza przypadku: Adresy ulic

Analiza przypadku: Adresy ulic

edytuj

Ta seria przykładów została zainspirowana problemami z prawdziwego życia. Kilka lat temu gdzieś nie w Polsce, zaszła potrzeba oczyszczenia i ustandaryzowania adresów ulic zaimportowanych ze starego systemu, zanim zostaną zaimportowane do nowego. (Zauważmy, że nie jest to jakiś wyimaginowany przykład. Może on się okazać przydatny.) Poniższy przykład przybliży nas do tego problemu.

Przykład. Dopasowywanie na końcu napisu
>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')               #(1)
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')               #(2)
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.') #(3)
'100 NORTH BROAD RD.'
>>> import re                              #(4)
>>> re.sub('ROAD$', 'RD.', s)              #(5) (6)
'100 NORTH BROAD RD.'
  1. Naszym celem jest ustandaryzowanie adresów ulic, więc skrótem od 'ROAD' jest 'RD.'. Na pierwszy rzut oka wydaje się, że po prostu można wykorzystać metodę łańcucha znaków, jaką jest replace. Zakładamy, że wszystkie dane zapisane są za pomocą wielkich liter, więc nie powinno być problemów wynikających z niedopasowania, ze względu na wielkość liter. Wyszukujemy stały napis, jakim jest 'ROAD'. Jest to bardzo płytki przykład, więc s.replace poprawnie zadziała.
  2. Życie niestety jest trochę bardziej skomplikowane, o czym dość szybko można się przekonać. Problem w tym przypadku polega na tym, że 'ROAD' występuje w adresie dwukrotnie: raz jako część nazwy ulicy ('BROAD') i drugi raz jako oddzielne słowo. Metoda replace znajduje te dwa wystąpienia i ślepo je zamienia, niszcząc adres.
  3. Aby rozwiązać problem z adresami, gdzie podciąg 'ROAD' występuje kilka razy, możemy wykorzystać taki pomysł: tylko szukamy i zamieniamy 'ROAD' w ostatnich czterech znakach adresu (czyli s[-4:]), a zostawiamy pozostałą część (czyli s[:-4]). Jednak, jak możemy zresztą zobaczyć, takie rozwiązanie jest dosyć niewygodne. Na przykład polecenie, które chcemy wykorzystać, zależy od długość zamienianego napisu (jeśli chcemy zamienić 'STREET' na 'ST.', wykorzystamy s[:-6] i s[-6:].replace(...)). Chciałoby się do tego wrócić za sześć miesięcy i to debugować? Pewnie nie.
  4. Nadszedł odpowiedni czas, aby przejść do wyrażeń regularnych. W Pythonie cała funkcjonalność wyrażeń regularnych zawarta jest w module re.
  5. Spójrzmy na pierwszy parametr, 'ROAD$'. Jest to proste wyrażenie regularne, które dopasuje 'ROAD' tylko wtedy, gdy wystąpi on na końcu tekstu. Znak $ znaczy "koniec napisu". (Mamy także analogiczny znak, znak daszka ^, który znaczy "początek napisu".)
  6. Korzystając z funkcji re.sub, przeszukujemy napis s i podciąg pasujący do wyrażenia regularnego 'ROAD$' zamieniamy na 'RD.'. Dzięki temu wyrażeniu dopasowujemy 'ROAD' na końcu napisu s, lecz napis 'ROAD' nie zostanie dopasowany w słowie 'BROAD', ponieważ znajduje się on w środku napisu s.

Wracając do historii z porządkowaniem adresów, okazało się, że poprzedni przykład, dopasowujący 'ROAD' na końcu adresu, nie był poprawny, ponieważ nie wszystkie adresy dołączały 'ROAD' na końcu adresu. Niektóre adresy kończyły się tylko nazwą ulicy. Wiele razy to wyrażenie zadziałało poprawnie, jednak gdy mieliśmy do czynienia z ulicą 'BROAD', wówczas wyrażenie regularne dopasowywało 'ROAD' na końcu napisu jako część słowa 'BROAD', a takiego wyniku nie oczekiwaliśmy.

Przykład. Dopasowywanie całych wyrazów
>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)  #(1)
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)  #(2)
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)  #(3)
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s) #(4)
'100 BROAD RD. APT. 3'
  1. W istocie chcieliśmy odnaleźć 'ROAD' znajdujące się na końcu napisu i będące samodzielnym słowem, a nie częścią dłuższego wyrazu. By opisać coś takiego za pomocą wyrażeń regularnych korzystamy z \b, które znaczy tyle co "tutaj musi znajdować się początek lub koniec wyrazu". W Pythonie jest to nieco skomplikowane przez fakt, iż znaki specjalne (takie jak np. \) muszą być poprzedzone właśnie znakiem \. Zjawisko to określane jest czasem plagą ukośników (ang. backslash plague) i wydaje się być jednym z powodów łatwiejszego korzystania z wyrażeń regularnych w Perlu niż w Pythonie. Z drugiej jednak strony, w Perlu składnia wyrażeń regularnych wymieszana jest ze składnią języka, co utrudnia stwierdzenie czy błąd tkwi w naszym wyrażeniu regularnym, czy w błędnym użyciu składni języka.
  2. Eleganckim obejściem problemu plagi ukośników jest wykorzystywanie tzw. surowych napisów (ang. raw string), które opisywaliśmy w rozdziale 3, poprzez umieszczanie przed napisami litery r. Python jest w ten sposób informowany o tym, iż żaden ze znaków specjalnych w tym napisie nie ma być interpretowany; '\t' odpowiada znakowi tab, jednak r'\t' oznacza tyle, co litera t poprzedzona znakiem \. Przy wykorzystaniu wyrażeń regularnych zalecane jest stosowanie surowych napisów; w innym wypadku wyrażenia szybko stają się niezwykle skomplikowane (a przecież już ze swej natury nie są proste).
  3. Cóż... Niestety wkrótce okazuje się, iż istnieje więcej przypadków przeczących logice naszego postępowania. W tym przypadku 'ROAD' było samodzielnym słowem, jednak znajdowało się w środku napisu, ponieważ na jego końcu umieszczony był jeszcze numer mieszkania. Z tego powodu nasze bieżące wyrażenie nie zostało odnalezione, funkcja re.sub niczego nie zamieniła, a co za tym idzie napis został zwrócony w pierwotnej postaci (co nie było naszym celem).
  4. Aby rozwiązać ten problem wystarczy zamienić w wyrażeniu regularnym $ na kolejne \b. Teraz będzie ono pasować do każdego samodzielnego słowa 'ROAD', niezależnie od jego pozycji w napisie.