Skip to content
Powrót do bloga
Poradniki

Regex Cheat Sheet: metaznaki, grupy i lookaroundy

Opanuj regex z tym kompletnym cheat sheetem — metaznaki, kwantyfikatory, kotwice, grupy, lookaroundy i 15+ wzorców dla JavaScript i Python. Unikaj katastrofalnego backtrackingu.

12 min czytania

Regex cheat sheet: metaznaki, grupy i lookaroundy (kompletna referencja)

Wyrażenie regularne to mały język wzorców do dopasowywania tekstu. \d+ znaczy „jedna lub więcej cyfr”, a ^Error znaczy „wiersz zaczynający się od Error”. To cała robota. Ten regex cheat sheet zbiera składnię na jednej przewijanej stronie: metaznaki, kwantyfikatory, kotwice, grupy, lookaroundy i flagi, plus ponad 15 wzorców gotowych do wklejenia w JavaScript lub Python.

Tekst jest napisany dla deweloperów, którzy wiedzą już, czym jest łańcuch znaków, i potrzebują referencji, a nie wycieczki. Po sam zestaw symboli wystarczy przeskoczyć do tabeli szybkiej referencji. Sekcje o lookaroundach i pułapkach warto przeczytać, jeśli kiedykolwiek regex zawiesił serwer.

1. Czym jest regex i dlaczego nadal jest potrzebny w 2026

Regex to wzorzec kompilowany do maszyny stanów, która skanuje łańcuch i albo go dopasowuje, albo nie. Gramatyka jest niewielka, zastosowań bez liku.

AI może naszkicować wzorzec, ale trzy zadania nadal należą do człowieka piszącego regex ręcznie:

  • Parsowanie logów. Mając dziesięć milionów linii logów dostępu nginx i potrzebując każdego żądania 5xx z konkretnego user agenta, 40-znakowy regex puszczony przez grep -E kończy pracę w sekundach; wywołanie LLM na każdą linię — nie.
  • Walidacja formularzy i pól. Numery telefonów, kody pocztowe, znaczniki czasu ISO, klucze licencyjne. Wzorzec leży obok pola i uruchamia się przy każdym naciśnięciu klawisza w przeglądarce.
  • Masowe znajdź-i-zamień. Refaktoryzacja tysiąca plików, w której trzeba przechwycić nazwę i wstawić ją z powrotem. sed, ripgrep i edytorowe „Replace in files” natywnie mówią po regexie.

Drugą połowę tego samego zestawu narzędzi, tę dotyczącą JSON-a, opisujemy w naszym jq command-line cheat sheet.

1.1 Jak czytać wzorzec regex (5-sekundowy regular expression tutorial)

Większość wzorców łatwiej czytać od lewej do prawej, token po tokenie. Weźmy ^[A-Z]\w+\d{2,4}$ jako przykład:

  • ^ zakotwicza dopasowanie do początku łańcucha.
  • [A-Z] dopasowuje dokładnie jedną wielką literę.
  • \w+ dopasowuje jeden lub więcej znaków słownych.
  • \d{2,4} dopasowuje od dwóch do czterech cyfr.
  • $ zakotwicza do końca łańcucha.

Cała sztuka polega na tym, by najpierw czytać kotwice, potem klasy znaków, a na końcu kwantyfikatory.

2. Tabela szybkiej referencji

Po tę sekcję sięga większość czytelników. Można kopiować, co potrzebne.

Metaznaki

WzorzecDopasowuje
.Dowolny znak poza nową linią (lub dowolny znak z flagą s/dotall)
\dCyfra ([0-9] lub wszystkie cyfry Unicode z flagą u)
\DNiecyfra
\wZnak słowny ([A-Za-z0-9_])
\WZnak niesłowny
\sDowolny znak biały (spacja, tab, nowa linia, …)
\SDowolny znak niebiały

Kwantyfikatory

WzorzecDopasowuje
*0 lub więcej (zachłanny)
+1 lub więcej (zachłanny)
?0 lub 1 (zachłanny)
{n}Dokładnie n razy
{n,m}Od n do m razy
{n,}n lub więcej razy
*?, +?, ??, {n,m}?Leniwe warianty każdego kwantyfikatora

Kotwice

