Regex bez bólu: jak działa wyrażenie regularne i jak je testować online

0
10
Rate this post
Programista przy biurku analizuje kod na laptopie i monitorze
Źródło: Pexels | Autor: Jakub Zerdzicki

Nawigacja:

Po co w ogóle regex: realne zastosowania i rozsądne granice

Regex jako narzędzie do dopasowywania wzorców, nie magia

Wyrażenia regularne, w skrócie regex, służą do jednego konkretnego zadania: dopasowywania wzorców w tekście. Nie generują danych, nie „rozumieją” semantyki, nie są też magicznym skrótem do rozwiązania każdego problemu z tekstem. Traktują ciąg znaków jak liniowy strumień, po którym przesuwają się z lewej do prawej, próbując dopasować określony schemat.

Regex opisuje jak ma wyglądać dopasowany fragment: z jakich znaków ma się składać, w jakiej kolejności, ile razy dane fragmenty mogą się powtórzyć, na jakiej pozycji w ciągu mogą wystąpić. To wszystko. Cała „moc” wynika z bardzo zwartej, ale precyzyjnej składni. Jeśli składnia jest niezrozumiana, regex zaczyna przypominać losowy ciąg symboli. Gdy jest rozumiana – staje się szybkim i przewidywalnym narzędziem.

Kluczowa myśl: regex nie zgaduje twoich intencji. Dopasowuje dokładnie to, co opiszesz. Jeśli wzorzec jest nieprecyzyjny, wyniki też będą nieprecyzyjne. I odwrotnie – prosty, dobrze przemyślany regex może zaoszczędzić godziny ręcznej pracy.

Codzienne zastosowania: od programisty po użytkownika Excela

Regex kojarzy się z backendem i analizą logów, ale w praktyce przydaje się w wielu mało spektakularnych, za to realnych scenariuszach:

  • Przeszukiwanie logów – szybkie wyciąganie linii z konkretnym identyfikatorem żądania, adresem IP, statusem HTTP.
  • Walidacja danych wejściowych – wstępne sprawdzenie formatu e-maila, numeru telefonu, kodu pocztowego, numeru faktury.
  • Masowe zamiany w edytorze kodu – refaktoryzacja nazw metod, zmiana struktury importów, poprawa błędnych prefiksów.
  • Czyszczenie danych z CSV – wyciąganie numerów z opisów, usuwanie zbędnych prefiksów, normalizacja formatów dat.
  • Proste parsowanie logów lub raportów – wyciągnięcie daty, user ID i statusu z surowej linii tekstu.
  • Analiza tekstów w narzędziach typu Notepad++, VS Code, IntelliJ – wyszukiwanie i zastępowanie z użyciem regex.

Przykładowo, administrator systemu musi sprawdzić wszystkie logi z konkretnym użytkownikiem „user123” i błędem 403. Wystarczy regex dopasowujący linie zawierające user123 i 403 w odpowiedniej kolejności, zamiast ręcznego przeglądania setek tysięcy linii.

Gdzie regex działa świetnie, a gdzie zaczyna być nadużywany

Regex świetnie sprawdza się tam, gdzie:

  • Format tekstu jest mniej więcej przewidywalny (np. logi, CSV, stałe raporty).
  • Chodzi o dopasowanie wzorców na poziomie ciągów znaków, a nie zrozumienie struktury dokumentu.
  • Przydatna jest możliwość szybkiej zmiany wzorca bez pisania funkcji w kodzie.
  • Skala danych wymaga automatyzacji (dziesiątki tysięcy wierszy i więcej).

Z kolei regex nie jest dobry lub jest tylko półśrodkiem w takich sytuacjach:

  • Parsowanie złożonych formatów typu HTML, XML, JSON, CSV z cytowaniami – tam bardziej pasują parsery i biblioteki.
  • Zrozumienie kontekstu językowego – np. wyszukiwanie „tylko czasowników” w tekście.
  • Złożone walidacje biznesowe – np. numer PESEL z kontrolą daty urodzenia i sumy kontrolnej (regex może być częścią rozwiązania, ale nie całością).
  • Bardzo długie teksty i złożone wzorce – niektóre silniki mogą mieć problemy z wydajnością, a nawet zrywać działanie aplikacji.

Krótko: regex jest dobry do dopasowywania wzorca w tekstach o znanym kształcie. Jeśli trzeba rozumieć hierarchię, kontekst lub strukturę drzewa (HTML, JSON), lepiej sięgnąć po dedykowany parser, a regex ograniczyć do prostych fragmentów, np. wyciągnięcia ID z atrybutu.

Regex kontra proste funkcje stringowe

Zanim pojawi się impuls „napiszę regex”, warto zestawić to z prostymi operacjami na łańcuchach znaków. Funkcje takie jak split, substring, startsWith, endsWith, indexOf są:

  • czytelniejsze dla mniej zaawansowanych osób w zespole,
  • często szybsze, zwłaszcza dla prostych przypadków,
  • łatwiejsze do testowania krok po kroku.

