#04 — wyrażenia regularne

By 4 September 2014Kurs Javy

Dzisiejsza lekc­ja poświę­cona będzie w całoś­ci wyraże­niom reg­u­larnym. Wyraże­nia reg­u­larne (zwane też regex — od ang­iel­skiego wyraże­nia reg­u­lar expres­sions) to wzorce, dzię­ki którym może­my sprawdzać czy jak­iś ciąg znaków (np. taki, który odczy­tamy od użytkown­i­ka) ma określony przez nas for­mat (np. czy może być datą).

Wyraże­nia reg­u­larne to bard­zo potężne narzędzie, jed­nocześnie dość proste do opanowa­nia. To oczy­wiś­cie kwes­t­ia gus­tu, ale ja miałem świet­ną zabawę poz­na­jąc i zgłębi­a­jąc tajni­ki wyrażeń reg­u­larnych :) Ich zas­tosowań jest mnóst­wo — od sprawdza­nia wejś­cia użytkown­i­ka, przez wyszuki­wanie wzor­ców w tekś­cie, po automaty­czne przetwarzanie i anal­i­zowanie np. logów sys­te­mowych.

Lekcja

Przede wszys­tkim nauczymy się o budowie samych zapy­tań — do tego wystar­czy nam strona inter­ne­towa pozwala­ją­ca sprawdz­ić, jak będzie dzi­ałało nasze zapy­tanie.

Miej proszę na uwadze, że wyraże­nia reg­u­larne są może log­iczne, ale przy bardziej skom­p­likowanych łat­wo popełnić błąd / literówkę. Prob­lem z wyraże­ni­a­mi reg­u­larny­mi pole­ga na tym, że takie wyraże­nie z błę­dem najczęś­ciej będzie dzi­ałało, pro­gram nie zgłosi prob­lemów, ale nie będzie ono sprawdza­ło tego, co oczeku­je­my. Ostat­nia pod­sekc­ja jest poświę­cona najczęst­szym błę­dom w wyraże­ni­ach, które zdarza się popełnić.

Wyrażenia regularne — składnia i konstrukcja

Wyraże­nia reg­u­larne w Javie są bard­zo podob­ne do tych w języku Perl (dla zain­tere­sowanych różnice moż­na znaleźć w doku­men­tacji Ora­cle). Dlat­ego jeśli znasz skład­nie wyrażeń reg­u­larnych z innego języ­ka (np. PHP) możesz pom­inąć całą tą sekcję. Z drugiej strony, dzię­ki temu, że różnice nie są bard­zo istotne, możesz się wspier­ać mate­ri­ała­mi które zna­jdziesz pod hasłem PCRE (perl com­pat­i­ble reg­u­lar expres­sions).

Wyraże­nia reg­u­larne składa­ją się z sek­wencji ‘atom­ów’ (nieste­ty nie znalazłem sen­sownego tłu­maczenia na język pol­s­ki — jeśli znasz takowe, będę wdz­ięczny za infor­ma­c­je w komen­tarzu :) ; na potrze­by nau­ki zostańmy przy tej nazwie, ale zas­trzegam że to raczej nie jest popraw­na nazwa w języku pol­skim). Najprost­szy atom to lit­er­ał — tzn. np lit­era, cyfra, znak spec­jal­ny. Lit­er­ały moż­na grupować za pomocą naw­iasów. Poza lit­er­ała­mi mamy jeszcze kwan­ty­fika­to­ry, mówiące ile wys­tąpień danego ato­mu może być oraz oper­a­tor alter­naty­wy. Brz­mi niefa­jnie, praw­da? Zostawmy więc teorię i prze­jdźmy do prak­ty­ki :)

Najprostsze wyrażenia regularne

Najprost­sze wyraże­nie reg­u­larne to po pros­tu tekst — póki co pomińmy zna­ki spec­jalne, bo jak zobaczymy za chwilę, mogą mieć one szczególne znacze­nie.

abcde