WzorzecDopasowuje
^Początek łańcucha (lub początek wiersza z flagą m)
$Koniec łańcucha (lub koniec wiersza z flagą m)
\bGranica słowa
\BBrak granicy słowa
\ABezwzględny początek łańcucha (Python)
\ZBezwzględny koniec łańcucha (Python)

Klasy znaków

WzorzecDopasowuje
[abc]Dowolny z a, b, c
[^abc]Cokolwiek poza a, b, c
[a-z]Dowolna mała litera
[0-9]Dowolna cyfra
\p{L}Dowolna litera Unicode (flaga u w JS, domyślnie w Pythonowym re)

Grupy

WzorzecDopasowuje
(...)Grupa przechwytująca
(?:...)Grupa nieprzechwytująca
(?<name>...)Nazwane przechwycenie (JS ES2018+); w Pythonie (?P<name>...)
\1, \2Backreference do grupy 1, 2

Lookaround

WzorzecDopasowuje
(?=...)Pozytywny lookahead
(?!...)Negatywny lookahead
(?<=...)Pozytywny lookbehind
(?<!...)Negatywny lookbehind

Flagi

FlagaEfekt
iBez rozróżniania wielkości liter
mMultiline: ^ i $ dopasowują się per wiersz
sDotall: . dopasowuje nowe linie
gGlobal (JS) — znajduje wszystkie dopasowania
uTryb Unicode
ySticky (JS) — kotwiczy do lastIndex

3. Metaznaki i klasy znaków

3.1 Literały kontra znaki specjalne

Większość znaków jest literalna. Tych 12 metaznaków trzeba escape’ować, gdy ma być potraktowane dosłownie:

. ^ $ * + ? ( ) [ ] { } | \

Zapomnienie o escape’owaniu . to najczęstszy bug w regexach. \. dopasowuje literalną kropkę. Wewnątrz klasy znaków [.] również dopasowuje literalną kropkę — większość metaznaków traci moc wewnątrz [...] poza ], \, ^ (kiedy stoi na początku) i - (w środku).

3.2 Skrótowe klasy znaków

Klasy skrótowe wyglądają prosto, dopóki nie pojawi się Unicode:

// JavaScript — bez flagi u, \d obejmuje tylko ASCII
/\d/.test('5');    // true
/\d/.test('٥');    // false (cyfra arabsko-indyjska)
/\d/u.test('٥');   // false — nawet z u, \d w JS pozostaje ASCII
/\p{N}/u.test('٥'); // true — \p{N} to klasa cyfry świadoma Unicode
# Python — moduł re traktuje \d jako Unicode domyślnie
import re
re.match(r'\d', '٥')  # <Match span=(0, 1)>
re.match(r'(?a)\d', '٥')  # None — (?a) wymusza ASCII

Przy wejściach wyłącznie angielsko-ASCII \d i [0-9] są wymienne. W chwili gdy użytkownik wkleja imię z akcentem, lepszy jest \p{L} niż \w.

3.3 Własne klasy znaków

// JavaScript
/[A-Za-z][A-Za-z0-9_-]{2,29}/.test('valid_handle-1'); // true

// Negacja i zakresy razem
/[^aeiou\s]/g  // dowolny znak niebędący samogłoską ani białym znakiem

Dla kategorii Unicode \p{L} to „dowolna litera”, \p{N} to „dowolna cyfra”, \p{Script=Han} to „dowolny znak Han”. JavaScript wymaga flagi u; Python wspiera \p{...} tylko przez pakiet regex z PyPI, nie standardowe re.

Pracując w wierszu poleceń, można też spotkać klasy znaków POSIX:

Klasa POSIXDopasowujeOdpowiednik ASCII
[[:alpha:]]litery[A-Za-z]
[[:digit:]]cyfry[0-9] (\d)
[[:alnum:]]litery + cyfry[A-Za-z0-9]
[[:space:]]białe znaki\s
[[:upper:]]wielkie litery[A-Z]
[[:lower:]]małe litery[a-z]

Klasy POSIX działają w grep -E, sed -E. Nie działają w JavaScripcie ani w Pythonowym re — tam stosuje się \d, \s, \w.

4. Kwantyfikatory oraz zachłanność kontra leniwość

4.1 Podstawowe kwantyfikatory