Regex ma przewagę, gdy warunki zaczynają być kombinacją wielu reguł, np.:

  • „ciąg zaczyna się od INV-, potem 4 cyfry, ewentualnie / i litery A–Z”.
  • „ciąg zawiera adres e-mail, ale nie w cytacie, a w czystym tekście”.

Reguła praktyczna jest prosta: jeśli problem można jasno opisać jako sekwencję prostych kroków na stringu – nie ma przymusu używania regex. Jeżeli jednak warunki zaczynają się mnożyć i rozrasta się ilość warunków if/else, regex bywa bardziej zwięzły i mniej podatny na pominięcie któregoś przypadku.

Ekran laptopa z kodem w debuggerze podczas pracy nad oprogramowaniem
Źródło: Pexels | Autor: Daniil Komov

Składnia od zera: literały, metaznaki i tryby dopasowania

Zwykłe znaki i pierwsze metaznaki

Najprostszy regex to po prostu zwykły tekst, np. wzorzec abc dopasowuje ciąg znaków „abc” gdziekolwiek w tekście. Literały nie mają specjalnego znaczenia – muszą wystąpić dokładnie tak, jak są zapisane.

Sytuacja zmienia się przy metaznakach, które mają specjalne znaczenie:

  • . – kropka dopasowuje dowolny pojedynczy znak (zwykle z wyjątkiem znaku nowej linii).
  • – backslash (ukośnik wsteczny) służy do „ucieczki” znaków specjalnych, tworzy też sekwencje typu d, w itd.
  • * – „0 lub więcej” poprzedzającego elementu.
  • + – „1 lub więcej” poprzedzającego elementu.
  • ? – „0 lub 1” poprzedzającego elementu (element opcjonalny).
  • | – alternatywa, czyli „lub”.
  • ( ) – nawiasy grupujące, o wielu zastosowaniach (grupy, kwantyfikacja, przechwytywanie).

Metaznaki działają na poprzedzający element. Przykładowo:

  • ab* – litera a i „0 lub więcej” liter b; dopasuje „a”, „ab”, „abbb”.
  • (ab)* – „0 lub więcej” powtórzeń całego ciągu „ab”; dopasuje pusty ciąg, „ab”, „abab”.

Dlaczego kropka nie zawsze oznacza kropkę

To jedna z pierwszych pułapek: kropka w regexie jest metaznakiem, a nie dosłowną kropką. Kto próbuje dopasować adres „example.com” wzorcem example.com, dopasuje m.in. „exampleXcom”, „example9com”, bo . oznacza „dowolny znak”.

Jeśli potrzebny jest dosłowny znak (kropka, plus, gwiazdka, nawias okrągły itd.), trzeba go uciec, czyli poprzedzić backslashem:

  • . – prawdziwa kropka,
  • * – prawdziwa gwiazdka,
  • ? – prawdziwy znak zapytania.

Dodatkowo w wielu językach programowania potrzebne jest podwójne uciekanie, bo backslash jest też znakiem specjalnym w stringach. W JavaScripcie ciąg znaków w kodzie będzie wyglądał tak:

// Dopasuj 'example.com'
const regex = /example.com/; // literał regex w JS

// Te same znaki zapisane jako string:
const pattern = "example.com"; // backslash w stringu

Różne warstwy (język programowania, regex) mają własne reguły uciekania znaków. Stąd tak częste wrażenie „podwójnej magii” – bez rozdzielenia tych poziomów trudno ocenić, czy błąd jest w regexie, czy w sposobie jego wstrzyknięcia do kodu.

Flagi: i, m, s, g, u i spółka

Większość implementacji regex obsługuje flagi (tryby), które zmieniają sposób dopasowania. Zestaw flag jest zależny od silnika, ale pewien kanon się powtarza:

  • iignore case, dopasowanie bez rozróżniania wielkości liter („A” = „a”).
  • mmultiline, zmiana działania ^ i $ (początek/koniec linii, nie tylko całego ciągu).
  • ssingleline (czasem „dotall”), kropka . zaczyna dopasowywać również znak nowej linii.
  • gglobal, dopasowanie wielu wystąpień, nie tylko pierwszego (np. w JS).
  • uunicode, dokładniejsze traktowanie znaków Unicode w niektórych silnikach (np. JS).

Przykład z JavaScript:

// szukaj 'test' niezależnie od wielkości liter, we wszystkich wystąpieniach
const regex = /test/gi;

W innych językach flagi przekazuje się inaczej, np. w Pythonie:

import re
pattern = re.compile(r"test", re.IGNORECASE | re.MULTILINE)

To, które flagi są dostępne i jak dokładnie działają (szczególnie u, s, m), zależy od silnika. Stąd tak ważne jest testowanie regex w tym języku lub narzędziu, w którym mają działać, albo w testerze online, który używa tego samego silnika.