Powyższe wyraże­nie reg­u­larne dopa­su­je tekst “abcde” i żaden inny. Dopa­su­je, tzn. że sprawdza­jąc czy zadany tekst “speł­nia” wyraże­nie reg­u­larne, otrzy­mamy prawdę. Oczy­wiś­cie takie wyraże­nie reg­u­larne nie ma dużego sen­su, ale cza­sa­mi będzie uży­wane (np. meto­da split klasy String przyj­mu­je wyraże­nie reg­u­larne — a cza­sem chce­my podzielić wg po pros­tu określonej liter­ki, znaku spec­jal­nego czy słowa).

Doda­jmy więc kwan­ty­fika­tor do litery a .

Kwantyfikatory

a+bcde

To wyraże­nie jest już ciekawsze, bo dopa­su­je zarówno “abcde” jak i “aabcde”, “aaaaabcde” itp. Jed­nym słowem — dowol­ną ilość literek a na początku (ale co najm­niej jed­ną) i później litery bcde. Poniżej zna­jdziesz tabelkę z pod­sumowaniem kwan­ty­fika­torów.

Kwan­ty­fika­tor Znacze­nie Przykład Przykład dopa­sowu­je
 * Zero lub więcej wys­tąpień  a*b ab, b, aab, aaaaaab, aaab (i podob­ne)
 + Jed­no lub więcej wys­tąpień  a+b ab, aab, aaaaaaab, aab (i podob­ne)
 ? Zero lub jed­no wys­tąpi­e­nie  a?b  ab, b
 {n,m} Co najm­niej n i maksy­mal­nie m wys­tąpień  a{1,4}b  ab, aab, aaab, aaaab
 {n,} Co najm­niej n wys­tąpień  a{3,}b aaab, aaaab aaaaab (i podob­ne)
 {,n} Maksy­mal­nie n wys­tąpień  a{,3}b  b, ab, aab, aaab
 {n} Dokład­nie n wys­tąpień  a{3}b  aaab

Kwan­ty­fika­to­ry doty­czą ato­mu, który jest od razu po jego lewej stron­ie (uwa­ga: jeśli będzie to spac­ja, doty­czył on będzie spacji). W powyższym przykładzie była to poje­dyncza lit­era — częs­to jed­nak chce­my powtórzyć pewną sek­wencję. Weźmy na przykład numer rachunku bankowego. Zaczy­na się on od liter PL, 2 cyfr, a nastep­nie 6 bloków po cztery cyfry które mogą być odd­zielone spacją. Zobaczmy jak pode­jść do tego prob­le­mu.

Zakresy i grupy

Zakresy w wyraże­ni­ach reg­u­larnych to w skró­cie rzecz ujmu­jąc gru­pa znaków, coś jak­by powiedzieć ‘w tym miejs­cu będzie jeden z tych znaków’. Taki zakres też jest atom­em. Zakresy defini­u­je­my w naw­iasach kwadra­towych, może­my to zro­bić na dwa sposo­by: wymienić wszys­tkie możli­we zna­ki (bez przecinków, jeden obok drugiego) lub wprowadz­ić przedzi­ał, może­my je oczy­wiś­cie łączyć. Przedzi­ał defini­u­je­my określa­jąc ele­ment początkowy oraz koń­cowy umieszcza­jąc między nimi myśl­nik; 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że­nie Opis
 [abcde] Jed­na z liter: a, b, c, d lub e
 [a-zA-Z] Jed­na z liter od a do Z mała lub duża
 [a-c3-5] Lit­era od a do c lub cyfra od 3 do 5
 [a-c14-7] Lit­era od a do c lub cyfra 1 lub cyfra od 4 do 7
 [abc\[\]] Lit­era a lub b lub c lub naw­ias kwadra­towy (dlaczego dodal­iśmy też odwró­cone ukośni­ki, czy­taj dalej)
 [.] Dowol­na lit­era (czy­taj dalej)

Grupy z kolei służą nam do łączenia bardziej skom­p­likowanych struk­tur (np. mamy wyraże­nie reg­u­larne które dopa­sowu­je frag­ment, który pow­tarza się kil­ka razy). Do oznacza­nia grup uży­wamy zwykłych naw­iasów, gru­pa trak­towana jest jako atom. Spójrzmy na poniższe przykłady:

Wyraże­nie Opis
 a(bcd)*  lit­era a oraz ciąg bcd zero lub więcej razy
 a(b(cd)?)+  lit­era a, a następ­nie jed­no lub więcej powtórzeń b lub bcd