/a*/.exec('aaab')      // ['aaa']     — 0 lub więcej
/a+/.exec('aaab')      // ['aaa']     — 1 lub więcej
/a?/.exec('aaab')      // ['a']       — 0 lub 1
/a{2,3}/.exec('aaaab') // ['aaa']     — od 2 do 3

4.2 Zachłanność kontra leniwość

Domyślnie kwantyfikatory są zachłanne: chwytają tyle, ile się da, po czym oddają, by dopasować cały wzorzec. Dodanie ? zmienia je w leniwe.

const html = '<p>one</p><p>two</p>';

html.match(/<p>.*<\/p>/)[0];   // '<p>one</p><p>two</p>'   (zachłanny zjada oba)
html.match(/<p>.*?<\/p>/)[0];  // '<p>one</p>'             (leniwy zatrzymuje się na pierwszym)

Wariant leniwy to niemal zawsze właściwy wybór przy wyciąganiu tagów lub stringów w cudzysłowach. Jeszcze lepiej unikać . w ogóle i sięgnąć po klasę zanegowaną: <p>[^<]*</p> jest szybsze niż <p>.*?</p>, bo nie ma już dokąd robić backtrackingu.

4.3 Katastrofalny backtracking

Tak właśnie regex zawiesza serwer. Wystarczy zagnieździć kwantyfikator w innym kwantyfikatorze z niejednoznacznym zakładkiem, a silnik eksploruje wykładniczą liczbę ścieżek, zanim się podda.

// Nie róbcie tego
/(a+)+b/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // trwa sekundy

Dla 41 znaków a, po których następuje !, silnik próbuje mniej więcej 2^41 punktów podziału, zanim stwierdzi, że b brakuje. Trzy poprawki:

  1. Spłaszczyć wzorzec: /a+b/ robi to samo bez zagnieżdżenia.
  2. Użyć grupy atomowej (Python regex, PCRE, Java, Ruby): (?>a+)+b. Gdy a+ się dopasuje, silnik odmawia backtrackingu do środka.
  3. Zmienić silnik: regexp z Go, RE2 i crate regex z Rusta używają NFA o czasie liniowym i z założenia nie potrafią backtrackować katastrofalnie.

JavaScript i Pythonowy re oba backtrackują i nie mają grup atomowych w bibliotece standardowej (pakiet regex z PyPI dodaje je w Pythonie). Gdy długość wejścia jest pod kontrolą, problem znika; gdy wejście pochodzi od użytkownika, warto najpierw zwalidować długość albo prekompilować pod RE2.

5. Kotwice i granice słów

5.1 ^ i $

Domyślnie ^ to początek całego wejścia, a $ to jego koniec. Z flagą m (multiline) stają się początkiem i końcem każdego wiersza:

const log = 'INFO start\nERROR boom\nINFO done';
log.match(/^ERROR.*/);    // null    — tryb jednowierszowy, ^ dopasowuje tylko indeks 0
log.match(/^ERROR.*/m);   // ['ERROR boom']

5.2 \b i \B

\b to asercja o szerokości zerowej: dopasowuje pozycję między znakiem słownym (\w) a znakiem niesłownym. Przydatne do wyszukiwania całych słów:

/\bcat\b/.test('the cat sat');     // true
/\bcat\b/.test('concatenate');     // false

Granice słów są zdefiniowane na \w, które domyślnie obejmuje ASCII. Tekst chiński, japoński i koreański nie ma spacji między słowami, więc \b nie wykryje tam krawędzi słów. Potrzebny jest tokenizator (jieba, MeCab) przed regexem, a nie zamiast niego.

5.3 Tryb multiline

import re
text = "INFO ok\nERROR fail\nINFO done\n"

re.findall(r'^ERROR.*$', text)              # []
re.findall(r'^ERROR.*$', text, re.MULTILINE) # ['ERROR fail']

W JavaScripcie to samo wygląda tak: text.match(/^ERROR.*$/gm). m w połączeniu z g chwyta każdy pasujący wiersz.

6. Grupy, przechwytywanie i backreferences

6.1 Grupy przechwytujące

Nawiasy robią dwie rzeczy: grupują podwzorce pod kwantyfikatory i przechwytują dopasowanie do późniejszego użycia.

'2026-05-13'.match(/(\d{4})-(\d{2})-(\d{2})/);
// ['2026-05-13', '2026', '05', '13', index: 0, ...]

Grupy są numerowane od lewej do prawej po nawiasie otwierającym, począwszy od 1.