Proste przykłady dopasowań

Kilka praktycznych wzorców na start:

  • Wyszukanie fragmentu słowa: mail dopasuje „email”, „gmailek”, „MAIL” (z flagą i).
  • Fragment adresu e-mail: @[a-z]+. – małpa, kilka małych liter, kropka; dopasuje np. „@gmail.”.
  • Ciąg cyfr: [0-9]+ lub krócej d+ (w większości silników – zaraz będzie o wyjątkach).

Na tym etapie reguła jest prosta: najpierw myśl o wzorcu słowami

Laptop z kodem programistycznym obok pluszaka w jasnym pokoju z roślinami
Źródło: Pexels | Autor: Daniil Komov

Klasy znaków i kwantyfikatory: jak precyzyjnie określić „ile i czego”

Klasy predefiniowane: d, w, s i ich pułapki z Unicode

Klasy znaków pozwalają określić, jaki rodzaj znaku ma zostać dopasowany. Najczęściej spotykane klasy predefiniowane to:

  • d – cyfra (zwykle 0–9, ale w trybie Unicode może obejmować inne cyfry, np. arabskie).
  • w – znak „słowa”: litera, cyfra, podkreślnik (w niektórych silnikach: tylko ASCII; w innych: pełny Unicode).
  • s – biały znak: spacja, tabulator, znak nowej linii, powrót karetki itp.
  • D, W, S – negacje powyższych (np. D = „nie cyfra”).

Tu pojawia się często bagatelizowany problem: zachowanie w kontekście Unicode. W niektórych silnikach:

  • w dopasowuje tylko litery łacińskie, cyfry i podkreślnik (np. starsze implementacje JS, niektóre tryby PCRE).
  • w trybie Unicode w może obejmować litery z wielu alfabetów – również polskie znaki: ą, ć, ł itd.

Jeśli regex ma pracować na danych wielojęzycznych, trzeba zweryfikować:

  • czy w faktycznie uznaje polskie litery za litery,
  • Własne klasy znaków: nawiasy kwadratowe i zakresy

    Obok klas predefiniowanych pojawia się drugi, bardzo użyteczny mechanizm: własne klasy znaków. Tworzy się je za pomocą nawiasów kwadratowych:

  • [abc] – „jeden znak: a lub b lub c”.
  • [0-9] – zakres od 0 do 9, odpowiednik prostszego d (w trybie ASCII).
  • [a-zA-Z] – małe lub wielkie litery łacińskie.

Nawiasy kwadratowe oznaczają „dowolny jeden znak z tej listy lub zakresu”. To ważne: [abc] nie dopasuje „abc” jako ciągu, tylko jeden z tych trzech znaków.

Do klasy można też wstawić kilka elementów naraz:

  • [a-z0-9_] – litera, cyfra lub podkreślnik.
  • [.,;] – dokładnie jeden ze znaków: kropka, przecinek lub średnik.

Negacja w klasie: gdy ma się nie dopasować

Umieszczenie ^ na początku nawiasu kwadratowego oznacza negację:

  • [^0-9] – „dowolny znak, który nie jest cyfrą”.
  • [^a-zA-Z] – „cokolwiek, co nie jest literą łacińską”.

Pułapka jest subtelna: ^ ma inne znaczenie w klasie (negacja), a inne poza nią (początek ciągu/linii). Dodatkowo:

  • [^] w niektórych silnikach oznacza „jakikolwiek znak, włącznie z nową linią”.
  • [-abc] – znak minus na początku klasy jest dosłownie minusem, a nie zakresem.

Minus i nawiasy w klasach: kiedy „uciekać”

Kilka znaków ma w klasach specjalne znaczenie i wymaga ostrożności:

  • - – tworzy zakres (np. a-z), ale jeśli jest na początku lub końcu, zwykle traktowany jest dosłownie ([-+], [0-9-]).
  • ] – kończy klasę, więc jeśli ma być dosłownie, trzeba go umieścić na początku (][]) lub uciec (]), w zależności od silnika.
  • ^ – jak wyżej, na początku = negacja, w środku klasy – zwykle znak dosłowny.

To właśnie w klasach widać różnice między dialektami: jedne wymagają uciekania znaku - prawie zawsze, inne są bardziej „wyrozumiałe”. Stąd prosty test w docelowym środowisku jest bezpieczniejszy niż poleganie na intuicji.

Kwantyfikatory: ?, *, + i wersje z nawiasami klamrowymi

Kiedy określone jest „co” (klasa znaków, literał, grupa), trzeba jeszcze zdefiniować „ile razy”. Do tego służą kwantyfikatory:

  • ? – 0 lub 1 raz (opcjonalnie),
  • * – 0 lub więcej razy,
  • + – 1 lub więcej razy,
  • {n} – dokładnie n razy,
  • {n,} – co najmniej n razy,
  • {n,m} – od n do m razy (zwykle włącznie z m).

