Dzisiejsza lekcja poświęcona będzie w całości wyrażeniom regularnym. Wyrażenia regularne (zwane też regex — od angielskiego wyrażenia regular expressions) to wzorce, dzięki którym możemy sprawdzać czy jakiś ciąg znaków (np. taki, który odczytamy od użytkownika) ma określony przez nas format (np. czy może być datą).
Wyrażenia regularne to bardzo potężne narzędzie, jednocześnie dość proste do opanowania. To oczywiście kwestia gustu, ale ja miałem świetną zabawę poznając i zgłębiając tajniki wyrażeń regularnych :) Ich zastosowań jest mnóstwo — od sprawdzania wejścia użytkownika, przez wyszukiwanie wzorców w tekście, po automatyczne przetwarzanie i analizowanie np. logów systemowych.
Lekcja
Przede wszystkim nauczymy się o budowie samych zapytań — do tego wystarczy nam strona internetowa pozwalająca sprawdzić, jak będzie działało nasze zapytanie.
Miej proszę na uwadze, że wyrażenia regularne są może logiczne, ale przy bardziej skomplikowanych łatwo popełnić błąd / literówkę. Problem z wyrażeniami regularnymi polega na tym, że takie wyrażenie z błędem najczęściej będzie działało, program nie zgłosi problemów, ale nie będzie ono sprawdzało tego, co oczekujemy. Ostatnia podsekcja jest poświęcona najczęstszym błędom w wyrażeniach, które zdarza się popełnić.
Wyrażenia regularne — składnia i konstrukcja
Wyrażenia regularne w Javie są bardzo podobne do tych w języku Perl (dla zainteresowanych różnice można znaleźć w dokumentacji Oracle). Dlatego jeśli znasz składnie wyrażeń regularnych z innego języka (np. PHP) możesz pominąć całą tą sekcję. Z drugiej strony, dzięki temu, że różnice nie są bardzo istotne, możesz się wspierać materiałami które znajdziesz pod hasłem PCRE (perl compatible regular expressions).
Wyrażenia regularne składają się z sekwencji ‘atomów’ (niestety nie znalazłem sensownego tłumaczenia na język polski — jeśli znasz takowe, będę wdzięczny za informacje w komentarzu :) ; na potrzeby nauki zostańmy przy tej nazwie, ale zastrzegam że to raczej nie jest poprawna nazwa w języku polskim). Najprostszy atom to literał — tzn. np litera, cyfra, znak specjalny. Literały można grupować za pomocą nawiasów. Poza literałami mamy jeszcze kwantyfikatory, mówiące ile wystąpień danego atomu może być oraz operator alternatywy. Brzmi niefajnie, prawda? Zostawmy więc teorię i przejdźmy do praktyki :)
Najprostsze wyrażenia regularne
Najprostsze wyrażenie regularne to po prostu tekst — póki co pomińmy znaki specjalne, bo jak zobaczymy za chwilę, mogą mieć one szczególne znaczenie.
abcde
Powyższe wyrażenie regularne dopasuje tekst “abcde” i żaden inny. Dopasuje, tzn. że sprawdzając czy zadany tekst “spełnia” wyrażenie regularne, otrzymamy prawdę. Oczywiście takie wyrażenie regularne nie ma dużego sensu, ale czasami będzie używane (np. metoda split klasy String przyjmuje wyrażenie regularne — a czasem chcemy podzielić wg po prostu określonej literki, znaku specjalnego czy słowa).
Dodajmy więc kwantyfikator do litery a .
Kwantyfikatory
a+bcde
To wyrażenie jest już ciekawsze, bo dopasuje zarówno “abcde” jak i “aabcde”, “aaaaabcde” itp. Jednym słowem — dowolną ilość literek a na początku (ale co najmniej jedną) i później litery bcde. Poniżej znajdziesz tabelkę z podsumowaniem kwantyfikatorów.
Kwantyfikator | Znaczenie | Przykład | Przykład dopasowuje |
---|---|---|---|
* | Zero lub więcej wystąpień | a*b | ab, b, aab, aaaaaab, aaab (i podobne) |
+ | Jedno lub więcej wystąpień | a+b | ab, aab, aaaaaaab, aab (i podobne) |
? | Zero lub jedno wystąpienie | a?b | ab, b |
{n,m} | Co najmniej n i maksymalnie m wystąpień | a{1,4}b | ab, aab, aaab, aaaab |
{n,} | Co najmniej n wystąpień | a{3,}b | aaab, aaaab aaaaab (i podobne) |
{,n} | Maksymalnie n wystąpień | a{,3}b | b, ab, aab, aaab |
{n} | Dokładnie n wystąpień | a{3}b | aaab |
Kwantyfikatory dotyczą atomu, który jest od razu po jego lewej stronie (uwaga: jeśli będzie to spacja, dotyczył on będzie spacji). W powyższym przykładzie była to pojedyncza litera — często jednak chcemy powtórzyć pewną sekwencję. Weźmy na przykład numer rachunku bankowego. Zaczyna się on od liter PL, 2 cyfr, a nastepnie 6 bloków po cztery cyfry które mogą być oddzielone spacją. Zobaczmy jak podejść do tego problemu.
Zakresy i grupy
Zakresy w wyrażeniach regularnych to w skrócie rzecz ujmując grupa znaków, coś jakby powiedzieć ‘w tym miejscu będzie jeden z tych znaków’. Taki zakres też jest atomem. Zakresy definiujemy w nawiasach kwadratowych, możemy to zrobić na dwa sposoby: wymienić wszystkie możliwe znaki (bez przecinków, jeden obok drugiego) lub wprowadzić przedział, możemy je oczywiście łączyć. Przedział definiujemy określając element początkowy oraz końcowy umieszczając między nimi myślnik; mogą to być zarówno cyfry (np. 1–3 ; 0–9; 1–5) jak i litery (np. a‑z ; A‑Z). Ale znowu brniemy w teorię, na przykładach na pewno od razu będzie łatwiej:
Wyrażenie | Opis |
---|---|
[abcde] | Jedna z liter: a, b, c, d lub e |
[a‑zA‑Z] | Jedna z liter od a do Z mała lub duża |
[a‑c3‑5] | Litera od a do c lub cyfra od 3 do 5 |
[a‑c14‑7] | Litera od a do c lub cyfra 1 lub cyfra od 4 do 7 |
[abc\[\]] | Litera a lub b lub c lub nawias kwadratowy (dlaczego dodaliśmy też odwrócone ukośniki, czytaj dalej) |
[.] | Dowolna litera (czytaj dalej) |
Grupy z kolei służą nam do łączenia bardziej skomplikowanych struktur (np. mamy wyrażenie regularne które dopasowuje fragment, który powtarza się kilka razy). Do oznaczania grup używamy zwykłych nawiasów, grupa traktowana jest jako atom. Spójrzmy na poniższe przykłady:
Wyrażenie | Opis |
---|---|
a(bcd)* | litera a oraz ciąg bcd zero lub więcej razy |
a(b(cd)?)+ | litera a, a następnie jedno lub więcej powtórzeń b lub bcd |
Teraz już możemy zbudować zapytanie do weryfikacji naszego rachunku bankowego (oczywiście nie sprawdza ona cyfry kontrolnej itp, a jedynie format numeru rachunku):
PL[0-9]{2}( ?[0-9]{4}){6}
Uwaga: do reprezentowania pewnych elementów (np. litery czy cyfry) są odpowiednie metatagi, o których więcej informacji znajdziesz w dokumentacji. Należy z nich korzystać w miarę możliwości ponieważ poprawiają czytelność i utrudniają popełnienie błędu/niedopatrzenia (np. pominięcia jakiejś litery). Nie robiliśmy tego powyżej, bo dla osoby nieobeznanej z wyrażeniami regularnymi mogłyby być one mylące. Pamiętaj jednak w pracy zawodowej, że są one dobra praktyką.
Znaki specjalne
Ostatnim ważnym elementem są znaki specjalne: poznamy tutaj kropkę oraz backslash.
Kropka dopasowuje dowolny znak. Dlatego wcześniej wspominałem, że należy uważać na znaki specjalne. Bardzo łatwo jest napisać np. wyrażenie do walidacji adresu IP (to adres, który identyfikuje nasz komputer w sieci, po szczegóły zainteresowanych odsyłam np. na wikipedię). Przykładowy adres ip to np: 255.255.255.255 . Uproszczone wyrażenie regularne które napisalibyśmy na szybko wyglądałoby więc nastepująco:
([0-9]{1,3}.){3}[0-9]{1,3}
Niestety poza naszym adresem dopasuje ono także “255 255 255 255”, a także “255u255p255s255”.
Aby kropka była traktowana w wyrażeniu regularnym jako kropka, musimy ją poprzedzić backslashem: \ . Ten znak mówi o tym, że kolejny znak należy traktować na specjalnych warunkach — w przypadku kropki te specjalne warunki to właśnie ‘traktuj jako normalny znak’. Prawidłowe wyrażenie regularne powinno więc wyglądać następująco:
([0-9]{1,3}\.){3}[0-9]{1,3}
(osoby zaznajomione z tematyką zapewne zauważą, że to wyrażenie dopasuje także nieprawidłowe adresy — wiem, ale wpis ten jest poświęcony wyrażeniom regularnym a nie zagadnieniom ruchu sieciowego, więc odpuszczamy absolutną poprawność na rzecz nauki ;) )
I tutaj jeszcze słowo odnośnie Javy i backslashów — ponieważ backslash jest też używany w ciągach znaków w Javie (np. żeby wstawić nową linię używamy \n) to każdy backslash musimy poprzedzić kolejnym. Dlatego wyrażenie regularne:
a\.b
zapiszemy jako:
String regex = "a\\.b";
W drugiej linijce najpierw mówimy, że kolejny znak po pierwszym backslashu traktuj specjalnie (== jest to normalny backslash), a więc wyrażenie regularne otrzymuje tylko jeden backslash. Ciekawie robi się jeśli chcemy np. dopasować właśnie backslash (np. a\b — taka konstrukcja przydatna jest do weryfikowania nazw plików/ścieżek w systemach Windows), dajmy na to wyrażeniem:
a\\b
Musimy je zapisać w Javie jako:
String regex = "a\\\\b";
Każda para backslashy przekłada sie na jeden backslash w wyrażeniu regularnym. Taka drobna niedogodność.
Korzystanie z wyrażeń regularnych w języku Java
W języku Java większość operacji na wyrażeniach regularnych wykonujemy z użyciem klas Pattern oraz Matcher. Klasy te oferują o wiele bardziej zaawansowaną funkcjonalność niż opisana poniżej (np. możemy analizować dopasowanie do każdej z grup z osobna), sporo kompletnych informacji znajdziemy na stronie Oracle. Tutaj zapoznamy się tylko z najprostszym użyciem, czyli sprawdzeniem czy dany ciąg znaków pasuje do naszego wyrażenia regularnego (innymi słowy, czy nasze wyrażenie regularne dopasowuje dany ciąg znaków).
Zacznijmy od klasy Pattern — klasa ta reprezentuje nasze wyrażenie regularne, skompilowane — tzn. wstępnie przetworzone przez komputer, dzięki czemu wielokrotne jego wywołanie będzie bardziej wydajne. Obiekt z reprezentacją naszego wyrażenia otrzymamy wywołując statyczną metodę compile:
Pattern pattern = Pattern.compile("a*b?cde");
Metoda ta rzuca wyjątek PatternSyntaxException, który jest jednak typu unchecked więc nie musimy dodawać try-catch (ale oczywiście możemy — jeśli chcesz odświeżyć sobie wiedzę o wyjątkach, zapraszam do ponownego odwiedzenia lekcji #3).
Otrzymany obiekt pattern ma metodę matcher, która zwraca nam obiekt typu Matcher.
Matcher matcher = pattern.matcher("jakis ciag znakow");
Mówiąc w uproszczeniu — Pattern to nasze wyrażenie regularne, a Matcher to jego dopasowanie (lub nie) do konkretnego ciągu znaków. Obiekt matcher posiada metodę matches, która mówi nam czy cały ciąg znaków użyty do utworzenia Matchera pasuje do naszego wyrażenia regularnego.
matcher.matches(); //zwraca true lub false
Jeśli chcemy wykonać sprawdzenie tylko jeden raz, możemy skorzystać z metody-skrótu klasy Pattern:
Pattern.matches("regex", "ciąg do sprawdzenia"); //zwraca true lub false
Jest to funkcjonalnie równoważne z poniższym fragmentem:
Pattern pattern = Pattern.compile("regex");
Matcher matcher = pattern.matcher("ciąg do sprawdzenia");
matcher.matches(); //zwraca true lub false
Prawda, że proste, logiczne i przyjemne? :) Zapraszam oczywiście do samodzielnych eksperymentów.
Zadanie
Zmodyfikuj program, który już napisałeś tak, aby używał wyrażeń regularnych w miejscach gdzie są klauzule try-catch.
Pseudokod powinien wyglądać następująco:
- Wczytaj informacje od użytkownika
- Dopóki to, co podał użytkownik nie pasuje do wyrażenia regularnego, proś go ponownie o podanie danych
- Jeśli to, co wpisał użytkownik pasuje do wyrażenia regularnego przejdź dalej i wykonaj właściwy kod (np. konwersja daty) — możliwe, że nadal będziesz musiał używać bloków try-catch w niektórych miejscach (przez checked exceptions)
Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!