2. Wyrażenia regularne


Wyrażenia regularne stanowią silne narzędzie przetwarzania tekstów (wyszukiwania, modyfikowania, analizy składniowej). Oparte na matematecznej teorii zbiorów i wyrażeń regularnych, weszły do szerszego zastosowania w informatyce u początku lat siedemdziesiątych wraz z narzędziamu systemu Unix (np. grep). Znalazły się następnie w arsenale wielu języków programowania, wśród których na szczególną wzmiankę zasługuje Perl, który wzbogacił składnię i możliwości wyrażeń regularnych. W Javie, poczynając od wersji 1.4, klasy i metody, umożliwiające posługiwanie się wyrażeniami regularnymi są dostępne w ramach standardowego zestawu pakietów.


2.1. Podstawowe zasady

Regularne wyrażenie stanowi opis wspólnych cech (składni) zbioru łańcuchów znakowych


Możemy sobie wyobrażać, że regularne wyrażenie jest pewnym wzorcem, który opisuje jeden lub wiele napisów, pasujących do tego wzorca. Wzorzec taki zapisujemy za pomocą specjalnej składni wyrażeń regularnych.

Najprostszym wzorcem jest po prostu sekwencja znaków, które nie mają specjalnego znaczenia (sekwencja literałów).
Np. wyrażenie regularne abc stanowi wzorzec opisujący trzy występujące po sobie znaki: a, b, i c. Wzorzec ten opisuje jeden napis "abc".

We wzorcach możemy stosować znaki specjalne (tzw. metaznaki) oraz tworzone za ich pomocą konstrukcje składniowe. Do znaków specjalnych należą:

$^.*
+?[]
(
}
\
Uwagi:
  1. jesli chcemy traktować znaki specjalne jako literały - poprzedzamy je odwrotnym ukośnikiem \.
  2. w niektórych konstrukcjach składniowych metaznaki tracą specjalne znaczenie i są traktowane literalnie.
Za pomocą znaków specjalnych i tworzonych za ich pomocą bardziej rozbudowanych konstrukcji składniowych opisujemy m.in.
Np. wyrażenie regularne [0-9] stanowi wzorzec opisujący jeden znak, który może być dowolną cyfrą 0,1,2,... ,9. Wzorzec ten opisuje wszystkie napisy składające się z jednej cyfry.
A wyrażenie regularne a.*z (a, kropka, gwiazdka, z) opisuje dowolną sekwencję znaków, zaczynających się od litery a i kończących się literą z. Do wzorca tego pasują np. następujące napisy: "az", "abz", "a x y z".

Składnię wyrażeń regularnych będziemy omawiać bardziej szczegółowo w następnych punktach.

Wyrażeń regularnych możemy użyć m.in. do:
W Javie służą do tego klasy pakietu java.util.regex: Pattern i Matcher.

Przed zastosowaniem wyrażenia regularnego do składniowej analizy jakiegoś napisu musi ono być skompilowane. Obiekty klasy Pattern reprezentują skompilowane wyrażenia regularne, a obiekty te uzyskujemy za pomocą statycznych metod klasy Pattern - compile(...), mających za argument wyrażenie regularne.
Obiekty klasy Matcher wykonują operacje wyszukiwania  w tekście za pomocą interpretacji skompilowanego wyrażenia regularnego i dopasowywania go do tekstu lub jego częsci.
Obiekt-matcher jest zawsze związany z danym wzorcem. Zatem uzyskujemy go od obiektu-wzorca za pomocą metody matcher(...)  klasy Pattern, podając jako jej argument przeszukiwany tekst. Następnie możemy dokonywać różnych operacji przeszukiwania i zastępowania tekstu poprzez użycie różnych metod klasy Matcher.
W szczególności:
Wszystkie metody dopasowania/wyszukiwania zwracają wartości typu boolean, stwierdzające dopasowanie (true) lub jego brak (false). Więcej informacji o dopasowaniu (jaki konkretnie tekst  pasuje do wzorca, gdzie jest jego początek, a gdzie koniec itp.) można uzyskać odpytując matcher o aktualny jego stan za pomocą odpowiednich metod.
Będziemy o tym mówić dokładnie w dalszej części rozdziału.

Typową sekwencję operacji, potrzebnych do zastosowania  wyrażeń regularnych można opisać w następujący schematyczny sposób.


A. Tekst, podlegający dopasowaniu może być reprezentowany przez obiekt dowolnej klasy implementującej interfejs CharSequence (np. String, StringBuffer, CharBuffer z pakietu java.nio) np:

    String text = "ala-127";

B. Tworzymy wyrażenie regularne jako napis np.

    String regexp = "[0-9]";  

C. Kompilujemy wyrażenie regularne i uzyskujemy skompilowany wzorzec.

    Pattern pattern = Pattern.compile(regexp);

D. Tworzymy obiekt-matcher związany z danym wyrażeniem, podając przy tym tekst do dopasowania:

    Matcher matcher = pattern.matcher(text);

E. Szukamy dopasowania tekstu ( w tekście ) zgodnie ze wzorcem np.

    boolean hasMatch = matcher.find();  
    albo:
    boolean isMatching = matcher.matches();


Inne możliwe sposoby postępowania opisane zostaną w podpunkcie dotyczącym dopasowania i wyszukiwania.

Przedtem zapoznamy się nieco bliżej ze składnią wyrażeń regularnych,
Przy  prezentacji przykładów pokazane zostanie zarówno działanie metody marches() jak i find(). Wykorzystujemy przy tym następującą metodę, która jako argumenty otrzymuje wyrażenie regularne oraz  analizowany za jego pomocą tekst, a zwraca opis wyników działania metod matches() i find().
  String report(String regex, String text) {
    String result = "Wzorzec: \"" + regex + "\"\n" +
                    "Tekst: \"" + text + "\"";
    // Kompilacja wzorca
    // Gdy wzorzec jest składniowo błędny
    // wystąpi wyjątek PatternSyntaxException
    Pattern pattern = null;
    try {
      pattern = Pattern.compile(regex);
    } catch (Exception exc) {
        return result + "\n" + exc.getMessage(); // zwracamy komunikat o błędzie
    }

    // Uzyakanie matchera dla podanego tekstu
    Matcher matcher = pattern.matcher(text);

    // Próba dopasowania całego tekstu do wzorca
    boolean isMatching = matcher.matches();
    result += "\nmatches(): Cały tekst" + (isMatching ? "" : " NIE") +
              " pasuje do wzorca.";

    matcher.reset(); // Przywrócenie początkowej pozycji matchera

    // Teraz stosujemy metodę find()
    // Jej wywołanie zwraca true po znalezieniu pierwszego
    // pasującego do wzorca podłańcucha w tekście
    // Kolejne wywołania pozwalają wyszukiwać kolejne pasujące podłańcuchy
    // wynik false oznacza, że w tekście nie ma już pasujących podłańcuchów
    boolean found = matcher.find();
    if (!found)
      result += "\nfind(): Nie znaleziono żadnego podłańcucha " +
                "pasującego do wzorca";
    else
      do {
        result += "\nfind(): Dopasowano podłańcuch \"" + matcher.group() +
                   "\" od pozycji " + matcher.start() +
                   " do pozycji " + matcher.end() + ".";
      } while(matcher.find());

    return result;
  }
Uwaga: nie szkodzi jeśli kod tej metody nie jest teraz w pełni zrozumiały, wykorzystamy ją w następnych punktach wyłącznie do prezentacji składni i efektów interpretacji wyrażeń regularnych. Szczegółowe omówienie metod dopasowania oraz metod uzyskiwania informacji o stanie matchera zawarto dalej.

Składnia wyrażeń regularnych i reguły rządzące jej interpretacją są - abstrahując od najprostszych przypadków - dość skomplikowane, a czasem mało intuicyjne.
Tworzenie dobrych wyrażeń regularnych jest prawdziwą sztuką, a zagadnieniu temu poświęcono wielosetstronicowe książki. Do najlepszych (a zarazem dość zaawansowanych) należy "Mastering regular expressions" autorstwa Jeffreya E. F. Friedla,  wydana przez O'Reilly w roku 1997 i 2003, jak również nie tak dawno w tłumaczeniu polskim w Wydawnictwie Helion. Oczywiście, w tym rozdziale nie sposób w tak dogłębny i szczegółowy sposób przedstawić składni i mechanizmów interpretacji wyrażeń regularnych. Mając nadzieję, że przedstawiony dalej materiał będzie wystarczający dla codziennych potrzeb, Czytelnikom zainteresowanym pogłebieniem wiedzy o wyrażeniach regularnych można polecić tę dodatkową lekturę.

2.2. Literały

Literały użyte w wyrażeniu regularnym są dopasowywane po kolei.
Na przykład wyrażenie regularne:

    String regexp = "ala";

jest interpretetewane w następujący sposób: w podanym łąńcuchu wejściowym wyszukiwane są kolejno występujące po sobie znaki 'a'. 'l' i 'a'.  Wynikiem metody matches() jest true tylko wtedy, gdy cały tekst pasuje do tego wzorca ("ala"), metoda find() umożliwia znalezienie w tekście wielu podłańcuchów "ala".

Przykład:
Wzorzec: "ala"
Tekst: "ala"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "ala" od pozycji 0 do pozycji 3.

Wzorzec: "ala"
Tekst: "Ta ala ma kota i ala ma psa ala"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "ala" od pozycji 3 do pozycji 6.
find(): Dopasowano podłańcuch "ala" od pozycji 17 do pozycji 20.
find(): Dopasowano podłańcuch "ala" od pozycji 28 do pozycji 31.


Jak już wspomniano, jako literałów nie możemy używać symboli mających specjalne znaczenie w składni wyrażeń regularnych. Powstanie wtedy błąd składniowy przy kompilacji wyrażenia. Błędy składniowe (różnego rodzaju) możemy obsługiwać przechwytując wyjątek PatternSyntaxException (jak pokazano na  wydruku metody report(...)).

Oto przykład.