Ter­az już może­my zbu­dować zapy­tanie do wery­fikacji naszego rachunku bankowego (oczy­wiś­cie nie sprawdza ona cyfry kon­trol­nej itp, a jedynie for­mat numeru rachunku):

PL[0-9]{2}( ?[0-9]{4}){6}

Uwa­ga: do reprezen­towa­nia pewnych ele­men­tów (np. litery czy cyfry) są odpowied­nie metata­gi, o których więcej infor­ma­cji zna­jdziesz w doku­men­tacji. Należy z nich korzys­tać w miarę możli­woś­ci ponieważ popraw­ia­ją czytel­ność i utrud­ni­a­ją popełnie­nie błędu/niedopatrzenia (np. pominię­cia jakiejś litery). Nie robil­iśmy tego powyżej, bo dla oso­by nieobez­nanej z wyraże­ni­a­mi reg­u­larny­mi mogły­by być one mylące. Pamię­taj jed­nak w pra­cy zawodowej, że są one dobra prak­tyką.

Znaki specjalne

Ostat­nim ważnym ele­mentem są zna­ki spec­jalne: poz­namy tutaj krop­kę oraz back­slash.

Krop­ka dopa­sowu­je dowol­ny znak. Dlat­ego wcześniej wspom­i­nałem, że należy uważać na zna­ki spec­jalne. Bard­zo łat­wo jest napisać np. wyraże­nie do wal­i­dacji adresu IP (to adres, który iden­ty­fiku­je nasz kom­put­er w sieci, po szczegóły zain­tere­sowanych odsyłam np. na wikipedię). Przykład­owy adres ip to np: 255.255.255.255 . Uproszc­zone wyraże­nie reg­u­larne które napisal­ibyśmy na szy­bko wyglą­dało­by więc nastepu­ją­co:

([0-9]{1,3}.){3}[0-9]{1,3}

Nieste­ty poza naszym adresem dopa­su­je ono także “255 255 255 255”, a także “255u255p255s255”.

Aby krop­ka była trak­towana w wyraże­niu reg­u­larnym jako krop­ka, musimy ją poprzedz­ić back­slashem: \ . Ten znak mówi o tym, że kole­jny znak należy trak­tować na spec­jal­nych warunk­ach — w przy­pad­ku krop­ki te spec­jalne warun­ki to właśnie ‘trak­tuj jako nor­mal­ny znak’. Praw­idłowe wyraże­nie reg­u­larne powin­no więc wyglą­dać następu­ją­co:

([0-9]{1,3}\.){3}[0-9]{1,3}

(oso­by zaz­na­jomione z tem­atyką zapewne zauważą, że to wyraże­nie dopa­su­je także niepraw­idłowe adresy — wiem, ale wpis ten jest poświę­cony wyraże­niom reg­u­larnym a nie zagad­nieniom ruchu sieciowego, więc odpuszcza­my abso­lut­ną poprawność na rzecz nau­ki ;) )

I tutaj jeszcze słowo odnośnie Javy i back­slashów — ponieważ back­slash jest też uży­wany w cią­gach znaków w Javie (np. żeby wstaw­ić nową lin­ię uży­wamy \n) to każdy back­slash musimy poprzedz­ić kole­jnym. Dlat­ego wyraże­nie reg­u­larne:

a\.b

zapisze­my jako:

String regex = "a\\.b";

W drugiej lin­i­jce najpierw mówimy, że kole­jny znak po pier­wszym back­slashu trak­tuj spec­jal­nie (== jest to nor­mal­ny back­slash), a więc wyraże­nie reg­u­larne otrzy­mu­je tylko jeden back­slash. Ciekaw­ie robi się jeśli chce­my np. dopa­sować właśnie back­slash (np. a\b — taka kon­strukc­ja przy­dat­na jest do wery­fikowa­nia nazw plików/ścieżek w sys­temach Win­dows), dajmy na to wyraże­niem:

a\\b

Musimy je zapisać w Javie jako:

String regex = "a\\\\b";

Każ­da para back­slashy przekła­da sie na jeden back­slash w wyraże­niu reg­u­larnym. Taka drob­na niedo­god­ność.

Korzystanie z wyrażeń regularnych w języku Java

