Regex w Java
Wyrażenia regularne, klasy Pattern i Matcher
Wyrażenie regularne to tekst, który opisuje wzorzec wyszukiwania dopasowujący się do znaków innego tekstu. Takie wyrażenia przydają się do walidacji danych wejściowych, a także zapewnienia, aby dane miały odpowiedni format. Na przykład kod ZIP musi się składać z pięciu cyfr, a nazwisko może zawierać tylko litery, spacje, apostrofy i łączniki. Jednym z zastosowań wyrażeń regularnych jest wspomaganie procesu tworzenia kompilatora. Często duże i złożone wyrażenie regularne służy do sprawdzenia poprawności składni programu. Jeśli kod programu nie pasuje do wyrażenia, kompilator „wie”, że w kodzie znajduje się błąd
składniowy.
Klasa String oferuje kilka metod do przeprowadzania operacji na wyrażeniach regularnych — najprostszą z nich jest operacja dopasowania. Metoda matches otrzymuje tekst definiujący wyrażenie regularne i na jego podstawie sprawdza zawartość tekstu w obiekcie, dla którego została wywołana. Metoda ta zwraca wartość logiczną wskazującą, czy dopasowanie się powiodło.
Wyrażenie regularne składa się z literałów znakowych i symboli specjalnych. Rysunek 14.19 przedstawia kilka predefiniowanych klas znaków, które mogą być używane wewnątrz wyrażeń regularnych. Klasa znaków to sekwencja ucieczki reprezentująca grupę znaków. Cyfrą jest dowolny znak numeryczny. Znakiem słowa jest dowolna litera (mała lub duża, ale bez znaków diakrytycznych),
dowolna cyfra lub znak podkreślenia. Znakiem białej spacji jest spacja, tabulacja, powrót karetki, znak nowego wiersza lub znak nowej strony. Każda klasa odpowiada pojedynczemu znakowi w tekście, który chcemy dopasować.
Znak | Dopasowuje się do |
d | dowolnej cyfry |
w | dowolnego znaku słowa |
s | dowolnego znaku białej spacji |
D | dowolnego znaku niebędącego cyfrą |
W | dowolnego znaku niebędącego znakiem słowa |
S | dowolnego znaku niebędącego znakiem białej spacji |
Rysunek 14.19. Predefiniowane klasy znaków
Wyrażenia regularne nie ograniczają się do predefiniowanych klas znaków. Wykorzystują różne operatory i inne formy zapisu, aby umożliwić tworzenie złożonych wzorców. Kilka tych technik zastosujemy w aplikacji z rysunków 14.20 i 14.21, która za pomocą wyrażeń regularnych sprawdza dane wpisane przez użytkownika. (Uwaga: Przedstawiona aplikacja nie obsługuje wszystkich możliwych poprawnych wersji danych).
2 // Walidacja informacji za pomocą wyrażeń regularnych
3
4 public class ValidateInput {
5 // Walidacja imienia
6 public static boolean validateFirstName(String firstName) {
7 return firstName.matches("[A-Z][a-zA-Z]*");
8 }
9
10 // Walidacja nazwiska
11 public static boolean validateLastName(String lastName) {
12 return lastName.matches("[a-zA-z]+(['-][a-zA-Z]+)*");
13 }
14
15 // Walidacja ulicy
17 return address.matches(
18 "([a-zA-Z]+|[a-zA-Z]+s[a-zA-Z]+)s+d+");
19 }
20
21 // Walidacja miasta
22 public static boolean validateCity(String city) {
23 return city.matches("([a-zA-Z]+|[a-zA-Z]+s[a-zA-Z]+)");
24 }
25
26 // Walidacja województwa
27 public static boolean validateState(String state) {
28 return state.matches("([-a-zA-Z]+|[a-zA-Z]+s[a-zA-Z]+)");
29 }
30
31 // Walidacja kodu pocztowego
32 public static boolean validateZip(String zip) {
33 return zip.matches("d{5}");
34 }
35
36 // Walidacja numeru telefonu
37 public static boolean validatePhone(String phone) {
38 return phone.matches("[1-9]d{2}-d{3}-d{3}");
39 }
40 }
Rysunek 14.20. Walidacja informacji za pomocą wyrażeń regularnych
2 // Pobieranie i sprawdzanie danych od użytkownika za pomocą klasy ValidateInput
3 import java.util.Scanner;
4
5 public class Validate {
6 public static void main(String[] args) {
7 // Pobierz dane
8 Scanner scanner = new Scanner(System.in);
9 System.out.println("Wpisz imię:");
10 String firstName = scanner.nextLine();
11 System.out.println("Wpisz nazwisko:");
12 String lastName = scanner.nextLine();
13 System.out.println("Wpisz ulicę:");
14 String address = scanner.nextLine();
15 System.out.println("Wpisz miasto:");
16 String city = scanner.nextLine();
17 System.out.println("Wpisz województwo:");
18 String state = scanner.nextLine();
19 System.out.println("Wpisz kod pocztowy (bez łącznika):");
20 String zip = scanner.nextLine();
21 System.out.println("Wpisz numer telefonu:");
22 String phone = scanner.nextLine();
23
24 // Zwaliduj dane i wyświetl komunikat o błędzie
25 System.out.println(" Walidacja wyników:");
26
27 if (!ValidateInput.validateFirstName(firstName)) {
28 System.out.println("Niepoprawne imię");
29 }
30 else if (!ValidateInput.validateLastName(lastName)) {
31 System.out.println("Niepoprawne nazwisko");
32 }
33 else if (!ValidateInput.validateAddress(address)) {
34 System.out.println("Niepoprawna ulica");
35 }
36 else if (!ValidateInput.validateCity(city)) {
37 System.out.println("Niepoprawne miasto");
38 }
39 else if (!ValidateInput.validateState(state)) {
40 System.out.println("Niepoprawne województwo");
41 }
42 else if (!ValidateInput.validateZip(zip)) {
43 System.out.println("Niepoprawny kod pocztowy");
44 }
45 else if (!ValidateInput.validatePhone(phone)) {
46 System.out.println("Niepoprawny numer telefonu");
47 }
48 else {
49 System.out.println("Dane poprawne. Dziękujemy.");
50 }
51 }
52 }
Rysunek 14.20 przedstawia kod przeprowadzający walidację.
Wiersz 7. sprawdza poprawność imienia. Aby dopasować się do zbioru znaków, który nie ma predefiniowanej klasy znaków, należy użyć nawiasów kwadratowych []. Na przykład wzorzec "[aeyioąęuó]" dopasuje się do pojedynczej samogłoski w wyrazie. Zakres znaków zapisuje się, stosując znak łącznika (-) między dwoma znakami.
W przykładzie ["A-Z"] dopasowuje się do pojedynczej dużej litery (bez polskich znaków diakrytycznych). Jeśli pierwszym znakiem w nawiasach jest "^", wyrażenie przyjmuje dowolne znaki poza wskazanymi. Pamiętaj jednak, że "[^Z]" to nie to samo co "[A-Y]", czyli dopasowanie się do liter od A do Y — "[^Z]" dopasowuje się do dowolnego znaku poza dużą literą „Z”, włączając w to małe litery
i inne znaki, np. znak przejścia do nowego wiersza. Zakresy w klasach znaków definiują tak naprawdę wartości całkowite poszczególnych znaków. W przykładzie "[A-Za-z]" dopasowuje się do małych i dużych liter. Zakres "[A-z]" dopasowuje się do wszystkich liter, ale również do innych znaków (np. [ i ), które mają wartość liczbową między literami Z i a. Podobnie jak predefiniowane klasy znaków, klasy znaków zawarte w nawiasach kwadratowych dotyczą pojedynczego znaku w analizowanym
tekście.
W wierszu 7. znak gwiazdki (*) po drugiej klasie znaku wskazuje, że można dopasowywać się do dowolnej liczby liter. Najczęściej, gdy operator "*" pojawi się w wyrażeniu, aplikacja stara się dopasować do zera lub więcej wystąpień podwyrażenia znajdującego się tuż przed "*". Operator "+" dopasowuje się do jednego lub więcej wystąpień podwyrażenia bezpośrednio przed nim. Oznacza to,
że zarówno "A*", jak i "A+" dopasuje się do "AAA" lub "A", ale tylko "A*" dopasuje się do pustego tekstu.
Jeśli metoda validateFirstName zwróci true (wiersz 27. z rysunku 14.21), aplikacja rozpocznie walidację nazwiska (wiersz 30.), wywołując metodę validateLastName (wiersze od 11. do 13. z rysunku 14.20). Wyrażenie regularne związane z nazwiskiem dopasowuje się do dowolnej liczby liter z ewentualnymi znakami apostrofu i łącznika.
Wiersz 33. z rysunku 14.21 wywołuje metodę validateAddress (wiersze od 16. do 19. z rysunku 14.20), aby sprawdzić nazwę ulicy. Dopasowanie na końcu sprawdza, czy pojawi się jedna lub więcej liczb (d+). Używamy dwóch znaków , gdyż pierwszy znak rozpoczyna sekwencję ucieczki, więc dopiero d odpowiada wzorcowi wyrażenia regularnego d. Ponieważ między nazwą ulicy i numerem domu powinien być odstęp, stosujemy s+, aby dopasować się do jednej lub więcej białych spacji. Znak "|" dopasowuje wyrażenie do wersji z lewej lub z prawej. Na przykład wyrażenie "Witaj, (Janie|Anno)" dopasuje się zarówno do "Witaj, Janie", jak i do "Witaj, Anno". Nawiasy okrągłe służą do grupowania części wyrażenia regularnego. W przedstawionym przykładzie lewa strona dopasowuje się do pojedynczego słowa, a prawda do dwóch słów oddzielonych dowolną liczbą białych spacji. Oznacza to, że przed numerem domu może się znaleźć jedno lub dwa słowa. A zatem wyrażenie będzie pasowało zarówno do adresu "Na
skarpie 12", jak i do adresu "Lodowa 12". Wyrażenia dotyczące miasta (wiersze od 22. do 24. z rysunku 14.20) i województwa (wiersze od 27. do 29. z rysunku 14.20) dopasowują się do pojedynczego słowa z przynajmniej jednym znakiem lub ewentualnie do dwóch słów oddzielonych przynajmniej jedną białą spacją, więc dopasowanie powiedzie się zarówno dla "Gliwice", jak i dla "Ustrzyki
Dolne".
Regex Java - Kwantyfikatory
Znaki gwiazdki (*) i plusa (+) są formalnie nazywane kwantyfikatorami. Na rysunku 14.22 wymieniamy wszystkie kwantyfikatory. Opisaliśmy już, jak działają kwantyfikatory gwiazdki (*) i plusa (+). Wszystkie kwantyfikatory wpływają tylko na podwyrażenie bezpośrednio przed nimi. Kwantyfikator znaku zapytania (?) dopasowuje się do zera lub jednego wystąpienia wyrażenia. Nawiasy klamrowe zawierające liczbę ({n}) powodują dopasowanie się do dokładnie n wystąpień wyrażenia. Ten sposób użycia wykorzystujemy w celu dopasowania się do kodu pocztowego (wiersz 33. z rysunku 14.20). Jeśli nawiasy zawierają dwie liczby ({n,m}), dopasowanie dotyczy od n do m wystąpień wyrażenia. Kwantyfikatory mogą być stosowane do wzorców zawartych w nawiasach, aby w ten sposób tworzyć bardziej złożone wyrażenia regularne.
Kwantyfikator | Pasuje do… |
* | zera lub więcej wystąpień wzorca |
+ | jednego lub więcej wystąpień wzorca |
? | zera lub jednego wystąpienia wzorca |
{n} | dokładnie n wystąpień wzorca |
{n,} | przynajmniej n wystąpień wzorca |
{n,m} | od n do m (włącznie) wystąpień wzorca |
Rysunek 14.22. Kwantyfikatory używane w wyrażeniach regularnych
Wszystkie kwantyfikatory są zachłanne. Będą starały się dopasować do jak największej liczby wystąpień, o ile nadal będzie spełnione dopasowanie. Jeśli jednak po którymkolwiek z kwantyfikatorów pojawi się znak zapytania (?), kwantyfikator staje się leniwy (przestaje być zachłanny). W tej wersji będzie się starał dopasować do jak najmniejszej liczby wystąpień, o ile nadal wartość będzie pasowała do wyrażenia.
Kod pocztowy (wiersz 33. z rysunku 14.20) stosuje pięciokrotne dopasowanie do cyfry. Wyrażenie regularne używa klasy dla cyfr i kwantyfikatora z liczbą 5 w nawiasach. Numer telefonu (wiersz 38. z rysunku 14.20) dopasowuje się do trzech cyfr (pierwsza z nich musi być różna od 0), znaku łącznika, trzech następnych cyfr, ponownie znaku łącznika i trzech ostatnich cyfr. Metoda matches klasy String dopasowuje cały tekst do wyrażenia regularnego. Jako nazwisko chcemy dopasować "Nowak", a nie "9@Nowak#". Dopasowanie tylko do części tekstu spowoduje, że metoda zwróci wartość false.
Regex Java - Zastępowanie fragmentów tekstu i podział tekstu
Czasem warto zastąpić fragmenty tekstu lub podzielić go na fragmenty. Do wykonania tych zadań klasa String udostępnia metody replaceAll, replaceFirst i split. Metody te ilustruje rysunek 14.23.
2 // Metody replaceFirst, replaceAll i split klasy String
3 import java.util.Arrays;
4
5 public class RegexSubstitution {
6 public static void main(String[] args) {
7 String firstString = "To zdanie ma na końcu 5 gwiazdek *****";
8 String secondString = "1, 2, 3, 4, 5, 6, 7, 8";
9
10 System.out.printf("Oryginalny tekst 1: %s ", firstString);
11
12 // Zamień '*' na '^'
13 firstString = firstString.replaceAll("*", "^");
14
15 System.out.printf("^ zastąpiło *: %s ", firstString);
16
17 // Zamień 'gwiazdek' na 'karet'
18 firstString = firstString.replaceAll("gwiazdek", "karet");
19
20 System.out.printf(
21 ""karet" zastąpiło "gwiazdek": %s ", firstString);
22
23 // Zastąp każdy wyraz słowem 'wyraz'
24 System.out.printf("Każdy wyraz zastąpiony słowem "wyraz": %s ",
25 firstString.replaceAll("w+", "wyraz"));
26
27 System.out.printf("Oryginalny tekst 2: %s ", secondString);
28
29 // Zastąp pierwsze trzy cyfry słowem 'cyfra'
30 for (int i = 0; i < 3; i++) {
31 secondString = secondString.replaceFirst("d", "cyfra");
32 }
33
34 System.out.printf(
35 "Pierwsze cyfry zastąpione słowem "cyfra" : %s ", secondString);
36
37 System.out.print("Tekst po rozdzieleniu znakami przecinka: ");
38 String[] results = secondString.split(",s*"); // Podział na podstawie przecinka
39 System.out.println(Arrays.toString(results)); // Wyświetlenie wyniku
40 }
41 }
"karet" zastąpiło "gwiazdek": To zdanie ma na końcu 5 karet ^^^^^ Każdy wyraz zastąpiony słowem "wyraz": wyraz wyraz wyraz wyraz wyraz wyraz wyraz ^^^^^
Pierwsze cyfry zastąpione słowem "cyfra" : cyfra, cyfra, cyfra, 4, 5, 6, 7, 8
Tekst po rozdzieleniu znakami przecinka: [cyfra, cyfra, cyfra, 4, 5, 6, 7, 8]
Rysunek 14.23. Metody replaceFirst, replaceAll i split klasy String
Metoda replaceAll zastępuje tekst w obiekcie String nowym tekstem (drugi argument) za każdym razem, gdy oryginalny tekst pasuje do wyrażenia regularnego (pierwszy argument). Wiersz 13. zastępuje każde wystąpienie "*" w firstString wersją "^". Wyrażenie regularne "*" zawiera przed znakiem * dwa znaki lewego ukośnika. Standardowo * to kwalifikator, który dotyczy dopasowywania do dowolnej liczby wystąpień. Wiersz 13. „chce” jednak odnaleźć wszystkie wystąpienia literału *, wymaga to więc poprzedzenia znaku * znakiem ucieczki . Użycie znaku ucieczki powoduje, że wyrażenie zamiast stosować kwalifikator
użyje faktycznego znaku jako części wzorca. Ponieważ jednak wyrażenie znajduje się w tekście Javy, który również stosuje znak jako początek sekwencji ucieczki, musimy użyć dwóch lewych ukośników. Oznacza to, że tekst "*" reprezentuje wyrażenie regularne *, które spowoduje dopasowanie się do pojedynczego znaku *. W wierszu 18. każde dopasowanie wyrażenia regularnego "gwiazdek" z firstString jest zastępowane przez "karet". Wiersz 25. używa metody replaceAll do zastąpienia wszystkich wyrazów słowem "wyraz".
Metoda replaceFirst (wiersz 31.) zastępuje pierwsze wystąpienie wzorca. Ponieważ obiekty String w Javie są niezmienne, metoda zwraca nowy obiekt z zastąpionymi odpowiednimi znakami. Ten wiersz przyjmuje oryginalny tekst i zastępuje go tekstem uzyskanym z replaceFirst. Wykonując trzy iteracje, zastępujemy trzy pierwsze cyfry (d) w secondString tekstem "cyfra".
Metoda split dzieli obiekt String na kilka mniejszych tekstów. Podział następuje w każdym miejscu, które pasuje do wskazanego wyrażenia regularnego. Metoda split zwraca tablicę obiektów String znajdujących się między dopasowanymi wyrażeniami regularnymi. W wierszu 38. używamy metody split do tokenizacji tekstu z oddzielonymi przecinkami liczbami. Argumentem jest wyrażenie regularne określające fragment rozdzielający. W tym przypadku używamy wyrażenia regularnego ",s*" do oddzielania tekstów przy każdym wystąpieniu przecinka i spacji — tekst w Javie w postaci ",s*" to wyrażenie ,s*. Dopasowując
się do znaków białej spacji, eliminujemy z wyników również dodatkowe spacje przed liczbami. Przecinki i spacje nie są zwracane jako część fragmentu tekstu. Wiersz 39. używa metody toString tablic, aby wyświetlić zawartość tablicy results w nawiasach kwadratowych z wartościami oddzielonymi spacjami.
Regex Java - Klasy Pattern i Matcher
Poza możliwościami dotyczącymi wyrażeń regularnych oferowanymi przez klasę String Java zapewnia również kilka innych klas znajdujących się w pakiecie java.util.regex, aby wspomóc obsługę wyrażeń regularnych. Klasa Pattern reprezentuje wyrażenie regularne. Klasa Matcher zawiera zarówno wyrażenie regularne w postaci wzorca, jak i obiekt CharSequence, w którym nastąpi wyszukiwanie.
Typ CharSequence (pakiet java.lang) to interfejs, który umożliwia odczyt ciągu znaków. Interfejs wymaga zadeklarowania metod charAt, length, subSequence i toString. Ponieważ interfejs implementuje zarówno klasa String, jak i String Builder, którąkolwiek z nich można zastosować wraz z klasą Matcher.
Typowy błąd programistyczny
Wyrażenia regularne można testować względem dowolnego obiektu klasy implementującej interfejs CharSequence, ale musi ono być obiektem String. Próba utworzenia wyrażenia regularnego jako obiekt StringBuilder jest błędem.
Jeśli wyrażenie regularne będzie używane tylko raz, można zastosować metodę statyczną matches obiektu Pattern. Metoda ta przyjmuje tekst, który określa wyrażenie regularne, i obiekt CharSequence, na którym należy przeprowadzić dopasowanie.
Zwraca wartość logiczną wskazującą, czy analizowany tekst (drugi argument) pasuje do wyrażenia regularnego.
Jeżeli wyrażenie regularne ma być stosowane więcej niż raz (na przykład w pętli), wydajniejsze będzie użycie metody statycznej compile klasy Pattern, aby utworzyć obiekt Pattern z wyrażeniem regularnym. Metoda ta otrzymuje tekstową reprezentację wyrażenia regularnego, które może później posłużyć do wywołania metody matcher. Metoda otrzymuje obiekt CharSequence i zwraca obiekt Matcher.
Klasa Matcher udostępnia metodę matches, która realizuje te same zadania co metoda matches klasy Pattern, ale nie przyjmuje żadnych argumentów — wzorzec i tekst do przeszukania znajdują się już w obiekcie. Klasa Matcher udostępnia metody dodatkowe, takie jak find, lookingAt, replaceFirst i replaceAll. Rysunek 14.24 przedstawia prosty przykład wykorzystujący wyrażenia regularne.
Program dopasowuje daty urodzenia do wyrażenia regularnego. Dopasowanie wymaga, aby narodziny nie miały miejsca w kwietniu i dotyczyły tylko osób o imieniu zaczynającym się na literę "J".
2 // Klasy Pattern i Matcher
3 import java.util.regex.Matcher;
4 import java.util.regex.Pattern;
5
6 public class RegexMatches {
7 public static void main(String[] args) {
8 // Utwórz wyrażenie regularne
9 Pattern expression =
10 Pattern.compile("J.*dd-d[0-35-9]-dd");
11
12 String string1 = "Julia urodziła się 12-05-75 " +
13 "Zofia urodziła się 04-11-68 " +
14 "Jan urodził się 24-04-73 " +
15 "Jacek urodził się 17-12-77";
16
17 // Dopasowuje wyrażenie regularne do tekstu i je wyświetla
18 Matcher matcher = expression.matcher(string1);
19
20 while (matcher.find()) {
21 System.out.println(matcher.group());
22 }
23 }
24 }
Rysunek 14.24. Klasy Pattern i Matcher
Jacek urodził się 17-12-77
Rysunek 14.24. Klasy Pattern i Matcher — ciąg dalszy
Wiersze 9. i 10. tworzą obiekt Patter, wywołując metodę statyczną compile. Znak kropki (".") w wyrażeniu regularnym (wiersz 10.) dopasowuje się do pojedynczego znaku poza znakiem nowego wiersza. Wiersz 18. tworzy obiekt Matcher dla skompilowanego wyrażenia regularnego i wskazanej sekwencji (string1). Wiersze od 20. do 22. używają pętli while, aby przejść iteracyjnie przez obiekt
String. Metoda find (wiersz 20.) klasy Matcher próbuje dopasować fragment tekstu do wzorca. Każde wywołanie metody find zaczyna się od miejsca, gdzie zakończyło się poprzednie zapytanie. Pozwala to odnaleźć wiele pasujących fragmentów. Metoda lookingAt klasy Matcher działa podobnie, ale zawsze zaczyna się od początku tekstu i znajduje pierwsze dopasowanie (o ile istnieje).
Typowy błąd programistyczny
Metoda matches (klas String, Pattern lub Matcher) zwróci wartość true tylko wtedy, gdy cały tekst pasuje do wzorca. Metody find i lookingAt (klasa Matcher) zwracają true, jeśli fragment tekstu pasuje do wzorca.
Wiersz 21. używa metody group klasy Matcher, która zwraca fragment tekstu dopasowany do wzorca. To tekst dopasowany jako ostatni przez wywołanie metody find lub lookingAt. Wynik działania programu z rysunku 14.24 pokazuje, że znaleziono dwa dopasowania w tekście string1.
Java SE 8
Można połączyć przetwarzanie wyrażeń regularnych z lambdami i strumieniami Javy SE 8, aby w ten sposób tworzyć rozbudowane aplikacje do przetwarzania tekstów lub plików.
Java SE 9 — nowe metody klasy Matcher
Java SE 9 dodaje kilka nowych wersji metod klasy Matcher: appendReplacement, appendTail, replaceAll, results i replaceFirst. Metody appendReplacement i append Tail po prostu otrzymują obiekty StringBuilder zamiast StringBuffer. Metody replaceAll, results i replaceFirst powstały w celu ich użycia w połączeniu z lambdami i strumieniami.
Programowanie w Javie. Solidna wiedza w praktyce. Wydanie XI, Autorzy: Paul Deitel, Harvey Deitel, Wydawnictwo: Helion