Wzorzec: "(x"
Tekst: "(x"
Unclosed group near index 2
(x
  ^



Warto zauważyć, że użycie znaków specjalnych (metaznaków) może nie powodować błędu składniowego, ale inne od zamierzonych wyniki dopasowania.
Wyobraźmy sobie, że nie znamy znaczenia metaznaków '(' i ')' (no, rzeczywiście, na razie jeszcze nie znamy) i chcemy dopasować/znaleźć literalny tekst (x). Efekty będą inne od oczekiwanych:

Wzorzec: "(x)"
Tekst: "(x)"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "x" od pozycji 1 do pozycji 2.


Jeśli chcemy wyszukiwać w tekście literalne znaki specjalne to w wyrażeniu regularnym powinniśmy je poprzedzić odwrotnym ukośnikiem (nie dotyczy to metaznaków użytych w definicji klas znaków - o czym w następnym podpunkcie).


Wzorzec: "\(x"
Tekst: "(x"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "(x" od pozycji 0 do pozycji 2.


Używając odwrotnego ukośnika możemy także literalnie wprowadzać kody znaków oraz znaki kontrolne.
Tabela poniżej przdestawia sposób literalnego zapisu pewnych specjalnych znaków w wyrażeniu regularnym.

\0nZnak o kodzie ósemkowym n (0 <= n <= 7)
\0nnZnak o kodzie ósemkowym nn (0 <= n <= 7)
\0mnnZnak o kodzie ósemkowym  mnn (0 <= m <= 3, 0 <= n <= 7)
\xhhZnak o kodzie szesnastkowym xhh
\uhhhhZnak o kodzie szesnastkowym xhhhh
\tZnak tabulacji ('\u0009')
\nZnak nowego wiersza ('\u000A')
\rPowrót karetki ('\u000D')
\fNowa strona ('\u000C')
\aAlert (dzwonek) ('\u0007')
\eZnak \  ('\u001B'), można napisac również \\
\cxZnbak sterujący x

Przykład
Wzorzec: "ala\nma\nkota"
Tekst: "ala
ma
kota"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "ala
ma
kota" od pozycji 0 do pozycji 11.



Wyrażenia regularne (w tym te w postaci literałów) możemy ze sobą logicznie łączyć. Tak naprawdę wyrażenie regularne "abc" jest logiczną koniunkcją wyrażeń "a", "b" i "c".
Mamy też logiczną alternatywę wprowadzaną znakiem |.

Jeśli np. chcemy dopasować (lub znaleźć) tekst który jest napisem "ala" albo"kot", albo "pies", możemy zbudować wyrażenie regularne "ala|kot|pies"

Oto przyklad jego dzialania:

Wzorzec: "ala|kot|pies"
Tekst: "ala"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "ala" od pozycji 0 do pozycji 3.

Wzorzec: "ala|kot|pies"
Tekst: "pies"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "pies" od pozycji 0 do pozycji 4.

Wzorzec: "ala|kot|pies"
Tekst: "Loskotek, ala i kot i pies w jednym stali domu"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "kot" od pozycji 3 do pozycji 6.
find(): Dopasowano podłańcuch "ala" od pozycji 10 do pozycji 13.
find(): Dopasowano podłańcuch "kot" od pozycji 16 do pozycji 19.
find(): Dopasowano podłańcuch "pies" od pozycji 22 do pozycji 26.


Zwrócmy szczególną uwagę:

2.3. Klasy znaków

Stosując nawiasy kwadratowe możemy wprowadzać w wyrażeniu regularnym tzw. klasy znaków.

Prosta klasa znaków stanowi ciąg znaków ujętych w nawiasy kwadratowe np.

[123abc]

Matcher dopasuje do takiego wzorca dowolny z wymienionych znaków. Jest to w istocie skrót zapisu 1|2|3|a|b|c.

Jeśli pierwszym znakiem w nawiasach kwadratowych jest ^, to dopasowanie nastąpi dla każdego znaku oprócz wymienionych na liście. Jest to swoista negacja klasy znaków.
Np. do wzorca [^abc] będzie pasował każdy znak oprócz a, b i c.

Możliwe jest także formułowanie zakresów znaków (co już znacznie ułatwia zapis). Przy formułowaniu zakresów używamy naturalnego symbolu -.
Przykładowe wzorce:
[0-9] - dowolna cyfra,
[a-zA-Z] - dowolna mała i duża litera alfabetu angielskiego.
[a-zA-Z0-9] = dowolna cyfra lub litera

i nieco bardziej skomplikowany przykład, w którym po slowie Numer powinna następować spacja, potem zaś jedna z cyfr 1,2,3,7,8,9, następnie dowolna cyfra, ukośnik i dowolny znak oprócz cyfr 0,1,2,3 oraz malej litery alfabetu angielskiego.


Wzorzec: "Numer [1-37-9][0-9]/[^0-3a-z]"

Tekst: "Numer 11/9"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "Numer 11/9" od pozycji 0 do pozycji 10.

Tekst: "Numer 51/9"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca

Tekst: "Numer 38/A"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "Numer 38/A" od pozycji 0 do pozycji 10.

Tekst: "Numer 38/a"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca


Należy pamiętać, że klasa znaków określa jeden znak należący (lub nie) do podanego w nawiasach kwadratowych zestawu.  Kolejność znaków w zestawie nie jest istotna, ale zakresy muszą być podawane w porządku rosnącym.


Ostatnią obserwację obrazują przykłady.


Wzorzec: "[abc][0-9]"
Tekst: "a1"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "a1" od pozycji 0 do pozycji 2.

Wzorzec: "[abc][0-9]"
Tekst: "c3"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "c3" od pozycji 0 do pozycji 2.

Wzorzec: "[bca][0-9]"
Tekst: "a1"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "a1" od pozycji 0 do pozycji 2.

Wzorzec: "[bca][9-0]"
Tekst: "a1"
Illegal character range near index 8
[bca][9-0]
        ^


Ułatwieniem zapisu zakresów są tzw. klasy predefiniowane. Podstawowe klasy predefiniowane pokazuje poniższa tabela.

.Dowolny znak (w zależności od opcji kompilacji wzorca może pasować lub nie do znaku końca wiersza)
\dCyfra: [0-9]
\DNie-cyfra: [^0-9]
\s"Biały" znak: [ \t\n\x0B\f\r]
\SKażdy znak, oprócz "białego": [^\s]
\wJeden ze znaków: [a-zA-Z0-9],  znak "dopuszczalny w słowie"
\WZnak nie będący literą lub cyfrą [^\w]
Uwaga: ogólna reguła - klasy wprowadzane przez duże litery stanowią negację klas definiowanych przez małe litery.

Bardzo ważną predefiniowaną klasą jest "klasa wszystkich znaków", podawana jako kropka. Za jej pomocą możemy dopasować dowolny znak.

Przykład: wzorzec który dopasowuje teksty składające się z trzech dowolnych cyfr, następujących po nich trzech dowolnych znaków i dwóch znaków, nie będących cyframi.


Wzorzec: "\d\d\d...\D\D"

Tekst: "123###a$"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "123###a$" od pozycji 0 do pozycji 8.

Tekst: "123abc12"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca

Tekst: "abc123xx"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca


Pewnym rozszerzeniem podstawowych predefiniowanych klas są klasy znakowe, zdefiniowane w standardzie POSIX. Pokazuje je poniższa tabela,

\p{Lower}Mała litera: [a-z]
\p{Upper}Duża litera: [A-Z]
\p{ASCII}Dowolny znak ASCII :[\x00-\x7F]
\p{Alpha}Dowolna litera: [\p{Lower}\p{Upper}]
\p{Digit}Cyfra: [0-9]
\p{Alnum}Cyfra bądź litera: [\p{Alpha}\p{Digit}]
\p{Punct}Znak punktuacji: ! "#$%&'()*+,-./:;<=>?@[\]^_`{|}~
\p{Graph}Widzialny znak: [\p{Alnum}\p{Punct}]
\p{Print}Drukowalny znak: [\p{Graph}]
\p{Blank}Spacja lub tabulacja: [ \t]
\p{Cntrl}Znak sterujący: [\x00-\x1F\x7F]
\p{XDigit}Cyfra szesnastkowa: [0-9a-fA-F]
\p{Space}Biały znak: [ \t\n\x0B\f\r]

Wypróbujmy tę składnię na przykładzie wzorca, który opisuje tekst zczynający się od dowolnej litery, z następującym po niej dowolnym znakiem punktuacji i dowolną cyfrą.


Wzorzec: "\p{Alpha}\p{Punct}\d"

Tekst: "a,1"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "a,1" od pozycji 0 do pozycji 3.

Tekst: "aa1"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca


Dość nieprecyzyjnie powiedzieliśmy: "zaczynający się od dowolnej litery". Oczywiście (jak widać z tablicy) nie dotyczy to liter polskich (a także innych - od angielskiego - języków).
Zobaczmy:

Wzorzec: "\p{Alpha}\p{Punct}\d"
Tekst: "ć,1"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca


Aha, potrzebny jest Unicode. Do "dopasowywania" znaków Unicodu służą odrębne predefiniowane klasy, niektóre podane w tabeli:

\p{L}Dowolna litera (Unicode)
\p{Lu}Dowolna duża litera (Unicode)
\p{Ll}
Dowolna mała litera
\p{Sc}Symbol waluty
\p{InNazwaBlokuUnicode}
Znak należący do podanego bloku Unicode

Np. wzorzec \p{Ll} będzie pasował do dowolnego znaku Unicode, który jest małą literą, zatem np. do polskich znaków ą, ć, ś itd. (ale oczywiście nie tylko).

Jeśli chodzi nam o znaki tylko z konkretnych bloków Unicode'u (np. alfabetu greckiego, lub cyrylicy) stosujemy ostatni z podanych w tabeli wzorców, podając po słowie In (bez spacji) nazwę bloku np.
\p{InGreek}
\p{InCyrillic}

Stosowane są tutaj nazwy bloków standardu Unicode 3, które - dla ciekawości podaję poniżej (zwróćmy uwagę, że w Unicodzie mamy również najróżniejsze znaki specjalne, matematyczne itp.).
Bloki Unicode (wyciąg)

0000; 007F; Basic Latin
0080; 00FF; Latin-1 Supplement
0100; 017F; Latin Extended-A
0180; 024F; Latin Extended-B
0250; 02AF; IPA Extensions
02B0; 02FF; Spacing Modifier Letters
0300; 036F; Combining Diacritical Marks
0370; 03FF; Greek
0400; 04FF; Cyrillic
0530; 058F; Armenian
0590; 05FF; Hebrew
0600; 06FF; Arabic
0700; 074F; Syriac
0780; 07BF; Thaana
0900; 097F; Devanagari
0980; 09FF; Bengali
...
1F00; 1FFF; Greek Extended
2000; 206F; General Punctuation
2070; 209F; Superscripts and Subscripts
20A0; 20CF; Currency Symbols
20D0; 20FF; Combining Marks for Symbols
2100; 214F; Letterlike Symbols
2150; 218F; Number Forms
2190; 21FF; Arrows
2200; 22FF; Mathematical Operators
2300; 23FF; Miscellaneous Technical
2400; 243F; Control Pictures
2440; 245F; Optical Character Recognition
2460; 24FF; Enclosed Alphanumerics
2500; 257F; Box Drawing
2580; 259F; Block Elements
25A0; 25FF; Geometric Shapes
2600; 26FF; Miscellaneous Symbols
2700; 27BF; Dingbats
2800; 28FF; Braille Patterns
...
Spróbujmy zatem "pasować" tylko litery należące do bloku Latin Extended-A (gdzie znajdują się m.in. polskie znaki).


Wzorzec: "\p{InLatinExtended-A}"
Tekst: "a"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca

Wzorzec: "\p{InLatinExtended-A}"
Tekst: "ą"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "ą" od pozycji 0 do pozycji 1.



Oczywiście, wszystkie predefiniowane klasy (poza klasą wszystkich znaków, określaną przez kropkę)  można włączać do zestawu znaków na liście ujętej w nawiasach kwadratowych. W ten sposób możemy zażyczyć sobie np. by był dopasowany albo znak z bloku Latin Extended-A, albo symbol waluty, albo cyfra szesnastkowa:


Wzorzec: "[\p{InLatinExtended-A}\p{Sc}\p{XDigit}]"

Tekst: "Ć"
matches(): Cały tekst pasuje do wzorca.

Tekst: "9"
matches(): Cały tekst pasuje do wzorca.

Tekst: "$"
matches(): Cały tekst pasuje do wzorca.

Tekst: "k"
matches(): Cały tekst NIE pasuje do wzorca.


Klasy znaków (które przecież traktowane są jak zbiory)  możemy kombinować również w jeszcze bardziej elastyczny sposób. Służą od tego operacje:
Suma klas oznacza połączenie zestawu znaków tych klas. Na przykład:

[a-c[1-3]] - pasuje do znaków a, b, c, 1, 2, 3

Wspólna część klas określa znaki, które występują w obu klasach. Na przykład, wzorzec: [1-9&&3-7] pasuje do cyfr 3,4,5,6,7, które są wspólne dla obu wymienionych zakresów.
Ten sam wzorzec moglibyśmy zapisać i tak [1-9&&[3-7]].

Różnica klas tworzy klasę, która zawiera wszystkie znaki jednej z podanych klas za wyjątkiem znaków drugiej z podanych klas. Aby uzyskać taki efekt trzeba połączyć działanie operatorów && i negacji ^.

Na przykład, wzorzec  [\p{L}&&[^abc]] określa dowolną literę Unicode za wyjątkiem liter a, b i c o czym możemy się łatwo przekonać:

Wzorzec: "[\p{L}&&[^abc]]"

Tekst: "ą"
matches(): Cały tekst pasuje do wzorca.

Tekst: "a"
matches(): Cały tekst NIE pasuje do wzorca.

Tekst: "d"
matches(): Cały tekst pasuje do wzorca.

 
Na koniec bardzo ważna informacja.
Otóż użyte w zestawie znaków klasy (w nawiasach kwadratowych) metaznaki (oprócz odwrotnego ukośnika, symbolu ^ oraz -) tracą swoje specjalne znaczenie i są traktowane literalnie. W szczególności, kropka traktowana jest literalnie (poza nawiasami kwadratowymi oznacza dowolnyc znak). To samo z "zrezerwowanymi" nawiasami okrągłymi i klamrowymi.

Przykład:
Wzorzec: "[({]x[})]"
Tekst: "(x)"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "(x)" od pozycji 0 do pozycji 3.

Tekst: "{x}"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "{x}" od pozycji 0 do pozycji 3.


A jak dopasować znak x okolony dowolnymi nawiasami lub znakami - ? Przecież nawiasy kwadratowe oznaczają klasę, zaś znaki '-' służą do definiowania zakresów.
Z symbolem zakresu (-) nie będzie kłopotu - trzeba go tylko podać w definicji klasy albo na początku, albo na końcu (wredy oczywiście nie może służyć do oznaczania zakresów a machina przetwarzająca wyrażenia regularne jest na tyle inteligentna, że w takim kontekście traktuje go literalnie). Z nawiasami kwadratowymi nie możemy tak postępować, szczególnie w Javie (która - np. w przeciwieństwie do Perla - pozwala na zagnieżdżanie nawiasów kwadratowych). Musimy zatem użyć symbolu "ucieczki" - odwrotnego ukośnika dla wskazania, że nawias kwadratowy chcemy potraktowac literalnie.


Wzorzec: "[\[({-]x[-)}\]]"

Tekst: "-x-"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "-x-" od pozycji 0 do pozycji 3.

Tekst: "[x]"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "[x]" od pozycji 0 do pozycji 3.

Tekst: "(x)"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "(x)" od pozycji 0 do pozycji 3.

Tekst: "{x}"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "{x}" od pozycji 0 do pozycji 3.



2.4. Kwantyfikatory

Wyrażenia regularne byłyby całkiem nieprzydatne, gdyby nie można było za ich pomocą dopasowywać powtarzających się sekwencji znaków. Do specyfikacji powtórzeń służą kwantyfikatory.

Symbole kwantyfikatorów są następujące i oznaczają:

?wystąpienie jeden raz lub wcale
*wystąpienie zero lub więcej razy
+wystąpienie raz lub więcej razy
{n}wystąpienie dokładnie n razy
{n,}wystąpienie co najmniej n razy
{n,m}wystąpienie co najmniej  n ale nie więcej niż m razy

Czego dotyczy słowo "wystąpienie"?

W przypadku gdy kwantyfikator następuje po literale - wymagane jest wystąpienie (liczba wystąpień zależy od kwantyfikatora, w szczególności może być 0) tego literału np. "12a+" oznacza 1, potem 2, następnie wystąpienie znaku 'a' jeden lub więcej razy.
Uwaga: "12a+" nie oznacza wystąpienia ciągu znaków 12a jeden lub więcej razy!

Gdy kwantyfikator występuje po klasie znaków - dotyczy dowolnego znaku z tej klasy. Np. [abc]+ oznacza wystąpienie jeden lub więcej razy znaku a, lub znaku b, lub znaku c. Wzorzec ten pasuje do takich tekstów jak np.
"abc"
"bcaabc"
"aaaaaaaaaa"

Jeśli natomiast chcemy, by kwantyfikator dotyczył dowolnego wyrażenia regularnego X -  to powinniśmy zastosować jedną z poniższych konstrukcji składniowych:

(X)symbol_kwantyfikatora
(?:X)symbol_kwantyfikatora


Konstrukcje takie tworzą tzw. grupy. Grupy ujęte w nawiasy okrągłe (pierwsza z w/w form składniowych) służą też do zapamiętywania tekstu pasującego do wzorca podanego w nawiasach. Druga forma służy wyłącznie grupowaniu, bez zapamiętywania.  Więcej na ten temat powiemy za chwilę. Teraz spójrzmy na kilka przykładów grupowania, służącego przede wszystkim zastosowaniu kwantyfikatorów, ale również po to, by zmieniać porządek interpretacji wyrażenia.

Pierwszy wzorzec opisuje jedno lub wiele wystąpień dowolnych ze słów pies, kot, krowa (nie rozdzielonych spacjami).

Wzorzec: "(pies|kot|krowa)+"

Tekst: "pieskot"
matches(): Cały tekst pasuje do wzorca.

Tekst: "piespiespies"
matches(): Cały tekst pasuje do wzorca.

Tekst: "kotkotkrowa"
matches(): Cały tekst pasuje do wzorca.

Tekst: "krowapiespies"
matches(): Cały tekst pasuje do wzorca.


Bardziej skomplikowany przypadek uzyskamy jeśli zażyczymy sobie, by tekst:
Jednym z możliwych wyrażeń regularnych opisujących tę sytuację jest następujący wzorzec:

(pies|kot|krowa)( {1,3}(pies|kot|krowa))*
Czytamy go tak:
Przetestujmy czy to działa.

Wzorzec: "(pies|kot|krowa)( {1,3}(pies|kot|krowa))*"
Tekst: "pies"
matches(): Cały tekst pasuje do wzorca.

Tekst: "krowa krowa pies pies"
matches(): Cały tekst pasuje do wzorca.

Tekst: "pieskot"
matches(): Cały tekst NIE pasuje do wzorca.
ALE!!!:
find(): Dopasowano podłańcuch "pies" od pozycji 0 do pozycji 4.
find(): Dopasowano podłańcuch "kot" od pozycji 4 do pozycji 7.

Tekst: "pies  pies   pies"
matches(): Cały tekst pasuje do wzorca.

Tekst: "pies     pies pies"
matches(): Cały tekst NIE pasuje do wzorca.
ALE!!!:
find(): Dopasowano podłańcuch "pies" od pozycji 0 do pozycji 4.
find(): Dopasowano podłańcuch "pies pies" od pozycji 9 do pozycji 18.



Zwróćmy najpierw szczególną uwagę na zastosowanie nawiasów okrągłych. Służy ono nie tylko do stosowania kwantyfikatorów, ale również do zmiany kolejności interpretacji wyrażenia regularnego. Gdybyśmy pierwszą aletnatywę pies|kot|krowa nie ujęli w nawiasy, to interpretacja wyrażenia pies|kot|krowa({1,3}(pies|kot|krowa))* byłaby taka: albo pies, albo kot, albo krowa po której następuje 0 lub więcej powtórzeń kombinacji pies, kot, krowa rozdzielonych spacjami (od jednej od trzech). Zatem otrzymalibyśmy inne od oczekiwanych wyniki:

Wzorzec: "pies|kot|krowa( {1,3}(pies|kot|krowa))*"
Tekst: "pies kot"
matches(): Cały tekst NIE pasuje do wzorca.


poprawne tylko wtedy, gdy tekst zawierający więcej niż jeden wyraz  zaczynałby się od słowa "krowa"


Tekst: "krowa pies krowa"
matches(): Cały tekst pasuje do wzorca.


Tak samo potrzebne sa użyte w wyrażeniu ( {1,3}(pies|kot|krowa))* nawiasy wokół alternatywy pies|kot|krowa, bez nich bowiem znaczenie tego wzorca byłoby inne od oczekiwanego, mianowicie znaczyłby on od 1 do 3 spacji przed psem (i sam pies oczywiście), albo sam kot (już bez spacji) albo sama krowa (też bez spacji). 

Warto też zwrócić baczną uwagę na te kwantyfikatory, które dopuszczają brak wystąpień ciągu znaków. Kwantyfikator ? wymaga jednego wystąpienia lub braku wystąpień, kwantyfikator * wymaga zera lub więcej wystąpień. Tutaj trzeba uważać, bowiem matcher znajdzie (może znaleźć) dopasowania i przy braku specyfikowanego ciągu znaków. 

Właśnie dlatego metoda find() przy zastosowaniu wzorca "(pies|kot|krowa)( {1,3}(pies|kot|krowa))*" do tekstu "pieskot" znajdowała dopasowanie (tylko na podstawie
pierwszych nawiasów, część oznaczona kwantyfikatorem-gwiazdką mogła, ale nie musiała wystąpić).

Przy tej okazji uwidacznia się wspomniana wcześniej różnica pomiędzy metodą matches() (szukającej dopasowania do wzorca całego tekstu) i metodą find() - która przeszukuje tekst sekwencyjnie. Zobaczmy to jeszcze raz  na przykładzie tekstu "xxaxxaxx" i wzorca "x*" (0 lub więcej wystąpień znaku 'x').


Wzorzec: "x*"
Tekst: "xxaxxaxx"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "xx" od pozycji 0 do pozycji 2.
find(): Dopasowano podłańcuch "" od pozycji 2 do pozycji 2.
find(): Dopasowano podłańcuch "xx" od pozycji 3 do pozycji 5.
find(): Dopasowano podłańcuch "" od pozycji 5 do pozycji 5.
find(): Dopasowano podłańcuch "xx" od pozycji 6 do pozycji 8.
find(): Dopasowano podłańcuch "" od pozycji 8 do pozycji 8.


Oczywiście, nasz tekst wejściowy nie jest ciągiem 0 lub większej liczby znaków x. Zatem matches() słusznie powiada: "nie ma dopasowania". Ale metoda find() stosuje nasz wzorzec kolejno przeglądając tekst od początku. Znajduje dwa znaki x (od pozycji 0) - co spełnia wymagania wzorca (uzyskujemy dopasowanie), po czym natrafia na znak 'a' na pozycji 2. To nie jest 'x', ale wzorzec dopuszcza brak wystąpienia znaku 'x' - zatem find() ogłasza dopasowanie tekstu o zerowej długości (co oznacza ni mniej ni więcej, tylko to, że tutaj x nie występuje).
Wrócimy jeszcze do tego zagadnienia przy okazji omawiania wyszukiwania.

W polskiej literaturze greedy tłumaczy się zwykle jako zachłanny. Tutaj tego słowa użyto do tłumaczenia terminu possesive, zatem trzeba było zmienić tłumaczenie terminu greedy
Opisywane dotąd kwantyfikatory są tzw. kwantyfikatorami żarłocznymi (greedy).
Matcher - przy ich zastosowaniu - jest "żarłoczny", bowiem najpierw konsumuje cały tekst wejściowy (i stara się go dopasować). Jeśli to się nie uda, następuje cofanie znak po znaku, aż do uzyskania dopasowania lub jego braku.

W Javie dostępne są też dwa inne rodzaje kwantyfikatorów: wstrzemięźliwe lub leniwe (reluctant) i zachłanne (possesive). Kwantyfikatory wstrzemięźliwe - w przeciwieństwie do żarłocznych - rozpoczynają od początku tekstu wejściowego i pobierają znak po znaku szukając dopasowania. Ewentualna próba dopasowanie całego tekstu następuje na samym końcu.
Kwantyfikatory zachłanne (possesive) podobnie jak żarłoczne konsumują cały tekst i sprawdzają jego dopasowanie, jednak przy braku dopasowania - w przeciwieństwie do kwantyfikatorów żarłocznych - nie następuje cofanie znak po znaku.

Każdy z omówionych wcześniej kwantyfikatorów (które generalnie są żarłoczne - "greedy") można uczynić wstrzemiężliwym lub zachłannym..
W tym celu po kwantyfikatorze dodajemy odpowiednio znak ? lub +.

Aby uczynić
żarłoczny (greedy)
kwantyfikator: ?, *, +, (n), (n,), (n,m)
Wstrzemiężliwym
(reluctant)
Zachłannnym
(possesive)
Dodajemy po nim znak
?
+
Czyli kwantyfikatory wyglądają tak:
??, *?, +?, (n)? ...
?+, *+, ++, (n)+ ...


Różnicę w działaniu trzech rodzajów kwantyfikatoró pokazuje przykład.

Wzorzec: ".*ala"
Tekst: "to jest ala i ma psa ala"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "to jest ala i ma psa ala" od pozycji 0 do pozycji 24.

Wzorzec: ".*?ala"
Tekst: "to jest ala i ma psa ala"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "to jest ala" od pozycji 0 do pozycji 11.
find(): Dopasowano podłańcuch " i ma psa ala" od pozycji 11 do pozycji 24.

Wzorzec: ".*+ala"
Tekst: "to jest ala i ma psa ala"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca


Kwantyfikator żarłoczny (pierwszy przypadek) skonsumował cały tekst, po czym matcher wycofywał się, aż znalazł dopasowanie (słowa "ala" na końcu). Na tym matcher zakończył działanie, co widac wyraźnie z wyniku metody find().

Kwantyfikator wstrzemiężliwy (drugi przypadek) zaczął działanie od początku tekstu, dzięki czemu metoda find() miała okazję znaleźć w tekście dwa dopasowania do wzorca.

Kwantyfikator zachłanny (trzeci przypadek) skonsumował cały tekst i nic już nie zostało do dopasowania do słowa "ala" we wzorcu. Ten kwatyfikator nie pozwala matcherowi cofać się, zatem dostaliśmy brak dopasowania. 

To, który z rodzajów kwantyfikatorów wybrać zależy od kontekstu.

Generalnie, żarłoczność nie jest sama w sobie zła (często umożliwia uzyskanie efektów, które nie byłyby możliwe bez tej cechy kwantyfikatorów).
Spójrzmy na przykład: chcemy wyodrębnić z jakiegoś tekstu ostatnie zawarte w nim trzy po sobie następujące cyfry.
Zastosowanie żarłocznego kwantyfikatora pozwoli na uzyskanie tego efektu: ".*(\d\d\d).*".
Dla tekstu "a 123 b 456 cdefgh" zadziała on tak: .* skonsumuje cały tekst, a matcher cofając się napotka ostanie 3 cyfry, po czym następujący .* pominie wszystko do końca. W wyniku grupa w nawiasach okrągłych, będzie miała pożądaną wartość 456


Wzorzec: ".*(\d\d\d).*"
Tekst: "a 123 b 456 cdefgh"
matches(): Cały tekst pasuje do wzorca.
Wartość grupy w nawiasach: "456" 


Gdyby w tym przykładzie pierwszy kwantyfikator był wstrzemiężliwy, uzyskalibyśmy inny efekt:


Wzorzec: ".*?(\d\d\d).*"
Tekst: "a 123 b 456 cdefgh"
matches(): Cały tekst pasuje do wzorca.
Wartość grupy w nawiasach: "123" 


Oczywiście, nie zawsze żarłoczność kwantyfikatorów będzie nam odpowiadać. Wyobraźmy sobie, że chcemy uzyskać ostatnią liczbę całkowitą w tekście, niezaleznie do tego ile ma cyfr.
Powiemy tak: dowolna liczba dowolnych znaków, po niej jedna lub więcej cyfr, po czym dowolna liczba nie-cyfr (również brak znaków).


Wzorzec: ".*(\d+)\D*"
Tekst: "a 123 b 4567 cdefgh"
matches(): Cały tekst pasuje do wzorca.
Wartość grupy w naiwasach: "7"


Kwantyfikator pożarł wszystko, a matcher cofając się znalazł dopasowanie do wzorca (\d+)\D* już przy ostatniej cyfrze (7) i tym się zadowolił.

Natomiast zastosowanie kwantyfikora wstrzemięźliwego pozwoli na wyodrębnienie całej liczby. Zapiszmy wzorzec: ".*?(\d+)\D*", co znowu oznacza: dowolne znaki, jedna lub więcej cyfr, po których następuje dowolna (również zerowa) liczba znaków nie będących cyframi. Tym razem jednak kwantyfikator jest wstrzemiężliwy. Wynik:

Wzorzec: ".*?(\d+)\D*"
Tekst: "a 123 b 4567 cdefgh"
matches(): Cały tekst pasuje do wzorca.
Wartość grupy w nawiasach: "4567"


Można też zauważyć, że w wielu przypadkach wstrzemięźliwe kwantyfikatory działają bardziej efektywnie.
Gdy natomiast nasze wyrażenie jest dobrze skonstruowane i służy do sprawdzenia czy cały tekst pasuje/nie pasuje do wzorca - pochłonięcie tekstu przez kwantyfikator bez cofania się może okazać się bardzo efektywne niż przeglądanie od początku czy też dopuszczenie wycofywania się znak po zbaku. Zastosujemy wtedy kwantyfikator zachłanny, a nie  wstrzemięźliwy czy żarłoczny.

2.5. Granice

Jeżeli jesteśmy zainteresowani dopasowaniem wzorca w pewnym konkretnym miejscu tekstu stosujemy tzw. granice ("boundary matchers") inaczej zwane kotwicami ("anchors"). W szczególności możemy życzyć sobie, by dopasowanie następowało na początku lub końcu wiersza, czy na granicy słów.
Symbole służące oznaczaniu granic są następujące.

 Symbole granic
 ^  Początek linii
 $  Koniec linii
 \b  Na granicy słowa
 \B  Nie na granicy słowa
 \A  Początek wejścia
 \G Koniec poprzedniego dopasowania
 \Z Koniec wejścia (bez terminatora)
 \z Koniec wejścia
Uwaga: metaznak ^ zyskuje tutaj nowe znaczenie (poprzednio stosowaliśmy go do negacji klas).

Rozważmy przykłady.

Pierwszy dotyczy dopasowania linii, które zaczynają się od jednej lub więcej cyfr.


Wzorzec: "^\d+.*"
Tekst: "123"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "123" od pozycji 0 do pozycji 3.

Wzorzec: "^\d+.*"
Tekst: "ala 123"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca


Zwrócmy uwagę na różnicę: wzorzec "\d+.*" (czyli bez zaznaczenia, że cyfry mają wystąpić na początku linii) pozwoli metodzie find() na dopasowanie części napisu "ala 123":

Wzorzec: "\d+.*"
Tekst: "ala 123"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "123" od pozycji 4 do pozycji 7.



Możemy też sprawdzić, czy linia zawiera na samym końcu liczbę całkowitą (i wyodrębnić ją w grupie).

Wzorzec: ".+?(\d+)$"

Tekst: "a 123"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "a 123" od pozycji 0 do pozycji 5.
Wartość grupy w nawiasach: "123"

Tekst: "a 123 a"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca


Zauważmy, że gdyby zrezygnować z zaznaczenia końca wiersza symbolem $, w ostatnim przykładzie "a 123 a" metoda find() znalazła by dopasowanie:


 Wzorzec: ".+?(\d+)"
Tekst: "a 123 a"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "a 123" od pozycji 0 do pozycji 5.


Normalnie, symbole ^ i $ ignorują znaki końca wiersza i dopasowują początek i koniec całej sekwencji wejściowej (linia = String poddany analizie składniowej za pomoca wyrażenia regularnego).  Zatem, poprzednie przykładowe wzorce nie pozwolą metodzie find() znaleźć dopasowań w więcej niż w jednym wierszu. Można to zmienić, stosując flagi - o czym dalej.
 
Spróbujmy teraz wyodrębniać słowa. Jeżeli zapiszemy wzorzec "sto", to odnalezione zostaną wystąpienia tego ciągu znaków, niezależnie od tego, czy jest on słowem, czy też stanowi część jakiegoś słowa.


Wzorzec: "sto"
Tekst: "sto podstołecznych autobusów stoi na postoju numer sto"
find(): Dopasowano podłańcuch "sto" od pozycji 0 do pozycji 3.
find(): Dopasowano podłańcuch "sto" od pozycji 7 do pozycji 10.
find(): Dopasowano podłańcuch "sto" od pozycji 29 do pozycji 32.
find(): Dopasowano podłańcuch "sto" od pozycji 39 do pozycji 42.
find(): Dopasowano podłańcuch "sto" od pozycji 51 do pozycji 54.


Jeśli chcemy znaleźć tylko całe słowa "sto" zastosujemy symbole \b.


Wzorzec: "\bsto\b"
Tekst: "sto podstołecznych autobusów stoi na postoju numer sto"
find(): Dopasowano podłańcuch "sto" od pozycji 0 do pozycji 3.
find(): Dopasowano podłańcuch "sto" od pozycji 51 do pozycji 54.


Gdy chcemy znaleźć ciąg "sto", rozpoczynający słowo (przy czym mogący być, ale niekoniecznie będący słowem) napiszemy:

Wzorzec: "\bsto"
Tekst: "sto podstołecznych autobusów stoi na postoju numer sto"
find(): Dopasowano podłańcuch "sto" od pozycji 0 do pozycji 3.
find(): Dopasowano podłańcuch "sto" od pozycji 29 do pozycji 32.
find(): Dopasowano podłańcuch "sto" od pozycji 51 do pozycji 54.


Z kolei, jeśli chcemy znaleźć "sto" jako początki słów (ale nie słowa) możemy napisać:

Wzorzec: "\bsto\B"
Tekst: "sto podstołecznych autobusów stoi na postoju numer sto"
find(): Dopasowano podłańcuch "sto" od pozycji 29 do pozycji 32.


I wreszcie, ciąg "sto" w środku słów:

Wzorzec: "\Bsto\B"
Tekst: "sto podstołecznych autobusów stoi pusto na postoju numer sto"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "sto" od pozycji 7 do pozycji 10.
find(): Dopasowano podłańcuch "sto" od pozycji 45 do pozycji 48.


i na końcu słów (ale nie będący słowem):


Wzorzec: "\Bsto\b"
Tekst: "sto podstołecznych autobusów stoi pusto na postoju numer sto"
find(): Dopasowano podłańcuch "sto" od pozycji 36 do pozycji 39.



Zastosowanie symbolu \G ("koniec ostatniego dopasowania") ilustrują przykłady:

Wzorzec: "ala"
Tekst: "ala ala ala"
find(): Dopasowano podłańcuch "ala" od pozycji 0 do pozycji 3.
find(): Dopasowano podłańcuch "ala" od pozycji 4 do pozycji 7.
find(): Dopasowano podłańcuch "ala" od pozycji 8 do pozycji 11.

Wzorzec: "\Gala"
Tekst: "ala ala ala"
find(): Dopasowano podłańcuch "ala" od pozycji 0 do pozycji 3.

Wzorzec: "\Gala"
Tekst: "alaalaala"
find(): Dopasowano podłańcuch "ala" od pozycji 0 do pozycji 3.
find(): Dopasowano podłańcuch "ala" od pozycji 3 do pozycji 6.
find(): Dopasowano podłańcuch "ala" od pozycji 6 do pozycji 9.



Pierwszy przypadek jest naturalny: znajdowane jest każde wystąpienie ciągu "ala".
W przypadku drugim, żądamy by kolejne wystąpienie zaczynało się na końcu poprzedniego dopasowania. Dla tekstu "ala ala ala", find() nie dopasowuje koejnych wyrazów, bowiem, nie zaczynają się one na końcu poprzedniego dopasowania (spacja nie pasuje do wzorca). Trzeci przypadek - z usuniętymi spacjami - daje dopasowania "na końcach popzrednich dopasowań", zatem znowu odnajdywane są trzy wystąpienia ciągu "ala".


2.6. Flagi

Sposób interpretacji wyrażenia regularnego można modyfikować za pomocą tzw. flag. Flagi podajemy: 
Mamy do dyspozycji następujace flagi.

 Stała statyczna klasy
Pattern
Odpowiednik
w wyrażeniu regularnym
Znaczenie
 Pattern.CANON_EQ
 brak
Pozwala  na  pełne , kanoniczne porównywanie znaków i kodów znaków   (zmniejsza efektywność)
 Pattern.CASE_INSENSITIVE
 (?i)
Porównania liter bez uwzględnienia ich wielkości
 Pattern.COMMENTS
 (?x)
Pozwala na wstawianie komentarzy w wyrażeniu
 Pattern.MULTILINE
 (?m)
Pozwala na dopasowanie ^ i $ na początku i na końcu wierszy (separowanych znakiem końca wiersza)
 Pattern.DOTALL
 (?s)
Pozwala na dopasowanie metaznaku . (kropka) również do znaku końca wiersza.
 Pattern.UNICODE_CASE
 (?u)
Przy ignorowaniu wielkości liter w porównaniach uwzględnia znaki Unicode
 Pattern.UNIX_LINES
 (?d)
Uniksowe separatory wierszy.

Domyślnie działające opcje (bez użycia flag) są przeciwstawne znaczeniom podanym w tabeli (np. kropka nie dopasowuje znaków końca wiersza, porównania uwzględniają wielkość liter itp.).

Przy kompilacji wyrażenia podajemy flagi jako bitową sumę stałych o wyżej wymienionych nazwach np.
String regex = "....";
int flags = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
Pattern pattern = Pattern.compile(regex, flags);
Działają one wtedy dla całego wyrażenia.

Alternatywnie, możemy podać flagi bezpośrednio w wyrażeniu regularnym - wtedy (w przeciwieństwie do Perla) będą one działać od momentu wystąpienia w wyrażeniu regularnym.

Przykład.
Wyszukiwanie słowa "Ala" (z uwzględnieniem wielkości liter),  po którym wystepuje napis "ma kota", który może być pisany literami małymi, dużymi, lub mieszanymi.



Wzorzec: "Ala (?i)ma kota"

Tekst: "Ala ma kota"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "Ala ma kota" od pozycji 0 do pozycji 11.

Tekst: "Ala MA KOTA"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "Ala MA KOTA" od pozycji 0 do pozycji 11.

Tekst: "ALA MA KOTA"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca


Pamiętajmy o tym, że jeśli obojętność na wielkość liter chcemy rozciągnąć na inne niż angielski języki, oprócz flagi CASE_INSENSITIVE (?i) musimy podać flagę UNICODE_CASE (?u), co obrazuje poniższy przykład.


Wzorzec: "Ala (?i)ma kota pączka"
Tekst: "Ala ma kota Pączka"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "Ala ma kota Pączka" od pozycji 0 do pozycji 18.

Wzorzec: "Ala (?i)ma kota pączka"
Tekst: "Ala ma kota PĄCZKA"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Nie znaleziono żadnego podłańcucha pasującego do wzorca

Wzorzec: "Ala (?i)(?u)ma kota pączka"
Tekst: "Ala ma kota PĄCZKA"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "Ala ma kota PĄCZKA" od pozycji 0 do pozycji 18.


W pierwszym przypadku nie było kłopotu z samą flagą (?i), bo zmieniliśmy wielkość litery 'p' (należącej do alfabetu angielskiego). Gdy jednak - w drugim przypadku - podaliśmy duże Ą, matcher nie znalazł dopasowania. Dopiero dodatkowa specyfikacja flagi (?u) dała prawidłowy wynik.

Przy przetwarzaniu tekstów, składającyh się z wielu wierszy istotne jest czy metaznak . (kropka) będzie dopasowywał znaki końca wiersza czy też nie. Domyślny brak dopasowania można zmienić używając flagi DOTALL lub (?s) w wyrażeniu regularnym.

Poniższy przykład pokazuje jak różnie działa matcher przy fladze DOTALL wyłączonej (pierwszy przypadek) i włączonej (drugi). Tekst zawiera trzy wiersze z cyframi 1,2, 3 na początku każdego z nich odpowiednio.

Wzorzec: "\d.*"
Tekst: "1 aaaa
2 aaaa
3 aaaa"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "1 aaaa" od pozycji 0 do pozycji 6.
find(): Dopasowano podłańcuch "2 aaaa" od pozycji 7 do pozycji 13.
find(): Dopasowano podłańcuch "3 aaaa" od pozycji 14 do pozycji 20.

Wzorzec: "(?s)\d.*"
Tekst: "1 aaaa
2 aaaa
3 aaaa"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "1 aaaa
2 aaaa
3 aaaa" od pozycji 0 do pozycji 20.


Również znaki początku (^) i końca ($) wiersza domyślnie nie uwzględniają rozbicia tekstu na wiersze znakami końca wiersza (praktycznie oznaczają początek i koniec tekstu).
Ilustrują to poniższe przykłady tekstu składającego się z dwóch wierszy i stosujących wzorce najpiewr dopasowania "na początku" (^), a później "na końcu" ($)


Wzorzec: "^\d+ a+"
Tekst: "123 aaa
456 aaaaa"
find(): Dopasowano podłańcuch "123 aaa" od pozycji 0 do pozycji 7.

Wzorzec: "\d+ a+$"
Tekst: "123 aaa
456 aaaaa"
find(): Dopasowano podłańcuch "456 aaaaa" od pozycji 8 do pozycji 17.


Dopiero włączenie flagi MULTILINE czyli (?m) w wyrażeniu regularnym zmienia tę sytuację:


Wzorzec: "(?m)^\d+ a+"
Tekst: "123 aaa
456 aaaaa"
find(): Dopasowano podłańcuch "123 aaa" od pozycji 0 do pozycji 7.
find(): Dopasowano podłańcuch "456 aaaaa" od pozycji 8 do pozycji 17.

Wzorzec: "(?m)\d+ a+$"
Tekst: "123 aaa
456 aaaaa"
find(): Dopasowano podłańcuch "123 aaa" od pozycji 0 do pozycji 7.
find(): Dopasowano podłańcuch "456 aaaaa" od pozycji 8 do pozycji 17.




2.7. Grupy i odniesienia zwrotne

Jak już wiemy, użycie nawiasów okrągłych '('  i  ')'  pozwala zapamiętywać części tekstu pasujące do wzorca podanego w nawiasach. Do takich grup możemy się następnie odwoływac z poziomu języka oraz w samym wyrażeniu regularnym.

Grupy, które wprowadziliśmy w wyrażeniu regularnym nawiasami (), są kolejno numerowane poczynając od 1. Mówiąc ściślej są numerowane poprzez zliczanie otwartych i zamkniętych nawiasów (bowiem grupy mogą być zagnieżdżone).

Np. w wyrażeniu (A) (B) (C) mamy trzy grupy o numerach:
1 - grupa odpowiadająca wyrażeniu A
2 - grupa odpowiadająca wyrażeniu B
3 - grupa odpowiadająca wyrażeniu C

a w wyrażeniu ((A)((B)(C)))(D) mamy 6 grup o numerach
1 - grupa odpowiadająca wyrażeniu (A)((B)(C)
2 - grupa odpowiadająca wyrażeniu A
3 - grupa odpowiadająca wyrażeniu  (B)(C)
4 - grupa odpowiadająca wyrażeniu  B
5 - grupa odpowiadająca wyrażeniu C
6 - grupa odpowiadająca wyrażeniu  D

Liczbę grup możemy uzyskać używając metody groupCount() wobec matchera (obiektu klasy Matcher). Zawartość każdej z grup (tekst pasujący do wyrażenia podanego w jej nawiasach) - uzyskujemy za pomocą odwołania group(numer_grupy).
Tekst pasujący do całego wyrażenia dostępny jest poprzez odwołanie group() lub group(0).

Uwaga: maksymalnie dopuszczalna liczba grup wynosi 9.

Rozważmy przykład. Z wprowadzanych (np. w dialogach wejściowych) wierszy tekstu, które zawierają na początku jeden lub więcej znaków słowa (w alfabecie angielskim), następnie 1 lub więcej "białych znaków", po czym dowolną liczbę całkowitą, chcemy wyodrębnić początkowe słowo i podaną liczbę.
W tym celu zbudujemy wyrażenie regularne, które grupuje znaki słowa i cyfry:
(\w+)\s+(\d+)
W programie, który ma znajdować dopasowania pokażemy (tradycyjnie już) działanie metod matches() oraz find(), a także sposób odwołań do zawartości grup i raportowanie pozycji tekstu  na których wystąpiło dopasowanie
Zwróćmy uwagę: pozycje dopasowań oraz zawartość grup są stanami matchera, niezależnymi od tego czy używamy metody matches() czy find(), zatem uzyskiwanie informacji o grupach i pozycjach dopasowania wyodrębnimy w metodzie report(...), która jest wołana zarówno po matches() jak i find().
I istotna uwaga: przy braku dopasowania (albo gdy jeszcze nie próbowano dopasowywać tekstu) odwołania do metod matchera, zwracających jego stany (pozycje, grupy) powodują powstanie wyjątku IllegalStateException. Wyjątek ten będziemy obsługiwać, wyprowadzając komunikat o błędzie.
import java.util.regex.*;
import javax.swing.*;

public class Grupy {

  public static void main(String[] args) {

    // Kompilacja wzorca
    // uwaga: odwrotne ukośniki w tekście programu duplikujemy

    Pattern pattern = Pattern.compile("(\\w+)\\s+(\\d+)");

    String txt;  // przeszukiwany (dopasowywany) tekst
                 // pobierany w dialogach wejściowych, w pętli

    while((txt = JOptionPane.showInputDialog("Tekst")) != null) {

      Matcher matcher = pattern.matcher(txt); // uzyskanie matchera

      // Próba dopasowania całego tekstu i pokazanie wyniku
      boolean res = matcher.matches();
      report(txt, "matches()", res, matcher);

      // odtworzenie pierwotnego stanu matchera (po matches)
      matcher.reset();

      // Sekwencyjne wyszukiwanie za pomocą metody find
      while ((res = matcher.find()) != false) {
        report(txt, "find()", res, matcher);
      }
    }
    System.exit(0);
  }

  static void report(String txt, String who, boolean result, Matcher matcher) {

     System.out.println("Tekst: \"" + txt + "\"");

     int lgrup = matcher.groupCount(); // liczba grup
     System.out.println("Liczba grup w wyrażeniu: " + lgrup);

     System.out.println(who + ": znalazł dopasowanie ? - " + result);

     try {

       // Cały dopasowany/wyszukany fragment
       String whatMatches = "\"" + matcher.group() + "\"";

       // Pozycja tekstu na której zaczyna się dopasowanie
       int pos = matcher.start();
       System.out.println("Dopasowano co: " + whatMatches);
       System.out.println("Na pozycji   : " + pos);

       // Wyprowadzamy zwartość wszystkich grup
       for (int i=1; i<=lgrup; i++) {
         System.out.println("Grupa " + i + ": \"" + matcher.group(i) + "\"");
       }

     } catch (IllegalStateException exc) {  // wyjątek - gdy brak dopasowania
         System.out.println(exc.getMessage());
         return;
     }
  }

}
Program po wprowadzeniu w dialogach dwóch tekstów:
Java 1234
Bali 3456 Kawa 245

wyprowadzi następujące wyniki:

Tekst: "Java 1234"
Liczba grup w wyrażeniu: 2
matches(): znalazł dopasowanie ? - true
Dopasowano co: "Java 1234"
Na pozycji   : 0
Grupa 1: "Java"
Grupa 2: "1234"
Tekst: "Java 1234"
Liczba grup w wyrażeniu: 2
find(): znalazł dopasowanie ? - true
Dopasowano co: "Java 1234"
Na pozycji   : 0
Grupa 1: "Java"
Grupa 2: "1234"
Tekst: "Bali 3456 Kawa 245"
Liczba grup w wyrażeniu: 2
matches(): znalazł dopasowanie ? - false
No match found
Tekst: "Bali 3456 Kawa 245"
Liczba grup w wyrażeniu: 2
find(): znalazł dopasowanie ? - true
Dopasowano co: "Bali 3456"
Na pozycji   : 0
Grupa 1: "Bali"
Grupa 2: "3456"
Tekst: "Bali 3456 Kawa 245"
Liczba grup w wyrażeniu: 2
find(): znalazł dopasowanie ? - true
Dopasowano co: "Kawa 245"
Na pozycji   : 10
Grupa 1: "Kawa"
Grupa 2: "245"


I znowu widzimy, że find() działa sekwencyjnie, przeszukując tekst wedle wzorca, dzięki czemu w tekście "Bali 3456 Kawa 245" znaleziono dwa dopasawania (i wyróżniono po dwa słowa i liczby), gdy matches() stwierdził brak dopasowania całego tekstu do wzorca.

Odniesienie do zawartości grup mogą wystąpić w samym wyrażeniu regularnym. Wprowadzamy je za pomoca odwrotnego ukośnika z następującym numerem grupy (od 1 do 9). Taka konstrukcja oznacza, że w tym miejscu wzorca powinien wystąpić dokładnie taki sam ciąg znaków, jaki został zapamiętany w grupie o podanym numerze. Nazywa się to odniesieniem zwrotnym (backreference).

Korzystając z tego możemy np. zidentyfikować wszystkie podwójne, takie same po sobie następujące słowa (zapewne błędy) w tekście.
Posłużymy się w tym celu wzorcem:
(\b\p{L}+)[\s\p{Punct}]+\1
 który oznacza ciąg dowolnych liter Unicode rozpoczynający się na granicy słowa (słowo), jedną lub więcej spacji lub znaków punktuacji, a następnie powtórzenie tego samego słowa. Zuważmy: ciąg liter od początku słowa \b\p{L}+ ujęliśmy w nawiasy okrągłe, tworząc grupę. Jest to grupa o numerze 1. W naszym wzorcu możemy się odwołać do jej zawartości poprzez \1 i dzięki temu wykryć powtórzenia słów.

Wykorzystajmy ten wzorzec w przykładowym programie, który czyta plik tekstowy i raportuje powtarzające się słowa oraz numery wierszy, w których występują. Dodatkow skompilujemy wzorzec z flagą CASE_INSENSITIVE, by zapewnić wykrywanie duplikatów niezaleznie od wielkości liter.
import java.util.regex.*;
import java.io.*;

public class DupWords {

  public static void main(String[] args) {

    Pattern pattern = Pattern.compile("(\\b\\p{L}+)[\\s\\p{Punct}]+\\1",
                                       Pattern.CASE_INSENSITIVE);
    Matcher matcher = pattern.matcher("");

    try {
      LineNumberReader lr = new LineNumberReader(
                                 new FileReader(args[0])
                              );
      String line;
      while ((line = lr.readLine()) != null) {
        int  lineNr = lr.getLineNumber();

        matcher.reset(line);  // matcher nastawiamy na nowy tekst

        while (matcher.find()) {
          System.out.println("Powtórzenie słowa " + matcher.group(1) +
                             " w wierszu " + lineNr);
        }

      }
      lr.close();
    } catch(IOException exc) {
        System.out.println(exc);
        System.exit(1);
    }
  }

}
W tym programie warto zwrócić uwagę na wykorzystanie metody reset(...) matchera, ustawiającego nowy tekst do przeszukiwania (podany jako argument). W ten sposób unikamy tworzenia obiektów klasy Matcher przy każdej przeczytanej linii pliku: jeden obiekt-matcher, utworzony najpierw jako skojarzony z pustym tekstem jest ponownie wykorzystywany dla kolejnych wierszy.

Przetwarzanie przykładowego pliku:
Początek początek tekstu jest nie nie zbyt ciekawy.
Potem nie jest jest lepiej.
I i co co? Takie takie słowa.
Na przykład, ala ala, Czy też też ala ala, ala, ala.
I bez powtórzeń.
I z z powtórzeniami.
Niestety to też będzie powtórzenie: jejku, jejku!
I takie też: Co? Co?!
A na razie na granicy wierszy
wierszy nie wykrywamy!


da w wyniku:

Powtórzenie słowa Początek w wierszu 1
Powtórzenie słowa nie w wierszu 1
Powtórzenie słowa jest w wierszu 2
Powtórzenie słowa I w wierszu 3
Powtórzenie słowa co w wierszu 3
Powtórzenie słowa Takie w wierszu 3
Powtórzenie słowa ala w wierszu 4
Powtórzenie słowa też w wierszu 4
Powtórzenie słowa ala w wierszu 4
Powtórzenie słowa ala w wierszu 4
Powtórzenie słowa z w wierszu 6
Powtórzenie słowa jejku w wierszu 7
Powtórzenie słowa Co w wierszu 8


Oczywiście,  program nie jest bardzo uniwersalny. Nie wykrywamy powtórzeń na granicach wierszy (łatwo to zrobić bez raportowania numerów wierszy, trudniej gdy chcemy te numery właściwie podawać). Również, przyjęty "algorytm" decydowania co jest a co nie jest powtórzeniem słowa jest raczej bardzo ad hoc i w praktycznych zastosowaniach wymagałby gruntownego przemyślenia. Jednak ten program, jak również poprzedni przykładowy kod przybliżają nas mocno do zagadnień związanychy  z zastosowaniem róznych metod klas Pattern i Matcher do wyszukiwania i dopasowywania tekstów.
Zanim jednak zajmiemy się tym w sposób nieco bardziej usystematyzowany - ostatnia ważna uwaga, dotycząca grup.

Otóż, jak pamiętamy, nawiasy okrągłe służą nie tylko grupowaniu w celu zapamiętywania zawartości grup, ale również po to by właściwie ustalać sposób interpretacji wyrażenia (np. powtórzenia jakiegoś wzorca albo zmianę kolejności opracowania wyrażenia gdy stosujemy operator alternatywy). Byłoby wielce niewygodne, gdyby wszystkie takie zastosowania wiązały się z zapamiętywaniem grup - musielibyśmy wtedy bacznie śledzić numery tych grup, których zawartość nas interesuje, a byłyby one zapewne nieciągłe.
Dlatego mamy również do dyspozycji tzw. grupy niezapamiętywane (non-capturing groups). Wprowadzamy je za pomocą konstrukcji:

    (?:wyrażenie)

Takie grupy nie są zapamiętywane i nie są numerowane. Możemy więc oddzielić to co chcemy zapamiętać, od tego co chcemy tylko odpowiednio porządkowac.
Np. jeśli chcemy z tekstów w postaci:
wyłuskać pierwszą liczbę i ostatnią cyfrę, to możemy zbudowac następujące wyrażenie:
 (?:[ABC]x)+(\d+)(?:[\s\p{Punct}-]x)+(\d)
zapewniając, że interesująca nas zawartość będzie dostępna w grupach o numerach 1 i 2 (i żadna inna grupa nie będzie zapamiętana, choć musimy zastosować grupowanie dla dopasowania części tekstu nie będących cyframi).
Przykładowy wynik działania tego wyrażenia:

Wzorzec: "(?:[ABC]x)+(\d+)(?:[\s\p{Punct}-]x)+(\d)"
Tekst: "AxBxAx321-x-x-x:x:x:x7"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "AxBxAx321-x-x-x:x:x:x7" od pozycji 0 do pozycji 22.
Grupy:
1 "321"
2 "7"




2.8. Dopasowanie i wyszukiwanie.

Wielokrotnie już widzieliśmy działanie metod matches() i find() klasy Matcher. Oprócz tego dostępna jest metoda find z argumentem typu int, mamy też do dyspozycji jeszcze jedną, dotąd nie omawianą metodę wyszukiwania: lookingAt(). Podsumujmy różnice pomiędzy tymi metodami.

booleanfind()
         Przeszukuje łańcuch wejściowy poczynając od początku (lub poprzedniego dopasowania) w poszukiwaniu podłańcucha pasującego do wzorca. Jeśli wzorzec został dopasowany (wynik true), kolejne wywołanie tej metody przeszukuje łańcuch wejściowy poczynając od znaku po ostatnim dopasowanym znaku. Jeśli wzorzec nie został znaleziony  metoda zwraca wartość false.
 booleanfind(int start)
          Resetuje matcher i przeszukuje łańcuch wejściowy poczynając od podanej pozycji start w poszukiwaniu podłańcucha pasującego do wzorca. Zwraca true przy znalezieniu dopasowania. Kolejne find() będzie szukać podłańcucha poczynając od znaku po ostatnio dopasowanym znaku, a find(int start) - od podanej pozycji.
 booleanmatches()
          Sprawdza dopasowanie całego łańcucha wejściowego do wzorca. Zwraca true, jeśli takie dopasowanie występuje i false w przeciwnym razie.
 booleanlookingAt()
          Próbuje zmaleźć dopasowanie do wzorca dowolnego pdołańcucha zaczynającego się od początku łańcucha wejściowego. Zwraca true, jeśli takie dopasowanie występuje i false w przeciwnym razie. W przeciwieństwie do metody matches() może być dopasowana tylko część łańcucha wejściowego.


Poniższy przykład pokazuje różnice pomiędzy metodami matches(), lookingAt() i find().

Wzorzec: "ala"
Tekst: "ala"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "ala" od pozycji 0 do pozycji 3.
lookingAt(): Dopasowano podłańcuch "ala" Od początku do pozycji: 3.

Wzorzec: "ala"
Tekst: "ala ala"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "ala" od pozycji 0 do pozycji 3.
find(): Dopasowano podłańcuch "ala" od pozycji 4 do pozycji 7.
lookingAt(): Dopasowano podłańcuch "ala" Od początku do pozycji: 3.

Wzorzec: "ala"
Tekst: "bela ala ala"
matches(): Cały tekst NIE pasuje do wzorca.
find(): Dopasowano podłańcuch "ala" od pozycji 5 do pozycji 8.
find(): Dopasowano podłańcuch "ala" od pozycji 9 do pozycji 12.
lookingAt(): Nie znaleziono początkowego podłańcucha pasującego do wzorca


W pierwszym przypadku (tekst "ala") cały tekst pasuje do wzorca i nie występują żadne różnice w działaniu metod. W drugim przypadku tekst "ala ala" nie pasuje (w całości) do wzorca, ale metoda find(), sekwencyjnie przeszukując łańcuch wejściowy, wyróżnia w nim dwa pasujące podłańcuchy, natomiast metoda lookingAt() znajduje pasujący podłańcuch początkowy.
W trzecim przypadku  (tekst "bela ala ala") tylko metoda find() znajdzie dopasowania (kolejne pasujące podłańcuchy). Metoda lookingAt() zawsze zaczyna od początku i nie znajduje - na początku tekstu - pasującego podłańcucha.

Zwróćmy jednak uwagę, że jeśli wyrażenie regularne kończone jest kwantyfikatorem, który nie ogranicza (z góry) liczby wystąpień znaków, to działanie metod find(...), matches() i lookingAt() jest identyczne.
Np.

Wzorzec: ".*\d+"
Tekst: "    123"
matches(): Cały tekst pasuje do wzorca.
find(): Dopasowano podłańcuch "    123" od pozycji 0 do pozycji 7.
lookingAt(): Dopasowano podłańcuch "    123" Od początku do pozycji: 7.


Każde wywołanie metod matches(), find() i lookingAt() ustala nowy stan obiektu-matchera.
Stan ten opisywany jest przez następujące charakterystyki, które - jeśli tylko dopasowanie zakończyło się sukcesem - możemy odczytać za pomocą podanych niżej metod.

Charakterystyka stanu matchera
Metoda odczytu
Początek dopasowania (pozycja, indeks)
int start()
Uwaga: dla matches() i lookingAt() zawsze 0
Koniec dopasowania
Uwaga: jest to pozycja pierwszego znaku po dopasowanym tekście
int end()
Dopasowany łańcuch/podłańcuch
String group() lub
String group(0)
Zawartość n-ej grupy.
Uwaga: może się zdarzyć przy dopasowaniu, że specyfikacja
 grupy nie pasuje - wtedy group(n) zwraca null. Niektóre konstrukcje składniowe dopasowują puste łańcuchy - wtedy zwracany jest pusty łańcuch znakowy.
String group(int n)
Pozycja początku n-ej grupy
(Pozycja pierwszego znaku dopasowanej n-ej grupy lub -1, jeśli całość pasuje, a grupa nie).
int start(int grupa)    
Pozycja końca n-ej grupy
Uwaga: jest to pozycja ostatniego dopasowanego znaku dla grupy + 1
int end(int grupa)
Jeżeli brak dopasowania lub dla danego matchera jeszcze nie podjęto próby dopasowania (nie wywołano żadnej z metod find(), matches(), lookingAt()), to wywołanie dowolnej z powyższych metod spowoduje wyjątek IllegalStateException.

Uwaga: liczba grup w wyrażeniu, zwracana przez metodę groupCount() jest niezalezna od wyników dopasowania i tekstu wejściowego i jest wyłącznie charakterystyką samego wyrażenia regularnego.
 
Należy zwrócić szczególną uwagę na to, że metody end(..) zwracają indeks końca dopasowania, zwiększony o 1. Długość dopasowanego podłańcucha jest zatem naturalnie określana jako end()-start(). Jak już wspomniano, niektóre konstrukcje składniowe (np. x* - zero lub więcej wystąpień znaku x) mogą dopasowywać dowolne teksty np. brak litery x. Mamy wtedy do czynienia z dopasowaniami zerowej długości i indeks początku i końca dopasowania jest taki sam (np. dla pustego tekstu będzie 0 i 0).

Pamiętajmy też o tym, że w przeciwieństwie do matches(), lookingAt() oraz find(int) bezargumentowa metoda find() rozpoczyna poszukiwanie, korzystając z zastanego stanu matchera. W szczególności, pozycja od której metoda find() będzie szukać dopasowania jest równa indeksowi ostatniego dopasowania + 1 (jest to wartość zwracana przez end() dla ostatniego dopasowania). Jest to oczywiście szczególnie wygodne w sekwencyjnym przeszukiwaniu tekstu wejściowego za pomocą metody find(), według następującego schematu:


Schemat odnajdywania wszystkich wystąpień wzorca regex w tekście wejściowym txt

Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(txt);
while (matcher.find()) {
    System.out.println("Znaleziono tekst: " + matcher.group() +
                                " zaczynający się na pozycji " + matcher.start() +
                                " kończący się na pozycji " + (matcher.end() -1) );
}


Ale, kiedy - po jakiejś próbie dopasowania - metodę find() chcemy zastosować znowu od początku tekstu wejściowego, to należy "zresetować" matcher, tak by jego poprzedni stan nie miał wpływu na nowe wyszukiwanie,
Służy temu metoda reset(). Jej wersja z argumentem klasy CharSequence (który to interfejs - przypomnijmy - jest implementowany w klasach String, StringBuffer oraz CharBuffer z pakietu NIO) pozwala jednocześnie ustalić nowy tekst do przeszukiwania.

Rozważmy przykład. W pliku tekstowym zapisane są liczby całkowite. Niektóre z nich występują "samotnie" w wierszach tego pliku (być może okolone spacjami). Inne mogą występować w wierszu  wraz z innymi liczbami lub słowami. Chcemy odrębnie zsumować liczby samotne i "niesamotne" i pokazać obie sumy.
Pierwsze przychodzące na myśl wyrażenie regularne, które możemy tu zastosować to dowolny ciąg cyfr z poprzedzającym i/lub następującym ciągiem zera lub więcej "białych znaków".
\s*\d+\s*
Oczywiście, chcemy wyodrębniać liczby całkowite, wobec tego uczynimy z nich grupę.
\s*(\d+)\s*
Metoda matches() zastosowana do tego wzorca dopasuje wszystkie samotnie występuje liczby. Metoda find() będzie znajdowac dopasowania sekwencyjnie, zatem - przy braku dopasowania przez matches() uzyskamy od niej liczby występujące w wierszach w ramach tekstu (kilka liczb, liczby z tekstem itp.).

W programie zastosujemy też metodę reset(..). Po pierwsze po to, by nie tworzyć przy każdym wierszu nowego obiektu-matchera (gdy zajrzymy do źródeł klas Pattern i Matcher, okaże się, że każde odwołanie pattern.matcher() zwraca nowy obiekt). Po drugie, najpierw będziemy sprawdzać całkowite dopasowanie wzorca (a więc wyłuskiwac samotne liczby), jeśli się to nie powiedzie - w wierszu może być więcej liczb lub liczba z tekstem -  będziemy chcieli użyć metody find(), a ta korzysta z bieżącego stanu matchera, który może być zaburzony. Użyjemy więc reset() bez argumentów.

Oto program.
import java.util.regex.*;
import java.io.*;

public class Sumowanie {

  public static void sumAndReport(String fileName) {

    try {
      BufferedReader in = new BufferedReader(
                           new FileReader(fileName)
                       );

      Pattern pattern = Pattern.compile("\\s*(\\d+)\\s*");


      Matcher matcher = pattern.matcher("");

      int suma1 = 0,   // suma liczb występujących samotnie w wierszach
          suma2 = 0;   // suma liczb, które nie są w wierszach samotne

      String input;
      while ((input = in.readLine()) != null) {

        matcher.reset(input);          // nowy tekst wejściowy

        if (matcher.matches())         // czy mamy samotną liczbę?
          suma1 += Integer.parseInt(matcher.group(1));

        else {                         // nie, może jest kilka, albo w tekście?

          matcher.reset();             // konieczne, by ew. poprzedni stan
                                       // nie miał wpływu na find()
          while (matcher.find())
            suma2 += Integer.parseInt(matcher.group(1));
        }
      }
      in.close();
      System.out.println("Suma samotnych liczb: " + suma1);
      System.out.println("Suma niesamotnych liczb: " + suma2);
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }

  }

  public static void main(String[] args) {
    sumAndReport(args[0]);
  }

}
Przykładowe wyniki dzialania programu dla pliku wejściowego:
1
2 ala 4
   5
10 20 30


Suma samotnych liczb: 6
Suma niesamotnych liczb: 66


wydają się zadowalające.
Ale ten wzorzec i ten program pozwalają na kilka ciekawych obserwacji.
Obie związane są z zastosowanymi kwantyfikatorami.
Pierwsza dotyczy efektywności: we wzorcu zastosowaliśmy kwantyfikatory żarłoczne (greedy) , a jak pamiętamy powodują one pochłonięcie całego wejścia i dopasowanie przy cofaniu się znak po znaku. Przy dużych plikach lub dużej liczbie przetwarzanych plików zaczyna to odgrywać istotną rolę. Porównajmy czas działania tego samego programu dla większego pliku raz przy zastosowaniu kwantyfikatorów "greedy" (dotychczasowy wzorzec), a drugi raz przy kwantyfikatorach wstrzemięźliwych (reluctant), wprowadzonych w nowym wzorcu:
 
\s*?(\d+?)\s*?
Oto wynik porównania:

Suma samotnych liczb: 6840
Suma niesamotnych liczb: 1713600
Czas greedy: 1430
Suma samotnych liczb: 6840
Suma niesamotnych liczb: 1713600
Czas reluctant: 990


Zatem kwantyfikatory "reluctant" działają w tym przypadku o ok. 40% szybciej od kwantyfikatorów "greedy". To, oczywiście, zależy od wzorca i dopasowywanego tekstu. Czasem "greedy" mogą być lepszym rozwiązaniem, a nawet - w sensie koncepcyjnym - niezbędnym. Warto jednak mieć na uwadzę różnice w działaniu obu typów kwantyfikatorów (o których zresztą już była mowa w podrozdziale o kwantyfikatorach i do których jeszcze wrócimy pod sam koniec tego podrozdziału).

Inny problem związany jest z użyciem (bardzo wygodnego, ale dość niebezpiecznego) kwantyfikatora, dopuszczającego brak wystąpień podawanych znaków (w naszym przypadku 0 lub więcej spacji).

O ile w tym przykładzie na działanie metody matches() (znajdującej "samotne" liczby) nie ma to wpływu, to metoda find() - jak już widzieliśmy - z przyjemnością dopasuje do wzorca \s* każdy inny znak oprócz spacji. W rezultacie będziemy (jako niesamotne) sumować również liczby, które są częścią słów np. Nr1 (bez spacji). Być może wcale tego  nie chcemy i chodzi nam tylko a takie liczby, które nie są częścią słów w tekście.

Prosty test pokaże, że nasz dotychczasowy wzorzec jest w tym przypadku całkiem niezadawalający.
Dla następującego pliku wejściowego:
1
   2  
aa 5 b  5 5 5 c
100b
3
x3000
y40000y 11
Dostaniemy jako wynik:

Suma samotnych liczb: 6
Suma niesamotnych liczb: 43131


co oczywiście nie odpowiada naszym wymaganiom (zsumowane zostały liczby stanowiące cześć słów).

Niewątpliwie, dla kogoś kto zaczyna przygodę z wyrażeniami regularnymi dobór odpowiedniego wzorca nie jest łatwy. Dla przykładu, pokażę najpierw niewłaściwie rozumowanie (przy okazji ilustrując ciekawy przypadek, gdy wzorzec jest dopasowany, a nie wszystkie grupy zyskały zawartość).
Możemy oto rozumować następująco. Powodem naszych kłopotów był kwantyfikator * (0 lub więcej wystąpień, konkretnie spacji). Spróbujmy się więc ograniczyć i powiedzieć, że liczby mają być okalane conajmniej przez jeden biały znak. Ale to za mało. Mogą być przecież przypadki, kiedy wiersz zaczyna się od liczby, a później dopiero następują spacje (i ew. inne liczby w tekście), albo kiedy wiersz kończy się liczbą (bez końcowych spacji). Wydaje się zatem, że musimy przygotować alternatywne przypadki:
Alternatywy musimy grupować, ale ponieważ nie chcemy ich zapamiętywać jako grupy zastosujemy grupy niezpamiętywane. Zapamiętywac będziemy jednak grupy określające liczby (wystąpienia cyfr). Dochodzimy więc do następującego wzorca, który wstawimy do poprzedniego programu (w metodzie kompilacji).

(?:^(\d+)\s+)|(?:^(\d+)$)|(?:\s+(\d+)$)|(?:\s+(\d+)\s+)
W tym wzorcu mamy cztery grupy zapamiętywane (określane przez alternatywne dopasowania liczb). Oczywiście, przy każdym dopasowaniu całego wzorca tak naprawdę tylko jedna z grup może pasować. Pozostałe będą miały wartość null. W programie musimy więc odnajdywać tylko tę pasującą grupę i zrobimy to poprzez dostarczenie własnej metody
getMatchingGroupAsNumber(...). W metodzie tej będziemy też chcieli zobaczyć co naprawdę się dziej i jak są dopasowywane konkretne grupy. Informację tę pokażemy na konsoli.
Przykładowy program wygląda teraz tak:
import java.util.regex.*;
import java.io.*;

public class Sumowanie2 {

  private Pattern pattern = Pattern.compile("(?:^(\\d+)\\s+)|" +
                                        "(?:^(\\d+)$)|" +
                                        "(?:\\s+(\\d+)$)|" +
                                        "(?:\\s+(\\d+)\\s+)"
                                       );

  private Matcher matcher = pattern.matcher("");

  private int lgroup = matcher.groupCount(); // liczba grup

  public void sumAndReport(String fileName) {

    try {
      BufferedReader in = new BufferedReader(
                           new FileReader(fileName)
                       );
      int suma1 = 0,   // suma liczb występujących samotnie w wierszach
          suma2 = 0;   // suma liczb, które nie są w wierszach samotne

      String input;
      while ((input = in.readLine()) != null) {

        matcher.reset(input);          // nowy tekst wejściowy

        if (matcher.matches())         // czy mamy samotną liczbę?
          suma1 += getMatchingGroupAsNumber("matches");

        else {                         // nie, może jest kilka, albo w tekście?
          matcher.reset();
          while (matcher.find())
            suma2 += getMatchingGroupAsNumber("find");
        }
      }
      in.close();
      System.out.println("Suma samotnych liczb: " + suma1);
      System.out.println("Suma niesamotnych liczb: " + suma2);
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }

  }

  // Metoda zwracająca dopasowaną grupę
  // przekształconą na liczbę

  private int getMatchingGroupAsNumber(String info)
              throws NumberFormatException
  {
    System.out.println(info + "\nDopasowanie: \"" + matcher.group() + "\"");
    int wynik = 0;
    for (int i=1; i <= lgroup; i++) {
       System.out.println("Gr. " + i + " \"" + matcher.group(i) + "\"");
       if (matcher.group(i) != null) {
         wynik = Integer.parseInt(matcher.group(i));
       }
    }
    return wynik;
  }

  public static void main(String[] args) {
    new Sumowanie2().sumAndReport(args[0]);
  }

}
a wyniki jego działania dla  pliku wejściowego:
1
   2  
aa 5 b  5 5 5 c
100b
3
x3000
y40000y 11

są następujące:

matches
Dopasowanie: "1"
Gr. 1 "null"
Gr. 2 "1"
Gr. 3 "null"
Gr. 4 "null"
matches
Dopasowanie: "   2   "
Gr. 1 "null"
Gr. 2 "null"
Gr. 3 "null"
Gr. 4 "2"
find
Dopasowanie: " 5 "
Gr. 1 "null"
Gr. 2 "null"
Gr. 3 "null"
Gr. 4 "5"
find
Dopasowanie: "  5 "
Gr. 1 "null"
Gr. 2 "null"
Gr. 3 "null"
Gr. 4 "5"
find
Dopasowanie: " 5 "
Gr. 1 "null"
Gr. 2 "null"
Gr. 3 "null"
Gr. 4 "5"
matches
Dopasowanie: "3"
Gr. 1 "null"
Gr. 2 "3"
Gr. 3 "null"
Gr. 4 "null"
find
Dopasowanie: " 11"
Gr. 1 "null"
Gr. 2 "null"
Gr. 3 "11"
Gr. 4 "null"
Suma samotnych liczb: 6
Suma niesamotnych liczb: 26


Prawie dobrze! Niestety, find() zgubił jedną piątkę z wiersza
aa 5 b  5 5 5 c
i - oczywiście - jest to wina źle pomyślanego wyrażenia regularnego.
Sposób dopasowania wiersza do wzorca możemy prześledzić dokładniej:

Wzorzec: "(?:^(\d+)\s+)|(?:^(\d+)$)|(?:\s+(\d+)$)|(?:\s+?(\d+)\s+?)"
Tekst: "aa 5 b  5 5 5 c"
find(): Dopasowano podłańcuch " 5 " od pozycji 2 do pozycji 5.
find(): Dopasowano podłańcuch "  5 " od pozycji 6 do pozycji 10.
find(): Dopasowano podłańcuch " 5 " od pozycji 11 do pozycji 14.


Po dopasowaniu do wzorca drugiej piątki z okalającymi spacjami początek następnego wyszukiwania zaczyna się na (trzecim) znaku '5' - niestety nie pasuje on do żadnego z alternatywnych wzorców, zatem jest pomijany.

W tym przykładzie łatwo jest znaleźć takie (proste) wyrażenie regularne, które rozwiązuje problem. Wystarczy przywrócić (kłopotliwy poprzednio) kwantyfikator * , ale przy tym skorzystać z symbolu granicy słowa:
\s*\b(\d+)\b\s*
i wyniki (pierwotnego programu) będą nareszcie poprawne.
Np. dla pokazanego przed chwilą pliku testowego otrzymamy:

Suma samotnych liczb: 6
Suma niesamotnych liczb: 31


Nie zawsze jednak uda się skonstruować dobry wzorzec.
W Internecie, na różnego rodzaju forum, w różnego rodzaju podpowiedziach itp. można znaleźć wzorce opisujące wydawałoby się dość oczywiste kwestie (np. walidację adresu IP) zapisane w kilkunastu lub nawet kilkudziesięciu wierszach. Jest to oczywiście jak najbardziej niewłaściwy sposób korzystania z wyrażeń regularnych. 

Czasami trudno jest zbudować właściwy wzorzec. Czasami właściwy wzorzec jest na tyle skomplikowany i duży, że zgoła niebezpieczny (możliwe błędy). Czasami w ogóle nie można podać włąsciwego wyrażenia regularnego, jak np. w przypadku pełnej składniowej analizy wszystkich argumentów wywolania metody dla wszelkich możliwych wariantów zapisu tych argumentów (dowolny poziom zagnieżdżenia nawiasów).

Warto więc chyba zauwazyć, że:

Jako przykład połączenia wyrażeń regularnych i dodatkowych środków programistycznych pokażę w jaki sposób - posługując się niespelniającym wszystkich wymagań wszorcem - można w omawianym przykładzie sumowania liczb uzyskać prawidłowy wynik:
import java.util.regex.*;
import java.io.*;

public class Sumowanie3 {

  public static void sumAndReport(String fileName) {

    try {
      BufferedReader in = new BufferedReader(
                           new FileReader(fileName)
                       );

      // Ten wzorzec nie spłenia wszystkich wymagań
      Pattern pattern = Pattern.compile("\\s*(\\d+)\\s*");

      Matcher matcher = pattern.matcher("");

      int suma1 = 0,   // suma liczb występujących samotnie w wierszach
          suma2 = 0;   // suma liczb, które nie są w wierszach samotne

      String input;
      while ((input = in.readLine()) != null) {

        int len = input.length();       // długość tekstu

        matcher.reset(input);

        if (matcher.matches())         // czy mamy samotną liczbę?
          suma1 += Integer.parseInt(matcher.group(1));

        else {                         // nie, może jest kilka, albo w tekście?
          matcher.reset();
          while (matcher.find()) {

            // pozycja znaku tuż przed dopasowaną grupą cyfr
            int start = matcher.start(1) - 1;
            // pozycja znaku tuż po dopasowanej grupie cyfr
            int end = matcher.end(1);

            // jeżeli są takie znaki i nie są to spacje
            // - odrzucamy dopasowanie
            if (start == end ||
                (start >= 0 && !Character.isWhitespace(input.charAt(start))) ||
                (end < len && !Character.isWhitespace(input.charAt(end)))
               ) continue;

            // w przeciwnym razie mamy dopasowanie i sumujemy liczby
            suma2 += Integer.parseInt(matcher.group(1));
          }
        }
      }
      in.close();
      System.out.println("Suma samotnych liczb: " + suma1);
      System.out.println("Suma niesamotnych liczb: " + suma2);
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }

  }

  public static void main(String[] args) {
    sumAndReport(args[0]);
  }

}
Wykorzystaliśmy tutaj niektóre metody klasy Matcher (mianowicie uzyskiwanie informacji o pozycjach znaków, kstzałtujących grupy) i - wychodząc poza  sferę najprostszego zastosowania wyrażeń regularnych - sięgneliśmy do informacji o indeksach i znakach.
Dzięki temu mogliśmy korygować działanie metody find().

I na koniec o pewnym uproszczeniu. W omawianych tu przykładach wielokrotnie stosowaliśmy ten sam wzorzec do różnych tesktów wejściowych (wierszy pliku). W takich przypadkach ze względów efektywnościowych nieodzowny jest znany nam schemat postępowania, polegający na uprzedniej kompilacji wzorca, uzyskaniu matchera i wołaniu jego metod.
Wtedy jednak (i tylko wtedy), gdy chcemy dany wzorzec zastosować jednorazowo i to wyłącznie do stwierdzenia dopasowania całego tekstu możemy posłużyć się metodą matches() klasy Pattern.


Ma ona postać:

    boolean Pattern.matches(String regex, CharSequence input)

i powoduje - z naszego punktu widzenia w jednym kroku - kompilację wzorca regex, utworzenie matchera i zastosowanie jego metody matches() wobec tekstu wejściowego input.


2.9. Rozbiór

Klasa Pattern zawiera wygodną metodę split(...), która pozwala na dokonywanie podziału tekstu wejściowego na podłańcuchy separowane innymi podłańcuchami, które pasują do podanego wyrażenia regularnego.

Jest to swego rodzaju odpowiednik StringTokenizera, z tą różnicą, że separatorami wyłuskiwanych symboli mogą być dowolne napisy, pasujące do wzorca.

Aby rozłożyć tekst na symbole należy najpierw skompilować wzorzec, a następnie wobec uzyskanego obiektu klasy Pattern użyć metody split(...).
Występuje ona w dwóch formach:

 String[]split(CharSequence input)
          
 String[]split(CharSequence input, int limit)
          

Obie metody zwracają tablicę wyróżnionych podłańcuchów (symboli). Jeśli nie udało się znaleźć żadnego separatora pasującego do wzorca, to tablica ma jeden element, który zawiera cały rozkładany tekst (input).
W przeciwnym razie, kolejne elementy tablicy zawierają wyróżnione kolejne podłancuchy, każdy z których w sekwencji wejściowej kończy się podłańcuchem pasującym do wzorca lub końcem wejścia.
Argument limit okresla liczbę razy zastosowania wzorca do wyłuskiwania podłancuchów.
Jeżeli limit równy jest n >0, to wzorzec jest stosowany n-1 razy, wobec czego liczba elementów w tablicy równa jest co najwyżej n, a ostatni element stanowi podłańcuch od ostatniego separatora do końca tekstu wejściowego. Jeżeli limit jest mniejszy od 0, to wzorzec zostanie zastosowany tyle razy ile można, a tablica będzie zawierać wszystkie możliwe do wyłuskania podłańcuchy. To samo stosuje się do sytuacji, gdy limit równy jest 0, tyle, że ostatnie puste podłańcuchy nie zostaną umieszczone w tablicy.
Metoda split() bez parametru limit działa tak samo jak split() z parametrem limit = 0.

Poniższy program pokazuje przykładowe zastosowanie metody split() do rozbioru zawartości pliku podanego jako argument programu wedle separatorów określanych przez wzorce podawane w dialogach wejściowych. Przy okazji pokazano tutaj, że - tak samo jak w przypadku matchera - wejściowy tekst może być obiektem dowolnej klasy implementującej interfejs CharSequence i dlatego np. łatwo można zastosować machinę regularnych wyrażeń do buforów znakowych uzyskiwanych poprzez kanały plikowe. Oprócz tego warto zwrócić uwagę na flagę kompilacji DOTALL oraz na użycie metody pattern(), która zwraca aktualny wzorzec danego obiektu klasy Pattern.
import java.util.regex.*;
import java.nio.*;
import java.io.*;
import java.nio.channels.*;
import java.nio.charset.*;
import javax.swing.*;


public class Parsing {

  private CharBuffer input;
  private Charset inCharset  = Charset.forName("Cp1250");


  public void readFile(String fname) throws IOException {

    // Wczytanie pliku
    FileInputStream instream = new FileInputStream(fname);
    FileChannel in = instream.getChannel();
    ByteBuffer buf = ByteBuffer.allocate((int) in.size());
    in.read(buf); in.close(); instream.close();

    // Dekodowanie
    buf.flip();
    input = inCharset.decode(buf);
  }

  public void reporter(Pattern pattern) {
    String[] subs = pattern.split(input);
    System.out.println("Rozbiór wg wzorca: \""+ pattern.pattern() + "\"");
    boolean isOk = subs.length > 1;
    System.out.println( isOk ? " udany" : " nieudany");
    if (!isOk) return;
    System.out.println("Wyróżnione podłańcuchy:");
    for (int i=0; i < subs.length; i++) {
      System.out.println("[ " + (i+1) + " ] \"" + subs[i] + "\"");
    }
  }


  public static void main(String[] args) throws Exception {

    Parsing parse = new Parsing();
    parse.readFile(args[0]);

    String regex = "";
    while ((regex = JOptionPane.showInputDialog("Wzorzec", regex)) != null) {
      Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
      parse.reporter(pattern);
    }

  }

}
Podając jako testowy plik o następującej zawartości:
2 + ( 3 + 4)*10 = nie wiem ile;
ala + kot = ala ma kota;
jeden = 1;
dwa = 2;
trzy = 3; 
oraz stosując pokazane dalej wzorce, uzyskamy następujący wynik:

Rozbiór wg wzorca: "\r\n"
 udany
Wyróżnione podłańcuchy:
[ 1 ] "2 + ( 3 + 4)*10 = nie wiem ile;"
[ 2 ] "ala + kot = ala ma kota;"
[ 3 ] "jeden = 1;"
[ 4 ] "dwa = 2;"
[ 5 ] "trzy = 3;  "
Rozbiór wg wzorca: "([\r\n;])|( *= *)"
 udany
Wyróżnione podłańcuchy:
[ 1 ] "2 + ( 3 + 4)*10"
[ 2 ] "nie wiem ile"
[ 3 ] ""
[ 4 ] ""
[ 5 ] "ala + kot"
[ 6 ] "ala ma kota"
[ 7 ] ""
[ 8 ] ""
[ 9 ] "jeden"
[ 10 ] "1"
[ 11 ] ""
[ 12 ] ""
[ 13 ] "dwa"
[ 14 ] "2"
[ 15 ] ""
[ 16 ] ""
[ 17 ] "trzy"
[ 18 ] "3"
[ 19 ] "  "
Rozbiór wg wzorca: "[\r\n\p{Punct} ]+"
 udany
Wyróżnione podłańcuchy:
[ 1 ] "2"
[ 2 ] "3"
[ 3 ] "4"
[ 4 ] "10"
[ 5 ] "nie"
[ 6 ] "wiem"
[ 7 ] "ile"
[ 8 ] "ala"
[ 9 ] "kot"
[ 10 ] "ala"
[ 11 ] "ma"
[ 12 ] "kota"
[ 13 ] "jeden"
[ 14 ] "1"
[ 15 ] "dwa"
[ 16 ] "2"
[ 17 ] "trzy"
[ 18 ] "3"


2.10. Zastępowanie

Matcher umożliwia  zastępowanie fragmentów tekstu wejściowego pasujących do wzorca podanymi napisami. Służy do tego kilka metod klasy Matcher,
Dwie najprostsze działają w następujący sposób:

 StringreplaceAll(String replacement)
          Zwraca łańcuch znakowy, w którym zastąpiono każdy fragment tekstu wejściowego, pasujący do wzorca napisem podanym jako argument replacement.
 StringreplaceFirst(String replacement)
          Zwraca łańcuch znakowy, w którym zastąpiono pierwsze wystąpienie fragmentu tekstu wejściowego, pasującego do wzorca, napisem podanym jako argument replacement.
Uwaga. Należy baczną uwagę zwrócić na to, że metody te nie zmieniają tekstu wejściowego, ale zwracają nowy napis, w którym dokonano zmian.

Przykład:
Zamienić w pliku wejściowym wielokrotne spacje - pojedynczymi.
import java.util.regex.*;
import java.io.*;

public class Replace1 {

  public static void main(String[] args) {

    Matcher matcher = Pattern.compile(" +").matcher("");

    try {
      BufferedReader in = new BufferedReader(
                            new FileReader(args[0])
                          );
      String input;
      while ((input = in.readLine()) != null) {
        matcher.reset(input);
        input = matcher.replaceAll(" ");
        System.out.println(input);
      }
      in.close();
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }

  }

}

Podając na wejściu np. taki plik:
To jest jakiś tekst  gdzieniegdzie w nim jest   za dużo spacji
i     w tym wierszu też
a w tym nie.

w wyniku otrzymamy:

To jest jakiś tekst gdzieniegdzie w nim jest za dużo spacji
i w tym wierszu też
a w tym nie.


Inny zestaw metod  zastępowania pozwala na bardziej wybiórcze, selektywne i elastyczne zamiany.
Są to metody:
 MatcherappendReplacement(StringBuffer sb, String replacement)
          
 StringBufferappendTail(StringBuffer sb)
          

Metody te są głównie przeznaczone do wspólnego wykorzystania z metodą find().

Metoda appendReplacement(...) posługuje się specjalnym stanem matchera, który nazywa się "pozycją append" i  działa w następujący sposób:
  1. kopiuje do bufora StringBuffer wszystkie znaki  tekstu wejściowego od bieżącej pozycji "append" (przy pierwszym użyciu jest to początek tekstu wejściowego) do pozycji poprzedzającej ostatnie dopasowanie wzorca (czyli start()-1),
  2. następnie dopisuje do bufora tekst podany jako jej argument replacement,
  3. ustala bieżącą pozycję "append" na pierwszy znak po ostatnio dopasowanym wzorcu (czyli end())
Metoda appendTail(...) - wywoływana  po użyciu metody appendReplacemnet() - dopisuje do bufora pozostałą część napisu wejściowego (czyli od ostatniej pozycji "append" do końca tekstu).

Poprzedni przykład zastępowania spacji moglibyśmy za pomocą tych metod zapisać tak:
import java.util.regex.*;

public class Replace2 {

  public static void main(String[] args) {

    String input = "To jest   tekst  gdzie za dużo  spacji";
    Matcher matcher = Pattern.compile(" +").matcher(input);
    StringBuffer nowyTekst = new StringBuffer();
    while (matcher.find()) {
      matcher.appendReplacement(nowyTekst, " ");
    }
    matcher.appendTail(nowyTekst);
    System.out.println("Tekst wejściowy:");
    System.out.println(input);
    System.out.println("Tekst wyjściowy:");
    System.out.println(nowyTekst.toString());
  }

}
i otrzymać w wyniku:

Tekst wejściowy:
To jest   tekst  gdzie za dużo  spacji
Tekst wyjściowy:
To jest tekst gdzie za dużo spacji


Jako prosty przykład zwiększonej elastyczności zastępowania można podać zastąpeinie co drugiego wystąpienia cyfry 1 cyfrą 3:
import java.util.regex.*;

public class Replace3 {

  public static void main(String[] args) {

    String input = "1111111111111";
    Matcher matcher = Pattern.compile("1").matcher(input);
    StringBuffer nowyTekst = new StringBuffer();
    while (matcher.find() && matcher.find()) {
      matcher.appendReplacement(nowyTekst, "3");
    }
    matcher.appendTail(nowyTekst);
    System.out.println("Tekst wejściowy:");
    System.out.println(input);
    System.out.println("Tekst wyjściowy:");
    System.out.println(nowyTekst.toString());
  }

}

Tekst wejściowy:
1111111111111
Tekst wyjściowy:
1313131313131



Ważną właściwością wszystkich metod zastępowania jest to, że w napisie stanowiącym ich argument - tekst zastępujący - możemy odwoływać się do zawartości grup wzorca.
Wtedy tekst zastępujący będzie zawierał zawartość grupy z wyrażenia.
Inaczej niż w odniesieniach zwrotnych w wyrażeniu regularnym przy zastępowaniu stosujemy znak $ z następującym po nim numerem grupy.

Przykład.

Plik zawiera listę studentów w postaci:
sNumerIndeksu Imię Nazwisko

np.
s1111 Jan Kowalski

Chcemy tę liste zmiodyfikować w taki sposób, by najpierw było naziwsko, później imię a na końcu numer indeksu, ale bez popzredzającego s.

Zastosujemy metodę replaceFirst z argumentem zawierającym odniesienia do grup następującego wyrażenia:
^s(\d+) +([\p{L}-]+) +([\p{L}-]+)$
które oznacza:
poczatek wiersza, s, następnie jedna lub więcej cyfr, następnie po spacji lub spacjach ciąg liter Unicode lub znaków -, znowu spacje i znowu ciąg liter Unicode lub znaków - tuż przed końcem wiersza, Wyrażenie to odpowiada struktorze początkowej listy studentów (aczkolwiek  nie jest dość uniwersalne ani odporne na błędy). Numer indeksu jest tutaj grupą 1, imię - grupą 2, a nazwisko - grupą 3. W argumencie metody replaceFirst użyjemy odwołań do tych grup: "$3 $2 $1", co oznacza, że dopasowany do wzorca tekst wejściowy ma być zastąpiony przez:  nazwisko, spacja, imię, spacja, numer indeksu (bez s!).
Poniższy program:
import java.util.regex.*;
import java.io.*;

public class Replace4 {

  public static void main(String[] args) {

    Matcher matcher = Pattern.compile(
                        "^s(\\d+) +([\\p{L}-]+) +([\\p{L}-]+)$"
                      ).matcher("");

    try {
      BufferedReader in = new BufferedReader(
                            new FileReader(args[0])
                          );
      String input;
      while ((input = in.readLine()) != null) {
        matcher.reset(input);
        input = matcher.replaceFirst("$3 $2 $1");
        System.out.println(input);
      }
      in.close();
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }

  }

}
użyty wobec pliku przykładowego:

s1111 Jan Kowalski
s3211 Michał Michałowski
s4235 Kazimierz Kazik-Kaźmierski

wyprowadzi:

Kowalski Jan 1111
Michałowski Michał 3211
Kazik-Kaźmierski Kazimierz 4235




2.11. Metody klasy String związane z wyrażeniami regularnymi

Jako pewne uproszczenie, do zastosowań ad hoc, znajdziemy w klasie String metody, które odwzorowują niektore z omówionych metod klas Matcher i Pattern.
Są one następujące.

 booleanmatches(String regex)
          Czy ten napis pasuje do wzorca regex?
 StringreplaceAll(String regex, String replacement)
          Zastępuje każdy pasujący do regex podłańcuch tego napisu podanym napisem replacement
 StringreplaceFirst(String regex, String replacement)
          Zastępuje pierwszy pasujący do regex podłańcuch tego napisu podanym napisem replacement
 String[]split(String regex)
          Rozklada ten napis wokół separatorów, które są podłancuchami pasującymi do wzorca
 String[]split(String regex, int limit)
          j.w., ale nie więcej niż limit-1 razy


Metody te są ścisłymi odpowiednikami odpowiednich metod klas Matcher i Pattern. Faktycznie, "wewnętrznie" wykorzystują one właśnie te klasy i ich metody. Np. jeśli txt jest typu String, to
txt.matches("[0-9]+");
jest tożsame z :
Pattern.compile("[0-9]+").matcher().matches();

Oznacza to, że tych "skróconych" metod klasy String powinniśmy używać wyłącznie wetdy, gdy dane wyrażenie regularne i związany z nim matcher używane są jednokrotnie. Przy wielokrotnym użyciu nalezy najpierw wyrażenie skompilować (raz), po czym dopiero wielokrotnie użyć, co oczywiście już wymaga wykorzystania klas Pattern i Matcher.



2.12. Praktyczny przykład

Na koniec rozważmy bardziej rozbudowany praktyczny przykład, korzystający praktycznie ze wszystkich omówionych dotąd narzędzi wyrażeń regularanych, jak również pokazujący w jaki sposób można wspierać ich moc dodatkowymi środkami programistycznymi. Przy okazji jeszcze raz zwrócimy uwagę na pewne istotne kwestie, związane ze stosowaneim wyrażeń regularnych.

Chcemy oto zapisać do pliku wyjściowego wszystkie nagłówki (oznaczane znacznikami <h1>, <h2> itd.) z jakiegoś  pliku HTML, stworzyć swoisty spis treści.
Zadanie dobrania właściwego wzorca wydaje się trywialne. Nagłówek jest tekstem występującym pomiędzy otwierającym znacznikiem <hn> i zamykającym znacznikeim </hn>, gidze n - poziom nagłówka.
Narzucającym się wzorcem mógłby być:
<h[1-6]>(.+)</h[1-6]>
Istotnie, dla pliku w postaci:
<h1>Nagłówek 1</h1>
<h2>Nagłówek 1.1</h2>
<h2>Nagłówek 1.2</h2>
uzyskamy właściwe dopasowania i będziemy mogli wyłuskać zapamiętane w grupach nagłówki:

find(): Dopasowano podłańcuch "<h1>Nagłówek 1</h1>" od pozycji 0 do pozycji 19.
find(): Dopasowano podłańcuch "<h2>Nagłówek 1.1</h2>" od pozycji 20 do pozycji 41.
find(): Dopasowano podłańcuch "<h2>Nagłówek 1.2</h2>" od pozycji 42 do pozycji 63.


Jednak w plikach HTML nagłówki mogą przecież być dzielone na wiele wierszy. Dla pliku:

<h1>Nagłówek 1
        rozbity na wiersze</h1>
<h2>Nagłówek 1.1</h2>
<h2>Nagłówek 1.2</h2>
find() nie znajdzie dopasowania nagłówka poziomu 1. Dlaczego? Bo metaznak . domyślnie pasuje do każdego znaku oprócz znaku końca wiersza. Powinniśmy zatem zastosować - albo przy kompilacji wzorca, albo w samym wyrażeniu - flagę DOTALL - ?s (powodująca pasowanie metaznaku . również do znaków końca wiersza). A jeśli już, to uwzględnijmy też fakt, że znaczniki html mogą byc pisane małą i dużą literą (zatem wprowadźmy flagę CASE_INSENSITIVE - ?i).
Nowy - teraz wydawałoby się prawidłowy - wzorzec wyglądałby tak (flagi wprowadzimy w samym wyrażeniu).
(?i)(?s)<h[1-6]>(.+)</h[1-6]>
Czy teraz  find() znajdzie wszystkie trzy nagłówki? Nic z tych rzeczy!
Znajdzie tylko jeden duży tekst zaczynający się pierwszym znacznikiem <h1> i kończący się ostatnim znacznikiem </h2>. Znowu zwracam uwagę na żarłoczność (greediness) kwantyfikatorów, bowiem jest to temat dość nieintuicyjny.
Zobaczmy: żarłoczny .+ skonsumował wszystkie znaki do końca tekstu wejściwoego (mógł to zrobić, bo teraz kropka pasuje do znaków końca wiersza). Po czym matcher cofając się napotkał dopasowanie </h2> (ostatnie w tekście) i to spełniło wymagania wzorca dla metody find(), która w tym momencie zakończyła działanie.
Aha, zatem powinniśmy zastosować kwantyfikator wstrzemięźliwy (reluctant).
(?i)(?s)<h[1-6]>(.+?)</h[1-6]>

W teście wygląda to teraz całkiem dobrze:

Wzorzec: "(?i)(?s)<h[1-6]>(.+?)</h[1-6]>"
Tekst: "<h1>Nagłówek 1
   rozbity na wiersze</h1>
<h2>Nagłówek 1.1</h2>
<h2>Nagłówek 1.2</h2>"
find(): Dopasowano podłańcuch "<h1>Nagłówek 1
   rozbity na wiersze</h1>" od pozycji 0 do pozycji 41.
find(): Dopasowano podłańcuch "<h2>Nagłówek 1.1</h2>" od pozycji 42 do pozycji 63.
find(): Dopasowano podłańcuch "<h2>Nagłówek 1.2</h2>" od pozycji 64 do pozycji 85.


Spróbujmy więc zastosować ten właśnie wzorzec w programie wypisującym "spis treści" pliku html. Zobaczymy jednak, że sam wzorzec nie wystarcza. Musimy rozstrzygnąć jeszcze kilka kwestii:

Przy wprowadzaniu pliku najprostsze wydaje się wczytanie całego pliku do pamięci i traktowanie go w całości jako tekstu wejściowego matchera. Jak już widzieliśmy wygodnie będzie zrobić to za pomocą kanału plikowego, jednocześnie jednak wymagać to będzie odpowiedniego dekodowania bufora znakowego.  Przy dekodowaniu trzeba wiedzieć w jakiej stronie kodowej zapisany jest plik. Ta informacja powinna być dostępna w pliku html za pomocą metatagu charset, ale jeśli jej brak, to możemy przyjąć jakąś domyślną stronę kodową. Dla odnalezienia opisu strony kodowej w pliku html zastosujemy (oczywiście) wyrażenie regularne. Musimy przy tym wiedzieć, że w metatagu charset=... po obu stronach znaku = mogą wystąpić (bądź nie) spacje, że nazwa strony kodowej może składać się z liter, cyfr, znaku podkreślenia, myślnika, kropki, dwukropka i że wielkość liter w nazwie nie ma znaczenia.
Prosty wzorzec (nie wykluczający wadliwych metatagów charset) mógłby wyglądać tak:
(?i)charset *?= *?([\p{Alnum}.:_-]+)

Znaki nowego wiersza (występujące wewnątrz wypisywanych nagłówków)  powinniśmy zamienić na (jedną) spację. Ale "przejście do nowego wiersza" może być oznaczane przez znaki \r\n (0d0a) jak w Windows lub przez sam znak \n - jak w Unixie. Aktualny separator obowiązujący na danej platformie uzyskamy z właściwości systemowych. Gdyby nie metody replace... klasy matchera zamiana na (jedną!) spację wymagałaby od nas trochę uciążliwej pracy.

Możliwe błędy w HTML nie dadzą się opisać za pomocą wyrażeń regularnych. Musimy więc znowu zastosować dodatkowe środki programistyczne, korzystające oczywiście z różnych metod matchera. Mianowicie:
Te wskazówki wystarczą do napisania programu. Zobaczymy poniżej, że jego kod korzysta z kilku różnych wzorców i wielu metod klas Matcher i Pattern (w tym split i replaceAll). Sczegółowe wyjąsnienie znaleźć można w komentarzach.
import java.util.regex.*;
import java.nio.*;
import java.io.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class Headers1 {

  // Matcher do wyszukiwania metatagu charset
  // i wyłuskania strony kodowej
  private Matcher charsetMatcher = Pattern.compile(
                                   "charset *?= *?([\\p{Alnum}.:_-]+)",
                                   Pattern.CASE_INSENSITIVE
                                   ).matcher("");

  // Matcher do wyłuskania tytułu dokumentu
  private Matcher titleMatcher =  Pattern.compile(
                                  "<title>(.+?)</title>",
                                  Pattern.CASE_INSENSITIVE |
                                  Pattern.DOTALL
                                  ).matcher("");

  // Matcher do wyszukiwania podtytułów (headingów)
  private Matcher headingMatcher = Pattern.compile(
                                    "<h[1-6]>(.+?)</h[1-6]>",
                                    Pattern.CASE_INSENSITIVE |
                                    Pattern.DOTALL
                                    ).matcher("");

  // Matcher do zastępowania domyślnego dla danej platformy
  // zestawu znaków końca wiersza jedną spacją
  private Matcher replaceMatcher = Pattern.compile(
                                    System.getProperty("line.separator")//"\\r\\n"
                                   ).matcher("");

  // Wzorzec do podziału dokumentu na dwie części: nagłówek i ciało
  private Pattern splitPattern = Pattern.compile("</head>",
                                         Pattern.CASE_INSENSITIVE
                                 );


  // Domyślna strona kodowa
  private Charset defaultCharset = Charset.forName("ISO-8859-2");

  ByteBuffer channelBuffer;   // bufor bajtowy kanalu
  CharBuffer input;           // bufor znakowy z wejściem dla matcherów


  // Główna metoda
  // przetwarzająca dokument HTML
  // i wypisująca jego "spis treści"

  public void processFile(String fname) {
    // Wczytanie pliku
    try {
      FileInputStream instream = new FileInputStream(fname);
      FileChannel in = instream.getChannel();
      channelBuffer = ByteBuffer.allocate((int) in.size());
      in.read(channelBuffer);
      in.close();
      instream.close();
    } catch (IOException exc) { errMsg(exc); }

    // Dekodowanie (na razie wedle domyślnej strony)
    // Jeśli okaże się, że plik jest kodowany inaczej
    // - powtórzymy dekodowanie

    channelBuffer.flip();
    input = defaultCharset.decode(channelBuffer);

    // Teraz wyróżniamy dwie części pliku : nagłówek pliku
    // i ciało. W ten sposób matchery będą działać szybciej
    // Uwaga: podział następuje na pierwszym </head>
    // ew. następne w tekście html (np. w <pre>...</pre>
    // nie będą brane pod uwagę

    String[] part = splitPattern.split(input, 2);
    // jeżeli jest nagłówek - szukamy w nim określenia strony kodowej
    // i ew. ponawiamy dekodowanie za pomocą własnej metody newDecode,
    // a także wyszukujemy tytuł

    String title = "Bez tytułu";  // zakładamy, że nie ma tytułu
    String body;                  // tu będzie ciało, w nim będziemy szukać
                                  // headingów

    if (part.length == 2) {        // jeżeli jest nagłówek
       if (newDecoding(part[0]))   // jeżeli ponowiono dekodowanie
          part = splitPattern.split(input, 2); // - nowy podział na części

       titleMatcher.reset(part[0]);  // szukamy tytułu
       if (titleMatcher.find())
         title = titleMatcher.group(1);

       body = part[1];             // ciałem będzie druga część
    } else {                       // nie ma nagłówka, wtedy
       body = part[0];             // ciałem będzie jedyna pierwsz część
    }

    System.out.println(title);     // wypisanie tytułu

    // Teraz szukamy headingów
    headingMatcher.reset(body);
    while (headingMatcher.find()) {
      String heading = headingMatcher.group(1);
      replaceMatcher.reset(heading);  // zamiana znaku końca wiersza
      heading = replaceMatcher.replaceAll(" "); // na jedną spację

      // Sprawdzenie poprawności
      boolean valid = true;

      // - czy nie ma <h>  w srodku?
      if (heading.indexOf("<h") != -1) valid = false;

      // czy otwierający i zamykający numer poziomu jest zgodny?
      String match = headingMatcher.group();
      char level = match.charAt(2);
      if (level != match.charAt(match.length()-2)) valid = false;
      if (!valid)
        System.out.println("***** uwaga poniższy heading jest błędny!");
      else {
         int k = level - '0' - 1;
         while(k-- > 0)                // poziomy wyróżniamy tabulacjami
           System.out.print('\t');
      }
      System.out.println(heading);      // wypisanie podtytułu
    }

  }

  // Metoda wyszukuje metatatg charset i ew. ponawia dekodowanie
  // Jeśli tagu nie ma, albo podany charset jest taki sam jak
  // domyślny, albo jest wadliwy - nie ponawiamy dekodowania
  // Zwraca true, gdy zdekdodowano na nowa
  private boolean newDecoding(String header) {
    charsetMatcher.reset(header);
    if (charsetMatcher.find()) { // czy jest metatag charset?
       try {
           Charset fileCharset = Charset.forName(charsetMatcher.group(1));
           if (fileCharset.equals(defaultCharset)) // gdy taki sam jak domyślny
              return false;                        // nie trzeba dekodować
           channelBuffer.flip();                   // inny - ponowne dekodowanie
           input = fileCharset.decode(channelBuffer);
         } catch(Exception exc) {
             return false;
         }
       return true; // zdekodowany na nowo
    }
    return false;
  }

  private void errMsg(Exception exc) {
    exc.printStackTrace();
    System.exit(1);
  }

  public static void main(String[] args) {
     new Headers1().processFile(args[0]);
  }

}

2.13. Zadania i ćwiczenia

  1. Zbudować wyrażenie regularne, które dopasowuje dowolną liczbę - całkowitą lub rzeczywistą. Użyć go w programie, który z pliku wejściowego wybiera wszystkie takie liczby i zastępuje je ich dwukrotnością.
  2. Stworzyć program do przetwarzania plików HTML, w taki sposób, by zapisane w nich kody programów Javy, znajdujące się w znacznikach <pre>.... </pre> były zapisowane jako pliki .java z nazwą wyłuskaną z kodu na podstawie nazwy obecnej tam klasy publicznej.
  3. Napisać program, który  w pliku źródłowym zmienia nazwy wybranych metod. Uwaga: trzeba też zmeinić identyfikatory w wyrażeniach wywołujących metody.