6.2 Grupy nieprzechwytujące

Gdy potrzebne jest samo grupowanie, bez przechwytywania, sięga się po (?:...). Jest szybsze i utrzymuje porządek w numeracji grup:

/(?:https?):\/\/(\S+)/.exec('see https://go-tools.org');
// ['https://go-tools.org', 'go-tools.org']
// — protokół jest pogrupowany, ale nie przechwycony; grupa 1 to host

6.3 Grupy nazwane

Nazywanie grup czyni wzorce czytelnymi i odpornymi na refaktor.

// JavaScript (ES2018+)
const m = '2026-05-13'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m.groups.year;  // '2026'
# Python — zwróć uwagę na składnię (?P<...>)
import re
m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-05-13')
m.group('year')  # '2026'

6.4 Backreferences

Backreferences sprawiają, że późniejsza część wzorca powtarza to, co dopasowała wcześniejsza grupa przechwytująca.

// Znajdź dowolny znak powtarzający się po sobie
'bookkeeper'.match(/(\w)\1/g);   // ['oo', 'kk', 'ee']

// Dopasuj parę tagów HTML po nazwie
const tag = /<(\w+)>(.*?)<\/\1>/;
'<b>bold</b>'.match(tag);
// ['<b>bold</b>', 'b', 'bold']

W Pythonie \1 działa zarówno we wzorcu, jak i w zamienniku; nazwane referencje to (?P=name) we wzorcu i \g<name> w zamiennikach re.sub.

7. Lookaroundy: lookahead i lookbehind

Lookaroundy to asercje o szerokości zerowej. Sprawdzają warunek bez konsumowania znaków, więc dają się łączyć w łańcuchy.

7.1 Lookahead

// Hasło: co najmniej 8 znaków, jedna cyfra, jedna wielka, jedna mała litera
const strong = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{8,}$/;
strong.test('Hunter2!');   // true
strong.test('hunter2!');   // false — brak wielkiej litery

// Negatywny lookahead — nazwy plików, które nie są .tmp
/^[\w-]+(?!\.tmp$)\.[a-z]+$/.test('report.csv'); // true

7.2 Lookbehind

Lookbehind jest lustrzanym odbiciem: asercja sprawdza, co stoi przed bieżącą pozycją.

// Wyciągnij cenę po symbolu waluty — zachowaj liczbę, odrzuć $
'price: $42.50'.match(/(?<=\$)\d+(\.\d+)?/);   // ['42.50', '.50']

// Negatywny lookbehind — dopasuj Bond, ale nie James Bond
'Mr. Bond'.match(/(?<!James )Bond/);  // ['Bond']
'James Bond'.match(/(?<!James )Bond/); // null

7.3 Lookbehind w JavaScript kontra Python

To jedno z niewielu miejsc, w których oba silniki rozjeżdżają się na tyle, by zepsuć wzorzec przy portowaniu.

SilnikDługość lookbehind
JavaScript (V8, SpiderMonkey, JSC 16.4+)Zmienna szerokość od ES2018. (?<=\d+) jest poprawne.
Standardowy Pythonowy reTylko stała szerokość. (?<=\d+) rzuca error: look-behind requires fixed-width pattern.
Pythonowy pakiet regex z PyPIWspiera zmienną szerokość. import regex; regex.search(r'(?<=\d+)abc', '12abc').

Obejście w Pythonie: zapisać lookbehind ze znanym powtórzeniem ((?<=\d{3})) albo przechwycić prefiks i odciąć go po dopasowaniu.

8. Flagi i modyfikatory

8.1 i — bez rozróżniania wielkości liter

/error/i.test('FATAL ERROR'); // true
re.search(r'error', 'FATAL ERROR', re.IGNORECASE)  # <Match span=(6, 11)>

8.2 m i s

m przełącza ^ i $ w kotwice per wiersz. s (dotall) pozwala . dopasowywać nowe linie. Są niezależne i dają się łączyć, gdy potrzeba obu.

/<script>(.*?)<\/script>/s.exec('<script>\nalert(1)\n</script>')[1];
// '\nalert(1)\n'  — bez s kropka odmówiłaby nowych linii

8.3 g — globalny