Kilka konkretnych kombinacji:

  • d{4} – dokładnie cztery cyfry, np. rok w formacie „2024”.
  • [A-Z]{2,3} – dwie lub trzy wielkie litery, np. „PL”, „ENG”.
  • s* – dowolna liczba białych znaków (w tym zero).

Najczęstsza pułapka: kwantyfikator obejmuje tylko „poprzedni element”

Kwantyfikator działa na dokładnie to, co stoi tuż przed nim. Jeśli to pojedynczy znak – obejmuje ten znak, jeśli grupa – całą grupę:

  • ab+ – litera a, a potem 1 lub więcej liter b („ab”, „abb”, „abbbb”).
  • (ab)+ – 1 lub więcej powtórzeń całego ciągu „ab” („ab”, „abab”, „ababab”).

Przy bardziej złożonych wzorcach łatwo przez nieuwagę „skwantyfikować” tylko ostatni fragment zamiast całości. Pomaga czytanie wzorca na głos: „to, co w nawiasie, powtórz x razy”.

Zachłanne kontra nie-zachłanne (leniwe) dopasowania

Domyślnie kwantyfikatory są zachłanne (greedy): biorą możliwie najwięcej znaków, które wciąż pozwalają na ogólne dopasowanie. Dla tekstu:

<tag>treść1</tag> coś <tag>treść2</tag>

wzorzec:

<tag>.*</tag>

zazwyczaj dopasuje:

<tag>treść1</tag> coś <tag>treść2</tag>

czyli „od pierwszego <tag> do ostatniego </tag>”. Dla wielu scenariuszy to zupełnie nie to, o co chodzi.

Wersja nie-zachłanna (leniwa) dodaje ? po kwantyfikatorze:

  • *? – 0 lub więcej, ale jak najmniej,
  • +? – 1 lub więcej, jak najmniej,
  • {n,m}? – od n do m, możliwie najbliżej n.

Dla przykładu HTML:

<tag>.*?</tag>

dopasuje osobno:

<tag>treść1</tag>
<tag>treść2</tag>

Przy parsowaniu struktur z „otwierającym” i „zamykającym” znacznikiem to podstawowe narzędzie. Trzeba jednak zachować dystans: leniwe dopasowania rozwiązują część problemów, ale regex nadal nie jest parserem HTML/XML. To raczej szybki filtr niż pełna walidacja poprawności struktury.

Kotwice: początek i koniec dopasowania

Dobrze zdefiniowane „gdzie” jest tak samo ważne jak „co”. Temu służą kotwice (ang. anchors), które nie dopasowują znaków, tylko określają pozycję:

  • ^ – początek ciągu (lub linii, w trybie wieloliniowym),
  • $ – koniec ciągu (lub linii, w trybie wieloliniowym),
  • A, Z – w niektórych silnikach: bezwarunkowy początek i koniec całego tekstu.

Porównanie na prostym przykładzie:

  • abc – znajdzie „abc” gdziekolwiek, np. w „xxabcxx”.
  • ^abc – dopasuje tylko, jeśli „abc” jest na początku ciągu.
  • abc$ – tylko jeśli „abc” jest na końcu.
  • ^abc$ – cały ciąg musi być dokładnie „abc”.

Tryb wieloliniowy a zachowanie ^ i $

Flaga m (multiline) zmienia interpretację ^ i $. Bez niej:

  • ^ – początek całego tekstu,
  • $ – koniec całego tekstu.

Z nią – początek i koniec każdej linii:

const text = `linia1
linia2
linia3`;

// Bez 'm': dopasuje tylko 'linia1'
text.match(/^linia/m); // ['linia1']

// Z 'm': dopasuje wszystkie linie
text.match(/^linia/gm); // ['linia1', 'linia2', 'linia3']

To częsty powód nieporozumień przy walidacji „całego stringa”: ktoś używa ^ i $ w trybie wieloliniowym, a więc dopuszcza dopasowanie tylko fragmentu każdej linii zamiast pełnego ciągu.

Granice słowa: b i B

Do wyszukiwania całych słów, a nie fragmentów, służy granica słowa b. Działa ona na przejściu między znakiem „słowa” (w) i „nie-słowa” (W lub początek/koniec ciągu).

  • bcatb – dopasuje „cat” jako osobne słowo, ale nie „concatenate”.
  • bPLb – znajdzie „PL” oddzielone spacją, przecinkiem itp.

Negacja, czyli B, dopasowuje miejsce, które nie jest na granicy słowa. Można jej użyć np. do wyszukiwania słowa wyłącznie jako prefiksu lub sufiksu:

  • catB – „cat” z czymś za nim („catering”, „category”), ale nie „cat ”.
  • Bcat – „cat” z czymś przed („educate”), ale nie na początku słowa.

