Przejdź do treści

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.

JAVA WyrazeniaRegularne.java 130 linii Pobierz .java
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

  1. Klasa publiczna narzuca tu nazwę pliku, więc zapisz kod jako WyrazeniaRegularne.java i uruchom go poleceniem java WyrazeniaRegularne.java. Wydruk przechodzi przez siedem sekcji, a niemal każda kończy się celowo źle: wyjątkiem, wynikiem false albo po cichu okrojonym tekstem — chodzi o to, żeby zobaczyć porażkę z bliska, a nie samą ścieżkę szczęśliwą.
  2. Do szukania fragmentów służy find() w pętli, opakowane tutaj w znajdzWszystkie(WZORZEC, tekst), które zwraca List<String> dopasowań. matches() daje true dopiero wtedy, gdy wzorzec pokrywa cały tekst od pierwszego do ostatniego znaku, a lookingAt() — gdy pokrywa sam początek.
  3. Własne wzorce trzymaj tak samo jak tutaj: w polu private static final Pattern na poziomie klasy. Sam Pattern jest niezmienny i możesz go dzielić między wątkami, ale Matcher pamięta stan ostatniego dopasowania, więc każde wyszukiwanie potrzebuje świeżego, tworzonego przez wzorzec.matcher(tekst).
  4. Fragmenty odczytasz po nazwie: szukacz.group("numer"). Grupa obcy pochł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, zwraca null; literówka w nazwie grupy rzuca natomiast IllegalArgumentException. Wzorzec EMAIL celowo 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.compile parsuje wzorzec i buduje z niego drzewo obiektów, dlatego w tym skrypcie wzorce są stałymi. Metody String.matches, String.replaceAll i String.replaceFirst powtarzają 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 jest String.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_CLASS naprawia \w, \b i \s, ale przy okazji rozciąga \d na cyfry wszystkich pism — z tą flagą wzorzec kodu pocztowego przyjąłby zapis cyframi arabsko-indyjskimi. Dlatego KOD_POCZTOWY i TELEFON zostają przy ASCII, a flagę dostaje wyłącznie wzorzec dzielący tekst na słowa. Tak samo działa CASE_INSENSITIVE: bez UNICODE_CASE zna tylko pary z zakresu a-z, więc „Ł” i „ł” są dla niego dwiema różnymi literami.
  • Pattern.quote i Matcher.quoteReplacement to dwie różne rzeczy i zwykle potrzebujesz obu — pierwsza unieszkodliwia wzorzec, druga tekst zamiennika. Po stronie zamiennika $1 wskazuje grupę, więc $ przed cyfrą bez odpowiadającej grupy rzuca IndexOutOfBoundsException, a przed literą albo na końcu napisu — IllegalArgumentException. Gorszy jest backslash, bo znika po cichu: ścieżka C:\dane\raport wstawiona wprost daje C:daneraport bez śladu błędu. Nie zastępuj też Pattern.quote ręcznym doklejaniem \Q i \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 demie StackOverflowError jest łapany tylko po to, żeby wydruk dobiegł do końca; w normalnym kodzie błędów tej klasy się nie przechwytuje.
#regex #polskie znaki #unicode #parsowanie #wydajność

Podobne skrypty