W JavaScripcie g zmienia API, a nie samo dopasowanie. Bez g String.match zwraca grupy przechwytujące; z g zwraca każdy łańcuch dopasowania. Aby zachować grupy przechwytujące we wszystkich dopasowaniach, można sięgnąć po matchAll.

const text = 'a=1 b=2 c=3';

text.match(/(\w)=(\d)/);     // pierwsze dopasowanie z grupami
text.match(/(\w)=(\d)/g);    // ['a=1', 'b=2', 'c=3'] — bez grup
[...text.matchAll(/(\w)=(\d)/g)]; // każde dopasowanie, z grupami

Python nie używa g — globalne warianty to re.findall, re.finditer i re.sub.

8.4 u — Unicode oraz \p{...}

// Dopasuj dowolny znak Han (chiński, japońskie kanji)
/\p{Script=Han}+/gu.test('Hello 世界'); // true

// Dopasuj emoji (extended pictographic)
/\p{Extended_Pictographic}/u.test('👋'); // true

W Pythonie Unicode jest włączony domyślnie; re.findall(r'[一-鿿]+', text) to odpowiednik dla zakresu Han. Po pełne escape’y właściwości Unicode sięga się po pakiet regex z PyPI: regex.findall(r'\p{Script=Han}+', text).

9. Typowe wzorce na co dzień

9.1 Walidacja e-maila

Najpierw uczciwie: której wersji w ogóle się potrzebuje.

// Wzorzec na 95% — to, czego używa większość walidatorów formularzy
const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
email.test('a@b.co');  // true

// Wzorzec „naprawdę chcę być w stylu RFC 5322”
const rfc = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

Prawda jest taka: pełna walidacja e-maila wg RFC 5322 w czystym regexie to ~6000 znaków i nadal pomyłka w skrajnych przypadkach. Lepiej użyć wzorca na 95%, a potem wysłać e-mail weryfikacyjny. To jedyny test, który faktycznie działa.

9.2 Wyciąganie URL-i