W języku Java więk­szość oper­acji na wyraże­ni­ach reg­u­larnych wykonu­je­my z uży­ciem klas Pat­tern oraz Match­er. Klasy te ofer­u­ją o wiele bardziej zaawan­sowaną funkcjon­al­ność niż opisana poniżej (np. może­my anal­i­zować dopa­sowanie do każdej z grup z osob­na), sporo kom­plet­nych infor­ma­cji zna­jdziemy na stron­ie Ora­cle. Tutaj zapoz­namy się tylko z najprost­szym uży­ciem, czyli sprawdze­niem czy dany ciąg znaków pasu­je do naszego wyraże­nia reg­u­larnego (inny­mi słowy, czy nasze wyraże­nie reg­u­larne dopa­sowu­je dany ciąg znaków).

Zaczni­jmy od klasy Pat­tern — klasa ta reprezen­tu­je nasze wyraże­nie reg­u­larne, skom­pi­lowane —  tzn. wstęp­nie przetwor­zone przez kom­put­er, dzię­ki czemu wielokrotne jego wywołanie będzie bardziej wyda­jne. Obiekt z reprezen­tacją naszego wyraże­nia otrzy­mamy wywołu­jąc staty­czną metodę com­pile:

Pattern pattern = Pattern.compile("a*b?cde");

Meto­da ta rzu­ca wyjątek Pat­tern­Syn­tax­Ex­cep­tion, który jest jed­nak typu unchecked więc nie musimy dodawać try-catch (ale oczy­wiś­cie może­my — jeśli chcesz odświeżyć sobie wiedzę o wyjątkach, zapraszam do ponownego odwiedzenia lekcji #3).

Otrzy­many obiekt pat­tern ma metodę match­er, która zwraca nam obiekt typu Match­er.

Matcher matcher = pattern.matcher("jakis ciag znakow");

Mówiąc w uproszcze­niu — Pat­tern to nasze wyraże­nie reg­u­larne, a Match­er to jego dopa­sowanie (lub nie) do konkret­nego ciągu znaków. Obiekt match­er posi­a­da metodę match­es, która mówi nam czy cały ciąg znaków uży­ty do utworzenia Matchera pasu­je do naszego wyraże­nia reg­u­larnego.

matcher.matches(); //zwraca true lub false

Jeśli chce­my wykon­ać sprawdze­nie tylko jeden raz, może­my sko­rzys­tać z metody-skró­tu klasy Pat­tern:

Pattern.matches("regex", "ciąg do sprawdzenia"); //zwraca true lub false

Jest to funkcjon­al­nie równoważne z poniższym frag­mentem:

Pattern pattern = Pattern.compile("regex");
Matcher matcher = pattern.matcher("ciąg do sprawdzenia");
matcher.matches(); //zwraca true lub false

Praw­da, że proste, log­iczne i przy­jemne? :) Zapraszam oczy­wiś­cie do samodziel­nych ekspery­men­tów.

Zadanie

Zmody­fikuj pro­gram, który już napisałeś tak, aby uży­wał wyrażeń reg­u­larnych w miejs­cach gdzie są klauzule try-catch.

Pseudokod powinien wyglą­dać następu­ją­co:

  1. Wczy­taj infor­ma­c­je od użytkown­i­ka
  2. Dopó­ki to, co podał użytkown­ik nie pasu­je do wyraże­nia reg­u­larnego, proś go ponown­ie o podanie danych
  3. Jeśli to, co wpisał użytkown­ik pasu­je do wyraże­nia reg­u­larnego prze­jdź dalej i wykon­aj właś­ci­wy kod (np. kon­wer­s­ja daty) — możli­we, że nadal będziesz musi­ał uży­wać bloków try-catch w niek­tórych miejs­cach (przez checked excep­tions)

zip Pobierz rozwiązanie tego zada­nia

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przy­dat­ną, mamy małą prośbę: pol­ub nasz fan­page. Dzię­ki temu będziesz zawsze na bieżą­co z nowy­mi treś­ci­a­mi na blogu ( i oczy­wiś­cie, z nowy­mi częś­ci­a­mi kur­su Javy). Dzię­ki!

  • 2
  •  
  •  
  •  
  •