Wyrażenia regularne w praktyce
- Teksty i String
- poziom: średni
- aktualizacja: 16 sierpnia 2026
Wyrażenie regularne wygląda niewinnie, dopóki nie zderzysz go z polskim tekstem. Domyślnie \w i \b znają wyłącznie ASCII, więc „Zażółć gęślą jaźń” rozpada się w \w+ na strzępy „Za”, „g”, „l”, „ja”, a wzorzec łódź z flagą CASE_INSENSITIVE nie dopasuje napisu „ŁÓDŹ”. Lekarstwem są flagi UNICODE_CHARACTER_CLASS i UNICODE_CASE, których prawie nikt nie włącza, dopóki się na tym nie przejedzie. Skrypt pokazuje ten efekt na żywo — wydrukiem przed włączeniem flagi i po nim — a przy okazji przerabia resztę warsztatu: wyciąga kody pocztowe i numery telefonów, anonimizuje adres e-mail w logu, dzieli tekst i sprawdza, co się dzieje, gdy wzorzec puścisz na kilkadziesiąt tysięcy znaków.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Wyrażenia regularne w praktyce: Pattern, Matcher i ich pułapki — skryptyJAVA.pl
* Zapisz jako: WyrazeniaRegularne.java
*/
public class WyrazeniaRegularne {
// Pattern jest niezmienny i bezpieczny wątkowo; nowy Matcher tworzymy dla każdego szukania.
private static final Pattern KOD_POCZTOWY = Pattern.compile("\\b\\d{2}-\\d{3}\\b");
// Grupa obcy pochłania pełny obsługiwany zapis: +CC i dziewięć cyfr.
private static final Pattern TELEFON = Pattern.compile(
"(?<![\\d+])(?:(?<kraj>\\+48)|(?<obcy>\\+\\d{1,3}))?[\\h-]*"
+ "(?<numer>\\d{3}(?:[\\h-]*\\d{3}){2})(?!\\d)");
// Atext obejmuje typową niecytowaną część lokalną; domena pozostaje uproszczona do ASCII.
private static final Pattern EMAIL = Pattern.compile(
"(?<![\\p{L}\\p{M}\\p{N}!#$%&'*+/=?^_`{|}~.-])"
+ "(?<lokalny>[\\p{L}\\p{M}\\p{N}!#$%&'*+/=?^_`{|}~.-]+)"
+ "@(?<domena>[A-Za-z0-9.-]+\\.[A-Za-z]{2,})");
private static final Pattern SLOWA_ASCII = Pattern.compile("\\w+");
private static final Pattern SLOWA_PL = Pattern.compile("\\w+", Pattern.UNICODE_CHARACTER_CLASS);
/** Zwraca wszystkie fragmenty pasujące do wzorca; pusta lista, gdy nic nie pasuje. */
public static List<String> znajdzWszystkie(Pattern wzorzec, String tekst) {
Matcher szukacz = wzorzec.matcher(tekst);
List<String> wyniki = new ArrayList<>();
while (szukacz.find()) {
wyniki.add(szukacz.group());
}
return wyniki;
}
/** Normalizuje polskie telefony; rozpoznaje prefiks zapisany jako +CC przed dziewięcioma cyframi. */
public static List<String> normalizujTelefony(String tekst) {
Matcher szukacz = TELEFON.matcher(tekst);
List<String> wyniki = new ArrayList<>();
while (szukacz.find()) {
if (szukacz.group("obcy") != null) { continue; }
String cyfry = szukacz.group("numer").replaceAll("[\\h-]", "");
// Grupa opcjonalna, która nie wzięła udziału w dopasowaniu, zwraca null.
String uwaga = szukacz.group("kraj") == null ? " (kierunkowy dopisany)" : "";
wyniki.add("+48 " + cyfry.substring(0, 3) + " " + cyfry.substring(3, 6)
+ " " + cyfry.substring(6) + uwaga);
}
return wyniki;
}
/** Maskuje całą lokalną część uproszczonego adresu: żaneta@firma.pl -> ***@firma.pl. */
public static String anonimizujEmaile(String log) {
return EMAIL.matcher(log).replaceAll("***@${domena}");
}
/** Podmiana dosłowna: quote unieszkodliwia wzorzec, quoteReplacement — tekst zamiennika. */
public static String podmienDoslownie(String tekst, String szukane, String zamiennik) {
return Pattern.compile(Pattern.quote(szukane))
.matcher(tekst)
.replaceAll(Matcher.quoteReplacement(zamiennik));
}
/** Dopasowuje wzorzec do długiego tekstu i opisuje wynik zamiast przerywać program. */
private static String sprawdzDlugiTekst(Pattern wzorzec, String tekst) {
long start = System.nanoTime();
try {
boolean pasuje = wzorzec.matcher(tekst).matches();
return "wynik " + pasuje + " po " + (System.nanoTime() - start) / 1_000_000 + " ms";
} catch (StackOverflowError blad) {
// Silnik regexów schodzi w rekurencję, więc przepełnienie stosu to Error, nie wyjątek.
return "StackOverflowError po " + (System.nanoTime() - start) / 1_000_000 + " ms";
}
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) {
String adres = "Adres: 00-950 Warszawa, magazyn 31-548 Kraków";
System.out.println("MATCHES vs FIND");
System.out.println(" matches na całym zdaniu: " + KOD_POCZTOWY.matcher(adres).matches());
System.out.println(" matches na samym kodzie: " + KOD_POCZTOWY.matcher("00-950").matches());
System.out.println(" find znalazł: " + znajdzWszystkie(KOD_POCZTOWY, adres));
String kontakt = "Kontakt +48 601 202 303, magazyn 512-118-990, faktura 12345678901234";
System.out.println("GRUPY NAZWANE");
normalizujTelefony(kontakt).forEach(numer -> System.out.println(" " + numer));
Pattern naiwny = Pattern.compile("\\d{3}[ -]?\\d{3}[ -]?\\d{3}");
System.out.println(" wzorzec bez strażników: " + znajdzWszystkie(naiwny, kontakt));
System.out.println("ANONIMIZACJA W LOGU");
System.out.println(" " + anonimizujEmaile("14:02 login żaneta.kowalska@firma.com.pl, IP 10.0.0.7"));
System.out.println("ZNAKI SPECJALNE PO OBU STRONACH");
String szablon = "Cena: {kwota} za sztukę";
try {
System.out.println(szablon.replaceAll("{kwota}", "3,50 zł"));
} catch (PatternSyntaxException wyjatek) {
System.out.println(" klamra we wzorcu: " + wyjatek.getClass().getSimpleName());
}
try {
System.out.println(szablon.replaceAll(Pattern.quote("{kwota}"), "3,50 zł ($2 za kg)"));
} catch (IndexOutOfBoundsException wyjatek) {
System.out.println(" dolar w zamienniku: " + wyjatek.getMessage());
}
System.out.println(" " + podmienDoslownie(szablon, "{kwota}", "3,50 zł ($2 za kg)"));
String cel = "Kopiuj do {cel}", sciezka = "C:\\dane\\raport";
System.out.println(" backslash wprost: " + cel.replaceAll(Pattern.quote("{cel}"), sciezka));
System.out.println(" po quoteReplacement: " + podmienDoslownie(cel, "{cel}", sciezka));
System.out.println("POLSKIE LITERY W \\w");
System.out.println(" \\w+ bez flagi: " + znajdzWszystkie(SLOWA_ASCII, "Zażółć gęślą jaźń"));
System.out.println(" \\w+ z flagą: " + znajdzWszystkie(SLOWA_PL, "Zażółć gęślą jaźń"));
Pattern bezUnicodeCase = Pattern.compile("łódź", Pattern.CASE_INSENSITIVE);
Pattern zUnicodeCase = Pattern.compile("łódź", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
System.out.println(" ŁÓDŹ bez UNICODE_CASE: " + bezUnicodeCase.matcher("ŁÓDŹ").matches());
System.out.println(" ŁÓDŹ z UNICODE_CASE: " + zUnicodeCase.matcher("ŁÓDŹ").matches());
System.out.println("SPLIT I PUSTE POLA");
System.out.println(" domyślnie: " + Arrays.toString("Anna;Jan;;Ewa;;".split(";")));
System.out.println(" limit -1: " + Arrays.toString("Anna;Jan;;Ewa;;".split(";", -1)));
System.out.println("DŁUGI TEKST I REKURENCJA");
String duzy = "ala ma kota ".repeat(4000);
Pattern zGrupa = Pattern.compile("(\\s*\\w+)*\\s*");
System.out.println(" (\\s*\\w+)*\\s* -> " + sprawdzDlugiTekst(zGrupa, duzy));
System.out.println(" [\\w\\s]+ -> " + sprawdzDlugiTekst(Pattern.compile("[\\w\\s]+"), duzy));
}
}
Jak użyć tego skryptu
- Klasa publiczna narzuca tu nazwę pliku, więc zapisz kod jako
WyrazeniaRegularne.javai uruchom go poleceniemjava WyrazeniaRegularne.java. Wydruk przechodzi przez siedem sekcji, a niemal każda kończy się celowo źle: wyjątkiem, wynikiemfalsealbo po cichu okrojonym tekstem — chodzi o to, żeby zobaczyć porażkę z bliska, a nie samą ścieżkę szczęśliwą. - Do szukania fragmentów służy
find()w pętli, opakowane tutaj wznajdzWszystkie(WZORZEC, tekst), które zwracaList<String>dopasowań.matches()dajetruedopiero wtedy, gdy wzorzec pokrywa cały tekst od pierwszego do ostatniego znaku, alookingAt()— gdy pokrywa sam początek. - Własne wzorce trzymaj tak samo jak tutaj: w polu
private static final Patternna poziomie klasy. SamPatternjest niezmienny i możesz go dzielić między wątkami, aleMatcherpamięta stan ostatniego dopasowania, więc każde wyszukiwanie potrzebuje świeżego, tworzonego przezwzorzec.matcher(tekst). - Fragmenty odczytasz po nazwie:
szukacz.group("numer"). Grupaobcypochłania telefon z innym prefiksem, aby pętla mogła go pominąć bez ponownego dopasowania dziewięciocyfrowej końcówki. Grupa opcjonalna, która nie brała udziału w dopasowaniu, zwracanull; literówka w nazwie grupy rzuca natomiastIllegalArgumentException. WzorzecEMAILcelowo obsługuje tylko typowe, niecytowane adresy spotykane w logach: maskuje całą rozpoznaną część lokalną, ale nie jest pełnym walidatorem składni RFC.
Warto wiedzieć
Pattern.compileparsuje wzorzec i buduje z niego drzewo obiektów, dlatego w tym skrypcie wzorce są stałymi. MetodyString.matches,String.replaceAlliString.replaceFirstpowtarzają tę kompilację przy każdym wywołaniu, więc w pętli po stu tysiącach wierszy potrafią kosztować więcej niż samo dopasowanie. Wyjątkiem jestString.split: dla separatora będącego jednym zwykłym znakiem, na przykład średnika, ma szybką ścieżkę bez kompilowania czegokolwiek — ale wystarczy dopisać spacje jako\s*;\s*i kompilacja wraca do każdego wiersza.- Flaga
UNICODE_CHARACTER_CLASSnaprawia\w,\bi\s, ale przy okazji rozciąga\dna cyfry wszystkich pism — z tą flagą wzorzec kodu pocztowego przyjąłby zapis cyframi arabsko-indyjskimi. DlategoKOD_POCZTOWYiTELEFONzostają przy ASCII, a flagę dostaje wyłącznie wzorzec dzielący tekst na słowa. Tak samo działaCASE_INSENSITIVE: bezUNICODE_CASEzna tylko pary z zakresu a-z, więc „Ł” i „ł” są dla niego dwiema różnymi literami. Pattern.quoteiMatcher.quoteReplacementto dwie różne rzeczy i zwykle potrzebujesz obu — pierwsza unieszkodliwia wzorzec, druga tekst zamiennika. Po stronie zamiennika$1wskazuje grupę, więc$przed cyfrą bez odpowiadającej grupy rzucaIndexOutOfBoundsException, a przed literą albo na końcu napisu —IllegalArgumentException. Gorszy jest backslash, bo znika po cichu: ścieżkaC:\dane\raportwstawiona wprost dajeC:daneraportbez śladu błędu. Nie zastępuj teżPattern.quoteręcznym doklejaniem\Qi\E— gdy w tekście użytkownika trafi się samo\E, wzorzec pęknie w połowie.- Silnik regexów w Javie nie iteruje po tekście, tylko schodzi w rekurencję: każdy obrót grupy pod kwantyfikatorem to kilka ramek stosu. Dlatego wzorzec
(\s*\w+)*\s*puszczony na 48 tysięcy znaków kończy sięStackOverflowError, a równoważna klasa znaków[\w\s]+przerabia ten sam tekst w kilka milisekund, bo pojedyncze znaki silnik dopasowuje w pętli. Drugą stroną tego medalu jest katastrofalny nawrót, czyli atak ReDoS: wzorce w rodzaju(a+)+$na wejściu, które prawie pasuje, liczą wykładniczo długo. Nowsze wydania JDK część takich przypadków rozbrajają, ale nie licz na to — nie zagnieżdżaj kwantyfikatorów, tnij tekst na wiersze i ograniczaj długość danych z formularza. W demieStackOverflowErrorjest łapany tylko po to, żeby wydruk dobiegł do końca; w normalnym kodzie błędów tej klasy się nie przechwytuje.
Podobne skrypty
Walidacja danych: e-mail, PESEL i NIP
Zestaw gotowych metod sprawdzających adres e-mail, numer PESEL (data urodzenia, płeć i cyfra kontrolna) oraz NIP z sumą kontrolną modulo 11, z tolerancją myślników i spacji.
średni Zobacz kod → Teksty i StringPrzyjazny URL z polskich znaków
Zamienia tytuł „Żółć i Łódź!” na adres zolc-i-lodz: polskie litery rozkłada Normalizer, znaki specjalne wypadają, powtórzone separatory sklejają się w jeden, a duplikat nazwy dostaje kolejny numer.
średni Zobacz kod → Teksty i StringFormatowanie i składanie tekstu
Zestaw przykładów String.format, StringBuilder i bloków tekstu: kwoty po polsku, tabela z równymi kolumnami, skracanie zbyt długich nazw i bezpiecznie zakodowany fragment HTML.
początkujący Zobacz kod →