Zależność od w oznacza, że przy Unicode definicja „słowa” może być inna w różnych silnikach. W jednych „słowo” to tylko ASCII + podkreślnik, w innych wchodzą też litery narodowe. Przy przetwarzaniu tekstów w językach innych niż angielski opłaca się zrobić kilka testów, zamiast zakładać, że b zawsze zadziała intuicyjnie.

Grupy: porządkowanie, kwantyfikacja i przechwytywanie

Nawiasy okrągłe ( ) pełnią w regexie kilka ról naraz:

  • grupują elementy do wspólnej kwantyfikacji,
  • definiują alternatywy,
  • opcjonalnie przechwytują dopasowany fragment do późniejszego użycia.

Najprostsze użycie to pogrupowanie kilku znaków, aby zastosować kwantyfikator do całości:

  • (ab){3} – „ababab”.
  • (d{2}-){3}d{2} – wzorzec jak „12-34-56-78”.

Grupy przechwytujące i numerowane odwołania

Domyślnie grupa jest przechwytująca: silnik zapamiętuje jej zawartość, aby można było się do niej odwołać:

  • (d{4})-(d{2})-(d{2}) – trzy grupy: rok, miesiąc, dzień.

W wielu językach można je potem wykorzystać:

const regex = /(d{4})-(d{2})-(d{2})/;
const m = "2024-03-15".match(regex);
// m[0] = '2024-03-15'
// m[1] = '2024'
// m[2] = '03'
// m[3] = '15'

Wewnątrz samego wzorca używa się backreferencji:

  • (w+)s+1 – dwa identyczne kolejne słowa, np. „very very”.

1 oznacza „to samo, co dopasowała pierwsza grupa”. To przydatne przy szukaniu powtórzeń czy walidacji symetrycznych struktur typu „(<tag>)(to samo co wcześniej)”, choć dla skomplikowanego HTML-u jest to mocno ograniczone.

Grupy nieprzechwytujące (?:…)

Nie każda grupa musi być przechwytywana. Jeśli potrzebne jest tylko zgrupowanie logiki (np. do kwantyfikacji czy alternatywy), a nie przechwytywanie, lepiej użyć grupy nieprzechwytującej:

  • (?:ab)+ – jedno lub więcej powtórzeń „ab”, bez tworzenia oddzielnego wpisu w tablicy dopasowań.
  • (?:jpg|png|gif) – alternatywa rozszerzeń pliku.

Jest to szczególnie praktyczne, gdy z wzorca korzysta się w kodzie i później używa grup po numerze. Nadmiar przechwytywanych grup zmienia indeksy i utrudnia utrzymanie kodu. Świadome stosowanie (?:...) porządkuje strukturę i ogranicza „szum”.

Alternatywy: kilka ścieżek dopasowania naraz

Pionowa kreska | wprowadza alternatywę – działa jak logiczne „lub”. W połączeniu z grupami daje możliwość zdefiniowania kilku wariantów jednego miejsca wzorca:

  • kot|pies – dopasuje „kot” albo „pies”.
  • (jpg|png|gif) – jedno z trzech rozszerzeń.

Bez nawiasów alternatywa dotyczy tylko najbliższych fragmentów po lewej i prawej stronie. Zmiana miejsca nawiasów całkowicie zmienia znaczenie:

  • https?|ftp – „http”, „https” lub „ftp”.
  • (https?|ftp):// – „http://”, „https://” lub „ftp://”.
  • https?://|ftp:// – formalnie to samo, ale mniej czytelne.

Przy dłuższych wzorcach lepiej nie oszczędzać nawiasów. Osoba, która za pół roku będzie ten regex czytać (często ty sam), szybciej zrozumie logikę z wyraźnymi grupami niż z jedną długą linią pełną |.

Grupowanie bez alternatywy: porządek w długich wzorcach

Nawet jeśli nie ma |, grupy pomagają uporządkować konstrukcję:

  • (?:+48s?)?d{9} – opcjonalny prefiks +48 (z ewentualną spacją), a potem dziewięć cyfr.
  • (?:d{1,3}.){3}d{1,3} – „coś jak adres IPv4”, trzy grupy „liczb + kropka”, a potem liczba bez kropki.

W takich miejscach grupa nie jest potrzebna do przechwytywania, tylko do nadania struktury. To częsty powód, dla którego doświadczone osoby odruchowo używają (?:...) zamiast zwykłego (...).

Zaawansowane grupy: nazwy, komentarze i conditionale

Bardziej rozbudowane silniki (np. PCRE, .NET, Python re z dodatkami) udostępniają dodatkowe konstrukcje grup, które poprawiają czytelność albo umożliwiają bardziej złożoną logikę.

Grupy nazwane: łatwiejsze odwołania

