Zanurkuj w Pythonie/Analiza przypadku: Przetwarzanie numerów telefonów
Analiza przypadku: Przetwarzanie numerów telefonów
edytujDo tej pory koncentrowaliśmy się na dopasowywaniu całych wzorców. Albo pasowały, albo nie. Ale wyrażenia regularne są dużo potężniejsze. Gdy zostaną dopasowane, można wyciągnąć z nich wybrane kawałki i dzięki temu sprawdzić, co i gdzie zostało dopasowane.
Oto kolejny przykład z życia wzięty, z jakim można się spotkać: przetwarzanie amerykańskich numerów telefonów. Klient chciał móc wprowadzać numer w dowolnej formie w jednym polu, ale potem chciał, żeby przechowywać oddzielnie numer kierunkowy, numer w dwóch częściach i opcjonalny numer wewnętrzny w bazie danych firmy. W Internecie można znaleźć wiele takich wyrażeń regularnych, ale żadne z nich nie jest aż tak bardzo restrykcyjne.
Oto przykłady numerów telefonów, jakie program miał przetwarzać:
- 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
Całkiem duże zróżnicowanie! W każdym z tych przypadków musimy wiedzieć, że numerem kierunkowym jest 800, pierwszą częścią numeru jest 555, drugą 1212, a numerem wewnętrznym (jeśli istnieje) - 1234.
Spróbujmy rozwiązać ten problem. Poniższy przykład pokazuje pierwszy krok.
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$') #(1)
>>> phonePattern.search('800-555-1212').groups() #(2)
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234') #(3)
>>>
- Zawsze odczytujemy wyrażenie regularne od lewej do prawej. Tutaj dopasowujemy początek łańcucha znaków, potem (\d{3}). Co to takiego te (\d{3})? {3} oznacza "dopasuj dokładnie 3 wystąpienia" (jest to wariacja składni {n, m}). \d oznacza "jakakolwiek cyfra" (od 0 do 9). Umieszczenie ich w nawiasach oznacza "dopasuj dokładnie 3 cyfry, i zapamiętaj je jako grupę, o którą można zapytać później". Następnie mamy dopasować myślnik. Dalej dopasuj kolejną grupę dokładnie trzech cyfr, a następnie kolejny myślnik, i ostatnią grupę tym razem czterech cyfr. Na koniec dopasuje koniec łańcucha znaków.
- Aby otrzymać grupy, które zapamięta moduł przetwarzania wyrażeń regularnych, należy skorzystać z metody
groups()
obiektu zwracanego przez funkcjęsearch
. Zwróci ona krotkę z ilością elementów równą ilości grup zdefiniowanych w wyrażeniu regularnym. W tym przypadku mamy trzy grupy: dwie po 3 cyfry i ostatnią czterocyfrową. - To jednak nie jest rozwiązaniem naszego problemu, bo nie dopasowuje numeru telefonu z numerem wewnętrznym. Musimy więc je rozszerzyć.
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$') #(1)
>>> phonePattern.search('800-555-1212-1234').groups() #(2)
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234') #(3)
>>>
>>> phonePattern.search('800-555-1212') #(4)
>>>
- To wyrażenie regularne jest praktycznie identyczne z wcześniejszym. Tak jak wcześniej, dopasowujemy początek łańcucha, potem zapamiętywaną grupę trzech cyfr, myślnik, zapamiętywaną grupę trzech cyfr, myślnik i zapamiętywaną grupę czterech cyfr. Nową częścią jest kolejny myślnik i zapamiętywana grupa jednej lub więcej cyfr. Na końcu jak w poprzednim przykładzie dopasowujemy koniec łańcucha.
- Metoda
groups()
zwraca teraz krotkę czterech elementów, ponieważ wyrażenie regularne definiuje teraz cztery grupy do zapamiętania. - Niestety nie jest to wersja ostateczna, gdyż zakładamy, że każda część numeru telefonu jest rozdzielona myślnikiem. Co jeśli będą rozdzielone spacją, kropką albo przecinkiem? Potrzebujemy bardziej ogólnego rozwiązania.
- Ups! Nie tylko to wyrażenie nie robi wszystkiego co powinno, ale cofnęliśmy się, gdyż teraz nie dopasowuje ono numerów bez numeru wewnętrznego. To nie jest to co chcieliśmy; jeśli w numerze jest podany numer wewnętrzny, to chcemy go znać, ale jeśli go nie ma, to i tak chcemy znać inne części numeru telefonu.
Następny przykład pokazuje wyrażenie regularne, które obsługuje różne separatory między częściami numeru telefonu.
>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$') #(1) >>> phonePattern.search('800 555 1212 1234').groups() #(2) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212-1234').groups() #(3) ('800', '555', '1212', '1234') >>> phonePattern.search('80055512121234') #(4) >>> >>> phonePattern.search('800-555-1212') #(5) >>>
- Teraz dopasowujemy początek łańcucha, grupę trzech cyfr, potem \D+... zaraz, zaraz, co to jest? \D dopasowuje dowolny znak, który nie jest cyfrą, a + oznacza "jeden lub więcej". Więc \D+ dopasowuje jeden lub więcej znaków nie będących cyfrą. Korzystamy z niego, aby dopasować różne separatory, nie tylko myślniki.
- Korzystanie z \D+ zamiast z - pozwala na dopasowywanie numerów telefonów ze spacjami w roli separatora części.
- Oczywiście myślniki też działają.
- Niestety, to dalej nie jest ostateczna wersja, ponieważ nie obsługuje ona braku jakichkolwiek separatorów.
- No i dalej nie rozwiązany pozostał problem możliwości braku numeru wewnętrznego. Mamy więc dwa problemy do rozwiązania, ale w obu przypadkach rozwiążemy problem tą samą techniką.
Następny przykład pokazuje wyrażenie regularne pasujące także do numeru bez separatorów.
>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') #(1) >>> phonePattern.search('80055512121234').groups() #(2) ('800', '555', '1212', '1234') >>> phonePattern.search('800.555.1212 x1234').groups() #(3) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() #(4) ('800', '555', '1212', '') >>> phonePattern.search('(800)5551212 x1234') #(5) >>>
- Jedyna zmiana jakiej dokonaliśmy od ostatniego kroku to zamiana wszystkich + na *. Zamiast \D+ pomiędzy częściami numeru telefonu dopasowujemy teraz \D*. Pamiętasz, że + oznacza "1 lub więcej"? * oznacza "0 lub więcej". Tak więc teraz jesteśmy w stanie przetworzyć numer nawet bez separatorów.
- Nareszcie działa! Dlaczego? Dopasowany został początek łańcucha, grupa 3 cyfr (800), potem zero znaków nienumerycznych, potem znowu zapamiętywana grupa 3 cyfr (555), znowu zero znaków nienumerycznych, zapamiętywana grupa 4 cyfr (1212), zero znaków nienumerycznych, numer wewnętrzny (1234) i nareszcie koniec łańcucha.
- Inne odmiany też działają np. numer rozdzielony kropkami ze spacją i x-em przed numerem wewnętrznym.
- Wreszcie udało się też rozwiązać problem z brakiem numeru wewnętrznego. Tak czy siak groups() zwraca nam krotkę z 4 elementami, ale ostatni jest tutaj pusty.
- Niestety jeszcze nie skończyliśmy. Co tutaj jest nie tak? Przed numerem kierunkowym znajduje się dodatkowy znak "(", a nasze wyrażenie zakłada, że numer kierunkowy znajduje się na samym przodzie. Nie ma problemu, możemy zastosować tę samą metodę, co do znaków rozdzielających.
Następny przykład pokazuje jak sobie radzić ze znakami wiodącymi w numerach telefonów.
>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') #(1) >>> phonePattern.search('(800)5551212 ext. 1234').groups() #(2) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() #(3) ('800', '555', '1212', '') >>> phonePattern.search('work 1-(800) 555.1212 #1234') #(4) >>>
- Wzorzec w tym przykładzie jest taki sam jak w poprzednim, z wyjątkiem tego, że teraz na początku łańcucha dopasowujemy \D* przed pierwszą zapamiętywaną grupą (numerem kierunkowym). Zauważ że tych znaków nie zapamiętujemy (nie są one w nawiasie). Jeśli je napotkamy, to ignorujemy je i przechodzimy do numeru kierunkowego.
- Teraz udało się przetworzyć numer telefonu z nawiasem otwierającym na początku. (Zamykający był już wcześniej obsługiwany; był traktowany jako nienumeryczny znak pasujący do teraz drugiego \D*.)
- Tak na wszelki wypadek sprawdzamy czy nie popsuliśmy czegoś. Jako, że początkowy znak jest całkowicie opcjonalny, następuje dopasowanie w dokładnie taki sam sposób jak w poprzednim przykładzie.
- W tym miejscu wyrażenia regularne sprawiają, że chce się człowiekowi rozbić bardzo dużym młotem monitor. Dlaczego to nie pasuje? Wszystko za sprawą 1 przed numerem kierunkowym (numer kierunkowy USA), a przecież przyjęliśmy, że na początku mogą być tylko nienumeryczne znaki. Ech...
Cofnijmy się na chwilę. Jak na razie wszystkie wyrażenia dopasowywaliśmy od początku łańcucha. Ale teraz widać wyraźnie, że na początku naszego łańcucha mamy nieokreśloną liczbę znaków których kompletnie nie potrzebujemy. Po co mamy więc dopasowywać początek łańcucha? Jeśli tego nie zrobimy, to przecież pominie on tyle znaków ile mu się uda, a przecież o to nam chodzi. Takie podejście prezentuje następny przykład.
>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') #(1) >>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() #(2) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212') #(3) ('800', '555', '1212', '') >>> phonePattern.search('80055512121234') #(4) ('800', '555', '1212', '1234')
- Zauważ, że brakuje ^ w tym wyrażeniu regularnym, Teraz już nie dopasowujemy początku łańcucha, bo przecież nikt nie powiedział, że wyrażenie musi pasować do całego łańcucha, a nie do fragmentu. Mechanizm wyrażeń regularnych sam zadba o namierzenie miejsca do którego ono pasuje (o ile w ogóle).
- Teraz nareszcie pasuje numer ze znakami na początku (w tym cyframi) i dowolnymi, jakimikolwiek separatorami w środku.
- Na wszelki wypadek sprawdzamy i to. Działa!
- To też działa.
Widzimy, jak szybko wyrażenia regularne wymykają się spod kontroli? Rzućmy okiem na jedną z poprzednich przykładów. Widzimy różnice pomiędzy nim i następnym?
Póki jeszcze rozumiemy to, co napisaliśmy, przekształćmy to w rozwlekłe wyrażenie regularne, żeby nie zapomnieć, co odpowiada za co i dlaczego.
>>> phonePattern = re.compile(r''' # nie dopasowuj początku łańcucha, numer może się zacząć gdziekolwiek (\d{3}) # numer kierunkowy - 3 cyfry (np. '800') \D* # opcjonalny nienumeryczny separator (\d{3}) # pierwsza część numeru - 3 cyfry (np. '555') \D* # opcjonalny separator (\d{4}) # druga część numeru (np. '1212') \D* # opcjonalny separator (\d*) # numer wewnętrzny jest opcjonalny i może mieć dowolną długość $ # koniec łańcucha ''', re.VERBOSE) >>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() #(1) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212') #(2) ('800', '555', '1212', '')
- Pomijając fakt, że jest ono podzielone na wiele linii, to wyrażenie jest dokładnie takie samo jak po ostatnim kroku, więc nie jest niespodzianką, że dalej działa jak powinno.
- Jeszcze jedna próba. Tak, działa! Skończone!