const urlPattern = /https?:\/\/[^\s<>"]+/g;
const found = 'See https://example.com/a?b=1 and http://x.io'.match(urlPattern);
// ['https://example.com/a?b=1', 'http://x.io']

Po wyciągnięciu URL-a zwykle chce się przejrzeć jego query string. Wystarczy wkleić go do naszego Kodera i dekodera URL i parametry zakodowane procentowo stają się czytelne na pierwszy rzut oka. Pełny obraz tego, kiedy kodować, a kiedy dekodować, znajdziesz w przewodniku po kodowaniu i dekodowaniu URL.

9.3 Numery telefonów

// E.164 — międzynarodowy, opcjonalny + i 1-3 cyfry kodu kraju
const e164 = /^\+?[1-9]\d{1,14}$/;
e164.test('+14155551234');  // true

// Plan numeracji północnoamerykańskiej z separatorami
const nanp = /^(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
nanp.test('(415) 555-1234'); // true

Do wszystkiego ponad „czy taki kształt jest wiarygodny” służy libphonenumber. Regex nie zweryfikuje, czy dany numer kierunkowy istnieje.

9.4 IPv4 i IPv6

// IPv4 — ścisłe 0-255 na oktet
const ipv4 = /^((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d)$/;
ipv4.test('192.168.1.1');   // true
ipv4.test('999.0.0.1');     // false

// IPv6 — forma uproszczona. Pełny wzorzec wg RFC 4291 to ~600 znaków.
const ipv6simple = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
ipv6simple.test('2001:0db8:85a3:0000:0000:8a2e:0370:7334'); // true

Dla prawdziwego IPv6 ze skrótem ::, osadzonym IPv4 i identyfikatorami stref lepiej skorzystać z isIP() w node:net albo ipaddress.ip_address() w Pythonie. Próba zrobienia tego w czystym regexie to rytuał przejścia, a potem ciężar utrzymania.

9.5 Daty i znaczniki czasu ISO 8601

// Sama data — YYYY-MM-DD
const isoDate = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
isoDate.test('2026-05-13'); // true

// Data + czas + opcjonalne sekundy ułamkowe + Z lub przesunięcie
const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
iso.test('2026-05-13T09:30:00.123Z'); // true

ISO 8601 wygląda prosto, a jest pełen pułapek: sekundy przestępne, daty tygodniowe (2026-W19), daty porządkowe (2026-133). Sekundy od epoki kontra milisekundy i przesunięcia strefowe omawia przewodnik po Unix timestamp.

10. Przepływy znajdź/zamień z regexem

10.1 JavaScript — String.replace z $1

// Przeformatuj daty US: MM/DD/YYYY -> YYYY-MM-DD
'05/13/2026'.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$1-$2');
// '2026-05-13'

// Użyj callbacku, gdy zamiana jest warunkowa
'price 42 dollars'.replace(/(\d+) dollars/, (_, n) => `$${n}`);
// 'price $42'

$1, $2, … odwołują się do grup numerowanych. $<name> odwołuje się do grup nazwanych. $& to pełne dopasowanie; $$ to literalny $.

10.2 Python — re.sub z \1 i callbackami

import re

# Ten sam reformat daty co wyżej
re.sub(r'(\d{2})/(\d{2})/(\d{4})', r'\3-\1-\2', '05/13/2026')
# '2026-05-13'

# Callback — wszystkie adresy e-mail w łańcuchu zamień na wielkie litery
def upper_email(m):
    return m.group(0).upper()

re.sub(r'[\w.-]+@[\w.-]+', upper_email, 'mail me at hi@go-tools.org')
# 'mail me at HI@GO-TOOLS.ORG'

W zamiennikach Python używa \1 lub \g<name>. Prefiks raw string r'...' ma znaczenie — bez niego \1 staje się znakiem literalnym.

10.3 CLI: sed, grep, ripgrep, jq

Przy masowych refaktorach w wierszu poleceń regex przenosi się ze skryptu do shella:

# ripgrep — znajdź każde TODO z dopiętą nazwą
rg -n '\bTODO\(([^)]+)\)' --replace 'TODO(\1)'

# grep -E z kotwicami — nieudane logowania z auth.log
grep -E '^[A-Z][a-z]{2} +[0-9]+ .*Failed password' /var/log/auth.log

# sed — usuń końcowe białe znaki, in-place, w całym drzewie
find . -name '*.md' -print0 | xargs -0 sed -i -E 's/[[:space:]]+$//'

ripgrep korzysta z crate’a regex z Rusta (w stylu RE2, czas liniowy, brak lookbehind). grep -E i sed -E używają POSIX-owego rozszerzonego regexa, w którym brakuje \d; zamiast tego idą [0-9] i [[:digit:]]. Gdy dane są w JSON-ie, regex najlepiej zamienić na jq. Równoległą kartę referencyjną daje jq cheat sheet.

11. Typowe pułapki

11.1 Zapomnienie o escape’owaniu .

Realny bug, który nam się zdarzył: redactor logów miał maskować adresy IP.

// Źle — dopasowuje też '192a168b1c1'
/(\d+).(\d+).(\d+).(\d+)/.test('192a168b1c1');  // true

// Dobrze
/(\d+)\.(\d+)\.(\d+)\.(\d+)/.test('192a168b1c1'); // false

Wewnątrz klasy znaków . jest już literalna, więc działają oba: [.] i \.. W każdym innym miejscu — trzeba ją escape’ować.

11.2 Zachłanne .* zjada za dużo

'<a href="x"><b>bold</b></a>'.match(/<(.*)>/)[1];
// 'a href="x"><b>bold</b></a'  — całość!

Zachłanne .* skanuje do końca łańcucha, a potem cofa się, aż > się dopasuje, czyli do ostatniego > w wejściu. Albo idź leniwie (.*?), albo, szybciej i czytelniej, użyj klasy zanegowanej ([^>]*).

11.3 Kotwice multiline

Częste nieporozumienie: ^ i $ domyślnie nie dopasowują znaków nowej linii. Dopasowują pozycje na początku i końcu całego wejścia. Dopiero flaga m zmienia je w kotwice per wiersz. Z kolei flaga s pozwala . przechodzić przez nowe linie. Obie są ortogonalne, a przy parsowaniu logów zwykle potrzeba obu naraz.

11.4 ReDoS i jak go rozbroić

ReDoS, czyli regex denial of service, to produkcyjna wersja katastrofalnego backtrackingu. Sposoby na to:

  1. Analiza statyczna. Narzędzia takie jak safe-regex, recheck czy ESLint-owa reguła no-misleading-character-class wyłapują niebezpieczne wzorce, zanim trafią na produkcję.
  2. Grupy atomowe (Python regex, PCRE, Ruby, Java): (?>...) uniemożliwia silnikowi ponowne wejście w grupę przy backtrackingu.
  3. Kwantyfikatory zaborcze (*+, ++, ?+ w PCRE/Javie): ta sama idea, krótsza składnia.
  4. Przejście na silnik bez backtrackingu. regexp z Go, RE2, crate regex z Rusta i wiązanie re2 dla Pythona działają w czasie liniowym. ripgrep jest najpopularniejszym wdrożeniem RE2 w naturze.
  5. Zwalidować najpierw długość wejścia. 10 KB-owa bomba regexowa to bug; 10-bajtowy limit na wejściu to jedna linia kodu.

Szerszy inwentarz codziennych narzędzi, które idą w parze z regexem (formatery, dekodery, konwertery), zbiera nasz przewodnik po narzędziach deweloperskich.

Zanim wypuścisz złożony wzorzec na produkcję, przetestuj go interaktywnie. regex101.com przełącza się między smakami PCRE, JavaScript, Python i Go, objaśnia każdy token i pokazuje backtracking, dzięki czemu wyłapiesz katastrofalne wzorce.

12. FAQ

Jaka jest różnica między regexowymi * i +?

* dopasowuje zero lub więcej wystąpień (może dopasować pusty łańcuch); + dopasowuje jedno lub więcej (potrzebuje co najmniej jednego). a* dopasowuje '', 'a', 'aaaa'. a+ dopasowuje 'a' i 'aaaa', ale nie ''.

Jak dopasować regexem coś przez wiele wierszy?

Wystarczy włączyć flagę multiline (/.../m w JavaScripcie, re.MULTILINE w Pythonie), żeby ^ i $ kotwiczyły do każdego wiersza. Aby . również przekraczało nowe linie, dochodzi flaga dotall (s w JavaScripcie, re.DOTALL w Pythonie).

Czy regex jest taki sam w JavaScript i Python?

Trzon składni (kwantyfikatory, kotwice, klasy znaków, podstawowe grupy) pokrywa się w 90%. Dwie realne różnice: JavaScript (ES2018+) wspiera lookbehind zmiennej długości i zapisuje grupy nazwane jako (?<name>...); standardowy Pythonowy re wymaga lookbehinda stałej szerokości i używa (?P<name>...). Po lookbehind zmiennej długości w Pythonie sięga się po pakiet regex z PyPI.

Dlaczego mój regex ma katastrofalny backtracking?

Są zagnieżdżone kwantyfikatory z nakładającymi się dopasowaniami, na przykład (a+)+ lub (a|a)*. Na wejściu, które prawie pasuje, ale zawodzi blisko końca, silnik próbuje każdego podziału wewnętrznego kwantyfikatora — wykładniczej liczby ścieżek. Pomoże grupa atomowa (?>a+)+, kwantyfikator zaborczy a++ albo przejście na silnik bez backtrackingu w stylu RE2 czy regexp z Go.

Czy mogę używać lookbehind w JavaScript?

Tak. Pozytywny (?<=...) i negatywny (?<!...) lookbehind są obecne w V8 (Chrome, Node.js), SpiderMonkey (Firefox) i JavaScriptCore (Safari 16.4+) od ES2018. Lookbehind zmiennej długości jest wspierany. Dla starszego Safari pomoże transpilacja przez Babel albo wykrycie wsparcia funkcjonalnością przez try/catch wokół new RegExp.

Jak dopasować literalną kropkę . w regexie?

Escape’ujemy ją odwrotnym ukośnikiem: \. dopasowuje literalną kropkę. Wewnątrz klasy znaków kropka jest już literalna; działają oba zapisy: [.] i [\.]. Poza klasą nieescape’owana . to metaznak oznaczający „dowolny znak poza nową linią” (lub w ogóle dowolny znak z flagą dotall).

Co oznacza \s w regexie?

\s dopasowuje dowolny biały znak — spację, tabulator, nową linię, powrót karetki. W trybie Unicode dopasowuje też NBSP. \S to jego odwrotność.

Czy wyrażenia regularne rozróżniają wielkość liter?

Domyślnie tak. Stosuje się flagę i w JavaScripcie (/cat/i) albo re.IGNORECASE / (?i) w Pythonie.

Powiązane artykuły

Zobacz wszystkie artykuły