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
nginxi potrzebując każdego żądania 5xx z konkretnego user agenta, 40-znakowy regex puszczony przezgrep -Ekoń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,ripgrepi 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
| Wzorzec | Dopasowuje |
|---|---|
. | Dowolny znak poza nową linią (lub dowolny znak z flagą s/dotall) |
\d | Cyfra ([0-9] lub wszystkie cyfry Unicode z flagą u) |
\D | Niecyfra |
\w | Znak słowny ([A-Za-z0-9_]) |
\W | Znak niesłowny |
\s | Dowolny znak biały (spacja, tab, nowa linia, …) |
\S | Dowolny znak niebiały |
Kwantyfikatory
| Wzorzec | Dopasowuje |
|---|---|
* | 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
| Wzorzec | Dopasowuje |
|---|---|
^ | Początek łańcucha (lub początek wiersza z flagą m) |
$ | Koniec łańcucha (lub koniec wiersza z flagą m) |
\b | Granica słowa |
\B | Brak granicy słowa |
\A | Bezwzględny początek łańcucha (Python) |
\Z | Bezwzględny koniec łańcucha (Python) |
Klasy znaków
| Wzorzec | Dopasowuje |
|---|---|
[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
| Wzorzec | Dopasowuje |
|---|---|
(...) | Grupa przechwytująca |
(?:...) | Grupa nieprzechwytująca |
(?<name>...) | Nazwane przechwycenie (JS ES2018+); w Pythonie (?P<name>...) |
\1, \2 | Backreference do grupy 1, 2 |
Lookaround
| Wzorzec | Dopasowuje |
|---|---|
(?=...) | Pozytywny lookahead |
(?!...) | Negatywny lookahead |
(?<=...) | Pozytywny lookbehind |
(?<!...) | Negatywny lookbehind |
Flagi
| Flaga | Efekt |
|---|---|
i | Bez rozróżniania wielkości liter |
m | Multiline: ^ i $ dopasowują się per wiersz |
s | Dotall: . dopasowuje nowe linie |
g | Global (JS) — znajduje wszystkie dopasowania |
u | Tryb Unicode |
y | Sticky (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 POSIX | Dopasowuje | Odpowiednik 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:
- Spłaszczyć wzorzec:
/a+b/robi to samo bez zagnieżdżenia. - Użyć grupy atomowej (Python
regex, PCRE, Java, Ruby):(?>a+)+b. Gdya+się dopasuje, silnik odmawia backtrackingu do środka. - Zmienić silnik:
regexpz Go, RE2 i crateregexz 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.
| Silnik | Długość lookbehind |
|---|---|
| JavaScript (V8, SpiderMonkey, JSC 16.4+) | Zmienna szerokość od ES2018. (?<=\d+) jest poprawne. |
Standardowy Pythonowy re | Tylko stała szerokość. (?<=\d+) rzuca error: look-behind requires fixed-width pattern. |
Pythonowy pakiet regex z PyPI | Wspiera 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:
- Analiza statyczna. Narzędzia takie jak
safe-regex,recheckczy ESLint-owa regułano-misleading-character-classwyłapują niebezpieczne wzorce, zanim trafią na produkcję. - Grupy atomowe (Python
regex, PCRE, Ruby, Java):(?>...)uniemożliwia silnikowi ponowne wejście w grupę przy backtrackingu. - Kwantyfikatory zaborcze (
*+,++,?+w PCRE/Javie): ta sama idea, krótsza składnia. - Przejście na silnik bez backtrackingu.
regexpz Go, RE2, crateregexz Rusta i wiązaniere2dla Pythona działają w czasie liniowym. ripgrep jest najpopularniejszym wdrożeniem RE2 w naturze. - 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.