Wyrażenia regularne pozwalają jedną linijką sprawdzić, czy ktoś wpisał w formularzu poprawny kod pocztowy, NIP albo telefon, i wyciągnąć takie dane z tysięcy linii tekstu. Ten sam wzorzec potrafi jednak działać inaczej w JavaScripcie, w Pythonie i w poleceniu grep. Każdy z dziewięciu gotowych wzorców z tego poradnika uruchomiliśmy w Node.js 22 i Pythonie 3.14 na przypadkach poprawnych i błędnych, a różnice dialektów sprawdziliśmy też w GNU grep. Sprawdziliśmy też wzorzec napisany przez model językowy. Wyglądał poprawnie, a przepuścił numer telefonu 000 000 000.
Co to są wyrażenia regularne
Wyrażenie regularne, w skrócie regex, to wzorzec opisujący, jak ma wyglądać tekst. Wzorzec ^\d{2}-\d{3}$ znaczy: na początku dwie cyfry, potem myślnik, potem trzy cyfry i koniec. Pasuje do “00-950”, a do “00950” już nie.
W praktyce regex służy do trzech rzeczy:
- sprawdzania danych z formularza, zanim trafią do bazy,
- wyszukiwania w plikach, logach i dokumentach, na przykład wszystkich kodów pocztowych w eksporcie z systemu,
- zamiany tekstu według wzoru, na przykład dat z formatu 05.10.2026 na 2026-10-05.
Regex obsługuje prawie każdy język programowania, wiele edytorów kodu i narzędzia wiersza poleceń. Składnia jest w nich podobna, ale nie identyczna, i właśnie różnice powodują najwięcej błędów.
Składnia wyrażeń regularnych: ściąga
Większość wzorców składa się z kilkunastu elementów. Tabela pokazuje te, których używamy w dalszej części tekstu.
| Zapis | Znaczenie | Przykład | Pasuje do |
|---|---|---|---|
. | dowolny znak | k.t | kot, kit, k9t |
\d | cyfra | \d\d | 42 |
\w | litera, cyfra albo podkreślnik | \w+ | nazwa_pliku |
\s | biały znak (spacja, tabulator) | a\sb | a b |
[abc] | jeden ze znaków w nawiasie | [aeo] | a, e albo o |
[^abc] | dowolny znak poza wymienionymi | [^0-9] | każdy znak, który nie jest cyfrą |
[0-9], [a-z] | zakres znaków | [A-Z] | wielka litera bez polskich znaków |
* | zero lub więcej razy | ab* | a, ab, abbb |
+ | raz lub więcej | \d+ | 7, 2026 |
? | zero lub jeden raz | kolou?r | kolor, kolour |
{n}, {n,m} | dokładnie n razy, od n do m razy | \d{2,4} | 26, 2026 |
^, $ | początek i koniec tekstu | ^\d+$ | tekst złożony wyłącznie z cyfr |
\b | granica słowa | \bkot\b | kot, ale nie kotlet |
(...) | grupa, którą można potem wyciągnąć | (\d{2})\.(\d{2}) | 05.10, grupy: 05 i 10 |
(?:...) | grupa bez zapamiętywania | (?:tak|nie) | tak albo nie |
| | albo | kot|pies | kot albo pies |
(?=...) | dalej musi stać… (lookahead) | \d+(?= zł) | 120 w tekście “120 zł” |
(?<=...) | wcześniej musi stać… (lookbehind) | (?<=PL)\d+ | cyfry po “PL” |
\. | znak specjalny jako zwykły znak | \d+\.\d+ | 7.5 |
Dwa znaki z tabeli odpowiadają za większość błędów początkujących. Pierwszy to kropka: bez ukośnika przed nią oznacza dowolny znak, więc wzorzec 05.10.2026 pasuje też do “05-10-2026”. Drugi to brak ^ i $. Bez nich \d{2}-\d{3} znajdzie kod pocztowy wewnątrz “100-9500” i uzna pole formularza za poprawne.
Frontend Master 2026 · Kodożercy
Frontend dla osób, które chcą pisać kod, nie tylko prompty
Frontend Master 2026 to pakiet, który daje Ci fundamenty potrzebne w pierwszej pracy frontendowej. HTML, CSS, JavaScript, Git – polskie wideo, projekty, ścieżka od początku do deploy.
Zobacz program kursów →
Wyrażenia regularne: przykłady gotowych wzorców po polsku
Każdy wzorzec poniżej sprawdza cały tekst pola, dlatego zaczyna się od ^ i kończy na $. Do wyszukiwania w dłuższym tekście usuń ^ i $, a granice dobierz do formatu. \b zadziała tylko wtedy, gdy dopasowanie zaczyna się i kończy literą albo cyfrą, więc przy numerze z +48 zawiedzie. Wszystkie wzorce przetestowaliśmy na tych samych przykładach w JavaScripcie i Pythonie, a wyniki były w obu językach identyczne.
| Co sprawdzamy | Wzorzec | Przyjmuje | Odrzuca |
|---|---|---|---|
| kod pocztowy | ^\d{2}-\d{3}$ | 00-950, 31-422 | 00950, 0-950, 00 950 |
| telefon (po usunięciu spacji i myślników) | ^(?:\+48|0048)?[1-9]\d{8}$ | 512384907, +48512384907, 0048512384907 | 012384907, 51238490 |
| NIP | ^(?:\d{10}|\d{3}-\d{3}-\d{2}-\d{2}|\d{3}-\d{2}-\d{2}-\d{3})$ | 5250000000, 525-000-00-00, 525-00-00-000 | 525000000, PL5250000000 |
| PESEL (tylko format) | ^\d{11}$ | 90010112345 | 9001011234, 9001O112345 |
| data DD.MM.RRRR | ^(?:0[1-9]|[12]\d|3[01])\.(?:0[1-9]|1[0-2])\.\d{4}$ | 05.10.2026, 31.12.1999 | 5.10.2026, 32.01.2026, 05.13.2026 |
| data RRRR-MM-DD | ^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$ | 2026-10-05 | 2026-1-5, 2026-13-01 |
| godzina GG:MM | ^(?:[01]\d|2[0-3]):[0-5]\d$ | 09:00, 23:59 | 24:00, 9:00, 12:60 |
| e-mail “wystarczająco dobry” | ^[^\s@]+@[^\s@]+\.[^\s@]+$ | jan.kowalski@firma.pl | jan.kowalski@firma, @firma.pl |
| hasło: min. 8 znaków i cyfra | ^(?=.*\d).{8,}$ | zielony7las | zielonylas, abc1 |
Kod pocztowy i numer telefonu
Kod pocztowy to najprostszy przypadek: ^\d{2}-\d{3}$. Odrzuca kod bez myślnika i ze spacją. Jeśli chcesz je przyjmować, popraw dane przed sprawdzeniem, a nie wzorzec.
Z telefonem tak właśnie postępujemy. Ludzie wpisują numer na kilka sposobów: “512 384 907”, “512-384-907”, “+48 512 384 907”. Zamiast jednego skomplikowanego wzorca najpierw usuń spacje i myślniki, a potem sprawdź wynik. Numer krajowy ma według rozporządzenia w sprawie planu numeracji krajowej dziewięć cyfr, a pierwsza z nich jest od 1 do 9. Stąd [1-9]\d{8}. Kod kraju to 48, więc z przodu dopuszczamy +48 albo 0048.
const tel = '+48 512-384 907'.replace(/[\s-]/g, '');
/^(?:\+48|0048)?[1-9]\d{8}$/.test(tel); // true
NIP i PESEL: regex sprawdza tylko format numeru
Wyrażenie regularne nie policzy cyfry kontrolnej, więc numer z literówką w środku przejdzie test. Według biznes.gov.pl NIP ma 10 cyfr, a ostatnia jest cyfrą kontrolną. Numer należy podawać bez odstępów i myślników, ale na fakturach wciąż widać zapisy z myślnikami, dlatego wzorzec przyjmuje oba popularne układy.
PESEL ma 11 cyfr. Według Ministerstwa Cyfryzacji zawiera datę urodzenia, numer porządkowy, oznaczenie płci i liczbę kontrolną. Wzorzec ^\d{11}$ przepuści też “00000000000”, choć miesiąc 00 nie istnieje. Do weryfikacji numeru potrzebny jest osobny kawałek kodu liczący cyfrę kontrolną. Regex może tylko wstępnie odsiać wpisy, które na pewno są złe.
Data, e-mail i hasło
Wzorzec daty pilnuje zakresów: dzień od 01 do 31, miesiąc od 01 do 12. Kalendarza nie zna, więc przepuszcza “31.02.2026”. Datę po sprawdzeniu formatu przelicz funkcją języka, która zna długość miesięcy.
Reguły adresu e-mail są tak rozbudowane, że wzorzec obejmujący wszystkie przypadki byłby długi i trudny w utrzymaniu. Wersja “wystarczająco dobra” ^[^\s@]+@[^\s@]+\.[^\s@]+$ sprawdza tylko, czy jest małpa, kropka w domenie i brak spacji. Prawdziwym testem adresu jest wiadomość z linkiem potwierdzającym.
Wzorzec hasła pokazuje lookahead w praktyce. (?=.*\d) sprawdza, czy gdzieś dalej jest cyfra, nie zużywając przy tym znaków, a .{8,} pilnuje długości.
Wyrażenia regularne w JavaScript, Pythonie i grep: różnice
Te same wzorce dały w JavaScripcie i Pythonie identyczne wyniki. Rozjazdy wyszły dopiero przy polskich literach, lookbehindzie i w poleceniu grep.
| Sprawdzenie | JavaScript (Node 22) | Python 3.14 | GNU grep -E |
|---|---|---|---|
\w+ na słowie “żółw” | nie pasuje, także z flagą u | pasuje | zależy od ustawień języka systemu |
lookbehind zmiennej długości (?<=cena:\s*)\d+ | działa | błąd: wzorzec musi mieć stałą długość | brak lookbehindu |
\d | tylko cyfry 0-9 | cyfry dziesiętne Unicode (np. arabsko-indyjskie), z flagą re.ASCII tylko 0-9 | brak, użyj [0-9] |
JavaScript
W JavaScripcie wzorzec piszesz między ukośnikami: /^\d{2}-\d{3}$/.test('00-950'). \w obejmuje tylko litery łacińskie bez polskich znaków. Do polskich liter użyj \p{L} z flagą u: /^\p{L}+$/u.test('żółw') zwraca true. Lookbehind jest opisany w ściądze MDN, a w naszym teście działał także w wersji zmiennej długości.
Pułapka, na którą trafia prawie każdy: wzorzec z flagą g zapamiętuje miejsce, w którym skończył. Dwa wywołania test() na tym samym tekście dały u nas kolejno true i false. Do sprawdzania pól formularza flagi g nie używaj.
Regex przydaje się też przy testach API. W Postmanie i n8n sprawdza się nim, czy pole w odpowiedzi ma oczekiwany format.
Python
W Pythonie wzorce zapisuj jako surowe napisy z literą r przed cudzysłowem, na przykład r'\d{2}-\d{3}'. Dzięki temu Python nie przerabia ukośników po swojemu, co zaleca dokumentacja modułu re.
Najważniejsza różnica dotyczy funkcji. Funkcja re.match() sprawdza tylko początek tekstu, więc re.match(r'\d{2}-\d{3}', '00-9500') zwraca dopasowanie. Do pól formularza służy re.fullmatch(), która wymaga zgodności całego tekstu i ten sam przypadek odrzuca. Wszystkie dopasowania z dłuższego tekstu zwraca re.findall().
\w w Pythonie 3 obejmuje polskie litery. Lookbehind musi mieć stałą długość: (?<=cena: ) działa, a (?<=cena:\s*) kończy się błędem “look-behind requires fixed-width pattern”.
grep
GNU grep zna trzy składnie: podstawową, rozszerzoną (grep -E) i zgodną z Perlem (grep -P). W dwóch pierwszych nie ma \d. Podręcznik zastrzega, że zachowanie przy nieznanym znaku po ukośniku jest nieokreślone.
W naszym teście GNU grep 3.0 wywołany jako grep -E '\d{2}-\d{3}' nie znalazł kodu “85-005”. Znalazł za to linię “kod dd-ddd”, bo potraktował \d jak literę d. Bezpieczne zapisy to [0-9] albo [[:digit:]]:
grep -oE '[0-9]{2}-[0-9]{3}' adresy.txt
Przełącznik -o wypisuje tylko dopasowane fragmenty zamiast całych linii. Więcej poleceń do pracy z plikami znajdziesz w ściądze komend Linuxa.
Regex z AI: jak sprawdzić wzorzec napisany przez model
Model językowy napisze wyrażenie regularne w kilka sekund, ale wzorzec trzeba sprawdzić na przypadkach brzegowych, zanim trafi do kodu. Przekonaliśmy się o tym w teście.
Poprosiliśmy model przez Codex CLI trzy razy o to samo: wzorzec w JavaScripcie do polskiego numeru telefonu, z prefiksem +48 albo bez, ze spacjami albo myślnikami. Dwa razy dostaliśmy /^(?:\+48[ -]?)?\d(?:[ -]?\d){8}$/, raz wariant zapisany inaczej, ale działający tak samo. Na zwykłych numerach wzorzec działał bez zarzutu, a na brzegowych pokazał trzy problemy:
- przyjął “000 000 000”, choć numer krajowy nie może zaczynać się od zera,
- przyjął “1-2-3-4-5-6-7-8-9”, bo pozwala na myślnik po każdej cyfrze,
- odrzucił “0048 512 384 907”, czyli zapis z międzynarodowym prefiksem, który część osób wpisuje.
Żaden z tych błędów nie wyjdzie, jeśli sprawdzisz wzorzec na dwóch poprawnych numerach. Dlatego przy wzorcu od AI:
- Opisz format słowami i podaj przykłady. Przy okazji wypisz, co ma być odrzucone, a nie tylko, co ma przejść.
- Przygotuj listę przypadków brzegowych sam. Pusty tekst, same zera, spacje na końcu, zbyt długi i zbyt krótki numer, inne prefiksy. Lista od modelu bywa dopasowana do jego własnego wzorca.
- Uruchom wzorzec na tej liście. Kilka linijek wystarczy:
const re = /^(?:\+48|0048)?[1-9]\d{8}$/;
const dobre = ['512384907', '+48512384907'];
const zle = ['000000000', '51238490', '+4851238490'];
console.log(dobre.every(s => re.test(s)), zle.some(s => re.test(s))); // true false
- Poproś model o wyjaśnienie wzorca element po elemencie i porównaj je z tabelą składni. Jeśli opisu nie rozumiesz, nie wklejaj wzorca do projektu.
- Sprawdź, czy wzorzec nie ma zagnieżdżonych powtórzeń, o czym w następnej sekcji.
Jak precyzyjnie opisywać zadanie modelowi, pokazujemy w przykładach promptów. W Claude Code można poprosić agenta, żeby od razu dopisał test z przypadkami brzegowymi i go uruchomił.
ReDoS: kiedy wyrażenie regularne zawiesza serwer
ReDoS (Regular expression Denial of Service) to atak, w którym spreparowany tekst zmusza silnik wyrażeń regularnych do tak długiej pracy, że aplikacja przestaje odpowiadać. OWASP opisuje przyczynę: gdy dopasowanie się nie udaje, silnik cofa się i próbuje kolejnych ścieżek, a przy złym wzorcu ich liczba podwaja się z każdym kolejnym znakiem.
OWASP podaje przykłady takich wzorców: (a+)+$, ([a-zA-Z]+)*$, (a|aa)+$ i (a|a?)+$. Łączy je powtórzenie w grupie, która sama jest powtarzana. Sprawdziliśmy ^(a+)+$ na ciągu liter “a” zakończonym wykrzyknikiem:
| Liczba liter “a” | Node.js 22 | Python 3.14 |
|---|---|---|
| 22 | – | 107 ms |
| 24 | 63 ms | 460 ms |
| 26 | 280 ms | – |
| 28 | 1051 ms | – |
Czasy pochodzą z jednego przebiegu na naszym komputerze. W powtórzeniu liczby wyszły inne, ale tempo wzrostu to samo: każde dwa dodatkowe znaki mniej więcej czterokrotnie wydłużały czas. Wzorzec ^a+$, który robi praktycznie to samo bez zagnieżdżenia, sprawdził 28 liter w 0,03 ms. Kilkadziesiąt znaków wpisanych w formularz wystarczy więc, żeby jeden wzorzec zajął procesor serwera na długie sekundy.
Jak się chronić:
- nie zagnieżdżaj powtórzeń (
(x+)+,(x*)*) i nie stawiaj obok siebie alternatyw, które pasują do tego samego tekstu, - ogranicz długość danych, zanim trafią do wyrażenia regularnego,
- testuj wzorzec także na długim tekście, który prawie pasuje, bo dopiero on uruchamia cofanie.
Najczęstsze pytania
Czy regex sprawdzi, czy PESEL albo NIP jest prawdziwy?
Nie. Wyrażenie regularne sprawdza tylko format: liczbę cyfr i ewentualne myślniki. PESEL i NIP mają cyfrę kontrolną liczoną z pozostałych cyfr i do jej sprawdzenia potrzebny jest osobny kod. Nawet poprawna cyfra kontrolna nie potwierdza, że numer należy do konkretnej osoby lub firmy.
Jak zapisać “lub” w wyrażeniu regularnym?
Pionową kreską: kot|pies pasuje do “kot” albo “pies”. Gdy alternatywa ma dotyczyć tylko części wzorca, zamknij ją w grupie: ^(?:tak|nie)$ przyjmuje wyłącznie “tak” albo “nie”. Bez grupy ^tak|nie$ znaczy: “tak” na początku albo “nie” na końcu.
Jak zrobić negację w wyrażeniu regularnym?
Dla pojedynczego znaku służy daszek w nawiasie kwadratowym: [^0-9] to dowolny znak poza cyfrą. Dla całego ciągu potrzebny jest negatywny lookahead: ^(?!.*test).*$ przyjmuje tekst, w którym nigdzie nie występuje słowo “test”. Działa w JavaScripcie, Pythonie i grep -P, ale nie w grep -E.
Czy wyrażenia regularne działają tak samo w każdym języku?
Podstawy tak: znaki, klasy, kwantyfikatory, kotwice i grupy zapisuje się wszędzie podobnie. Różnią się szczegóły. W naszym teście \w łapało polskie litery w Pythonie, a w JavaScripcie nie, lookbehind zmiennej długości działał tylko w JavaScripcie, a grep -E w ogóle nie rozpoznawał \d.
Podsumowanie
Wyrażenia regularne to najszybszy sposób na sprawdzenie formatu danych i wyszukanie wzorca w tekście. Najwięcej błędów robi się przez brak ^ i $, nieescapowaną kropkę oraz różnice między dialektami. W naszym teście JavaScript i Python dały te same wyniki na wszystkich dziewięciu wzorcach. Rozjechały się na polskich literach w \w i na lookbehindzie zmiennej długości. GNU grep w trybie -E potraktował \d jak literę d, więc tam używaj [0-9]. Regex sprawdza tylko format i nie zastąpi liczenia cyfry kontrolnej PESEL ani NIP. Wzorzec od modelu AI przetestuj na własnej liście przypadków brzegowych: w naszym teście przepuścił numer 000 000 000. Unikaj też zagnieżdżonych powtórzeń, które otwierają drogę do ataku ReDoS.
Newsletter · DevstockAcademy & Kodożercy
Bądź na bieżąco ze światem IT, AI i automatyzacji
Co wtorek: newsy z branży, praktyczne tipy i narzędzia które warto znać. Zero spamu.