Numerowane grupy przy większym wzorcu szybko przestają być wygodne. Stąd grupy nazwane – zamiast 1 można użyć czytelnej etykiety. Składnia zależy od silnika, najczęściej:

  • tworzenie: (?<nazwa>...) lub (?<=...) w .NET,
  • odwołanie w wzorcu: k<nazwa> lub k'nazwa',
  • w JS (od ES2018): (?<year>d{4})-(?<month>d{2})-(?<day>d{2}).

Przykład w JavaScript:

const re = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/;
const m = "2024-03-15".match(re);
// m.groups.year  === '2024'
// m.groups.month === '03'
// m.groups.day   === '15'

W długim wzorcu różnica między 1, 2 a k<year> i k<month> jest znacząca. Kosztem drobnie dłuższego zapisu zyskuje się mniejszą podatność na błędy przy późniejszej modyfikacji regexa.

Grupy z komentarzem (inline)

Komentarze wklejone bezpośrednio w regex ułatwiają debugowanie złożonych wzorców. Najczęściej spotykana forma to:

  • (?# komentarz) – komentarz wstawiony w dowolnym miejscu wzorca.

Przykład:

d{2}(?# dzień)-d{2}(?# miesiąc)-d{4}(?# rok)

Dodatkowo, wiele implementacji (np. PCRE, .NET, Python) wspiera tryb „verbose” (np. flaga x lub re.VERBOSE), w którym spacje i nowe linie są ignorowane, a # rozpoczyna komentarz do końca linii. To pozwala pisać regexy „w kolumnie”, podobnie jak kod:

pattern = r"""
(?P<year>d{4})  # rok
-                 # separator
(?P<month>d{2})  # miesiąc
-                 # separator
(?P<day>d{2})    # dzień
"""
re.compile(pattern, re.VERBOSE)

Taki zapis bywa dłuższy, ale przydaje się, gdy wzorzec ma żyć w projekcie latami, a nie jednorazowo „na kolanie”.

Conditionale: dopasowanie zależne od wcześniejszej grupy

Niektóre silniki (PCRE, .NET) udostępniają warunkowe fragmenty wzorca, reagujące na to, czy wcześniejsza grupa się dopasowała. Składnia bywa egzotyczna:

(?(<nazwaLubNumerGrupy&gt)wzorzec_jeśli_tak|wzorzec_jeśli_nie)

Przykład w stylu PCRE:

(?<open><[A-Za-z]+>)?   # opcjonalny znacznik otwierający
(?(open) .*?</[A-Za-z]+> | w+)  # jeśli był otwierający, oczekuj zamykającego, inaczej zwykłego słowa

To narzędzie specjalistyczne – ma sens tam, gdzie regex faktycznie musi odzwierciedlić różne gałęzie logiki, ale łatwo przejechać się na czytelności. Przy prostym dopasowaniu tekstu lub walidacji formularza zwykle jest przerostem formy nad treścią.

Lookaround: spójrz obok, ale nie zjadaj znaków

Grupy lookaround umożliwiają sprawdzenie, co znajduje się przed lub za dopasowaniem, bez włączania tego w sam wynik. To coś w rodzaju „warunku brzegowego”.

Lookahead (spojrzenie wprzód)

Lookahead patrzy w prawo od bieżącej pozycji:

  • (?=...) – pozytywny lookahead (musi pasować),
  • (?!...) – negatywny lookahead (musi nie pasować).

Przykłady:

  • d+(?=px) – liczby tuż przed „px”, np. „12” w „12px”, ale nie „12” w „12 pt”.
  • bw+(?!s+Ltd)b – słowo, po którym nie stoi „Ltd”.

Dla użytkownika końcowego istotne jest to, że dopasowana liczba lub słowo nie zawiera tego, co sprawdza lookahead. To pozwala np. odczytać „12” bez „px”.

Lookbehind (spojrzenie wstecz)

Lookbehind działa symetrycznie, ale w lewo:

  • (?<=...) – pozytywny lookbehind,
  • (?<!...) – negatywny.

Przykłady:

  • (?<=<h1>).*?(?=</h1>) – zawartość nagłówka <h1>...</h1> bez znaczników.
  • (?<!w)cat – „cat”, którego bezpośrednio nie poprzedza litera/cyfra/podkreślnik.

Lookbehind ma istotne ograniczenie: w wielu silnikach (szczególnie starszych) musi mieć stałą długość. Dopuszczalne jest np. (?<=abc), ale już nie zawsze (?<=a+). Nowe wersje silników (np. nowsze JS, PCRE) radzą sobie też z „lookbehind o zmiennej długości”, ale to nie jest jeszcze uniwersalny standard.

Typowe zastosowania lookaround

Lookaround jest wygodne tam, gdzie trzeba „zobaczyć kontekst”, ale nie chcemy go przechwytywać ani „zjadać”:

  • wycinanie tekstu między znacznikami, bez samych znaczników,
  • walidacja „hasło musi zawierać cyfrę i wielką literę” (kilka lookaheadów na różne warunki),
  • wyszukiwanie fragmentów niepoprzedzonych/podążanych przez określony układ (np. liczby niepoprzedzone znakiem $).

Jeżeli problem da się rozwiązać prostym wzorcem bez lookaround i bez wyrafinowanej logiki, na ogół jest to czytelniejsze. Lookaround ma sens, gdy naprawdę oszczędza dodatkowe post‑przetwarzanie w kodzie.

Backtracking i wydajność: kiedy regex potrafi „zawiesić” aplikację

Silniki regex (szczególnie te popularne w językach skryptowych) działają zazwyczaj w trybie backtracking. Oznacza to, że przy niedopasowaniu cofają się i próbują innych ścieżek dopasowania. Zwykle działa to świetnie, ale na źle skonstruowanym wzorcu może prowadzić do eksplozywnego czasu działania.

Wzorce podatne na katastrofalny backtracking

Klasyczny przykład:

^(a+)+$

Na krótkich stringach testowych wygląda niewinnie. Ale na długim ciągu kilkudziesięciu tysięcy „a”, który ostatecznie nie pasuje, silnik może próbować ogromnej liczby kombinacji powtórzeń wewnętrznych. W praktyce:

  • aplikacja „wisi” na jednym zapytaniu,
  • CPU skacze do 100% dla jednego wątku,
  • użytkownik widzi timeout lub brak odpowiedzi.

Podobne ryzyko niosą wzorce w rodzaju:

  • (.*)+,
  • (.+)+,
  • (w+)*w* na długich ciągach bez wyraźnej granicy.

W uproszczeniu: zagnieżdżone, zachłanne kwantyfikatory na szerokich klasach znaków (.*, .+, w+) bez kotwic lub naturalnych ograniczeń to prośba o kłopoty.

Jak ograniczać eksplozję backtrackingu

Kilka prostych praktyk redukuje ryzyko:

  • zamiast .* używać bardziej precyzyjnych klas znaków, np. [^n]* dla „do końca linii”,
  • unikać zagnieżdżenia „szerokich” kwantyfikatorów ((.*)+ → wystarczy jedno .*),
  • kotwiczyć wzorzec (^, $, granice słów) tam, gdzie to możliwe,
  • testować najdłuższe realne dane, a nie tylko krótkie próbki.

Zwłaszcza przy regexach uruchamianych na danych od użytkowników (logi, pola formularzy, treści raportów) prościej zainwestować chwilę w testy wydajności niż debugować później losowe przywieszenia.

Różne dialekty regex: to samo wyrażenie, różne zachowanie

Powszechne przekonanie „regex to regex” jest tylko częścią prawdy. Wspólny jest ogólny koncept i spora część składni, ale szczegóły zależą od silnika, który stoi pod spodem. To tłumaczy, czemu wzorzec skopiowany z forum niekoniecznie zadziała w Pythonie, JS, .NET czy w edytorze tekstu.

Popularne silniki i ich cechy

W praktyce najczęściej spotyka się:

  • PCRE (Perl Compatible Regular Expressions) – wiele narzędzi linuksowych, PHP (preg_*), niektóre edytory; bardzo bogaty zestaw funkcji.
  • JavaScript (ECMAScript) – przeglądarki, Node.js; historycznie uboższy, ale nowsze standardy dodały lookbehind, flagę u, grupy nazwane.
  • .NET – Regex z własnymi rozszerzeniami (conditionale, bogate wsparcie nazwanych grup, tryby kulturowe).
  • Python re – zbliżony do PCRE, ale nie identyczny; część konstrukcji jest inna lub nieobsługiwana.
  • Rust/Go – często używają silników nastawionych bardziej na deterministyczną wydajność niż pełnię bajerów (np. brak niektórych form backreferencji czy zaawansowanego lookbehind).

To, że „gdzieś w internecie” pokazano zaawansowany wzorzec, zwykle oznacza: „zadziała w PCRE lub w konkretnym narzędziu”, ale nie ma gwarancji dla innych środowisk.

Różnice w obsłudze Unicode

Unicode jest jednym z głównych źródeł niespodzianek:

  • w w jednym silniku obejmuje tylko ASCII + _, w innym także litery narodowe,
  • b przy tekstach z akcentowanymi literami bywa niespójne między środowiskami,
  • Najczęściej zadawane pytania (FAQ)

    Do czego w praktyce używa się wyrażeń regularnych?

    Najczęstsze zastosowania to przeszukiwanie i filtrowanie tekstu, np. logów systemowych, plików CSV czy raportów. Regex pozwala szybko wyłuskać linie z konkretnym identyfikatorem, adresem IP, datą lub kodem błędu, zamiast ręcznego przeklikiwania tysięcy wierszy.

    Często używa się go też do wstępnej walidacji formatu danych (e-mail, numer faktury, kod pocztowy), masowych zamian w edytorach kodu (refaktoryzacja nazw, zmiana importów) oraz prostego parsowania tekstu: wyciągania dat, ID użytkownika czy statusu z jednej linii logu.

    Kiedy lepiej nie używać regex, tylko innych narzędzi?

    Regex jest kiepskim wyborem do parsowania złożonych, zagnieżdżonych formatów jak HTML, XML czy JSON. Tam bezpieczniej użyć bibliotek i parserów, które rozumieją strukturę dokumentu (tagi, atrybuty, drzewo DOM), zamiast próbować „odgadnąć” ją wzorcem tekstowym.

    Nie sprawdzi się też przy zadaniach wymagających zrozumienia kontekstu językowego (np. wyszukiwanie części mowy) czy skomplikowanych reguł biznesowych, gdzie sam format to za mało. Regex może być jednym z elementów rozwiązania, ale nie powinien zastępować logiki w kodzie.

    Regex czy zwykłe funkcje stringowe – co wybrać?

    Jeśli problem da się opisać prostym ciągiem operacji typu startsWith, endsWith, split, substring czy indexOf, zwykle lepiej zacząć od nich. Są czytelniejsze dla zespołu, łatwiej je debugować krok po kroku i często są szybsze.

    Regex zaczyna mieć przewagę, gdy warunków jest dużo i zaczynają się mnożyć instrukcje if/else. Przykład: „ciąg zaczyna się od INV-, potem ma 4 cyfry, opcjonalnie ukośnik i litery A–Z”. Taki zestaw łatwiej utrzymać jako jeden dobrze opisany regex niż kilka rozgałęzień w kodzie.

    Jak działają podstawowe metaznaki w regex (., *, +, ?)?

    Najczęściej używane metaznaki mają konkretne, dość proste znaczenie: kropka . oznacza dowolny pojedynczy znak (zwykle poza znakiem nowej linii), gwiazdka * – „0 lub więcej” poprzedzającego elementu, plus + – „1 lub więcej”, a znak zapytania ? – „0 lub 1” (element opcjonalny).

    Kluczowa pułapka: metaznak działa na poprzedzający element. ab* to litera a i „0 lub więcej” liter b (dopasuje a, ab, abbb), a (ab)* to „0 lub więcej” całego ciągu ab (dopasuje pusty ciąg, ab, abab). Błędne dobranie nawiasów zmienia sens wzorca.

    Dlaczego kropka w regex nie zawsze dopasowuje dosłowną kropkę?

    W składni regex kropka . jest metaznakiem i dopasowuje „dowolny znak”. Dlatego wzorzec example.com pasuje nie tylko do example.com, ale też np. exampleXcom czy example9com, bo środkowy znak może być czymkolwiek.

    Aby dopasować prawdziwą kropkę, trzeba ją „uciec” backslashem: .. Dodatkowo w wielu językach programowania backslash jest też znakiem specjalnym w stringach, więc w kodzie trzeba zapisać go podwójnie, np. w JavaScripcie: "example.com". Część błędów wynika właśnie z pomylenia poziomu regexu z poziomem stringa w języku.

    Jak działają flagi regex takie jak i, m, s, g, u?

    Flagi zmieniają sposób dopasowania bez zmiany samego wzorca. Najczęstsze to: i (ignorowanie wielkości liter, „A” = „a”), m (tryb wieloliniowy – ^ i $ odnoszą się do początku/końca linii), s lub „dotall” (kropka . dopasowuje również znak nowej linii), g (dopasowanie wielu wystąpień, np. w JS) oraz u (pełniejsze wsparcie Unicode w części silników).

    To, jakie flagi są dostępne i jak działają, zależy od implementacji (JavaScript, Python, Java itd.). Typowy przykład: /test/gi w JS dopasuje wszystkie wystąpienia słowa „test” niezależnie od wielkości liter, a w Pythonie podobny efekt da re.compile(r"test", re.IGNORECASE | re.MULTILINE) – inne API, ta sama idea.

    Jak bezpiecznie testować wyrażenia regularne online?

    Narzędzia online (regex101, regexr, Rubular i inne) pozwalają szybko zobaczyć, co dokładnie dopasowuje wzorzec, jak działają grupy i kwantyfikatory oraz gdzie wzorzec „przestrzela” lub pomija przypadki brzegowe. To wygodne środowisko do eksperymentów bez kompilowania kodu.

    Trzeba jednak uważać na dwa aspekty. Po pierwsze, wybrać w narzędziu silnik zgodny z językiem, którego używasz (np. PCRE vs JavaScript), bo szczegóły składni i flag potrafią się różnić. Po drugie, nie wrzucać tam wrażliwych danych produkcyjnych – lepiej zanonimizować fragmenty logów lub zbudować minimalne, reprezentatywne przykłady.