Przejdź do zawartości

Wyrażenie regularne

Z Wikipedii, wolnej encyklopedii

Wyrażenie regularne (ang. regular expression, w skrócie regex lub regexp) – wzorzec opisujący łańcuch symboli. Teoria wyrażeń regularnych jest związana z teorią języków regularnych. Wyrażenia regularne mogą określać zbiór pasujących łańcuchów, jak również wyszczególniać istotne części łańcucha.

W informatyce teoretycznej wyrażenia regularne są ciągami znaków pozwalającymi opisywać języki regularne. W praktyce znalazły bardzo szerokie zastosowanie, pozwalają bowiem w łatwy sposób opisywać wzorce tekstu, natomiast istniejące algorytmy w efektywny sposób określają, czy podany ciąg znaków pasuje do wzorca lub wyszukują w tekście wystąpienia wzorca. Wyrażenia regularne w praktycznych zastosowaniach są zapisywane za pomocą bogatszej i łatwiejszej w użyciu składni niż ta stosowana w rozważaniach teoretycznych. Co więcej, opisane niżej powszechnie wykorzystywane wsteczne referencje (czyli użycie wcześniej dopasowanego fragmentu tekstu jako części wzorca), powodują, że wyrażenie regularne je zawierające może nie definiować języka regularnego.

Wyrażenia regularne stanowią integralną część narzędzi systemowych takich jak sed, grep, wielu edytorów tekstu, języków programowania przetwarzających tekst AWK i Perl, a także są dostępne jako odrębne biblioteki dla wszystkich języków używanych obecnie.

Dwie najpopularniejsze składnie wyrażeń regularnych to składnia uniksowa i składnia perlowa. Składnia perlowa jest znacznie bardziej rozbudowana. Jest ona używana nie tylko w języku Perl, ale także w innych językach programowania: Ruby, bibliotece PCRE do C i w narzędziu powłoki o nazwie pcregrep (znanym też jako pgrep). Perlową składnię stosuje się również w maskach przepisań mod rewrite.

Historia

[edytuj | edytuj kod]

Pojęcie wyrażeń regularnych wprowadził w 1951 roku amerykański matematyk Stephen Cole Kleene, który opisał języki regularne za pomocą notacji nazwanej „zdarzeniami regularnymi” (regular events); notacja powstała przy próbie matematycznego opisu wczesnych modeli sieci neuronowych McCullocha i Pittsa[1][2].

Do praktyki programistycznej wyrażenia regularne trafiły w 1968 roku za sprawą Kena Thompsona, który wbudował notację Kleene’ego w edytor tekstu QED jako mechanizm wyszukiwania wzorców; w opublikowanym wówczas artykule opisał także sposób przekształcania wyrażenia w niedeterministyczny automat skończony, znany dziś jako konstrukcja Thompsona, a jego implementacja dla szybkości kompilowała wyrażenia bezpośrednio do kodu maszynowego komputera IBM 7094, co uchodzi za wczesny przykład kompilacji JIT[3][4]. Mechanizm ten Thompson przeniósł następnie do uniksowego edytora ed, z którego wywodzi się narzędzie grep – jego nazwa pochodzi od polecenia edytora g/re/p (global/regular expression/print)[5]. W latach 70. w Bell Labs powstały kolejne korzystające z wyrażeń regularnych narzędzia, m.in. sed, AWK, generator analizatorów leksykalnych lex oraz edytor vi[6].

Składnię narzędzi uniksowych ujednolicono w 1992 roku w standardzie POSIX, który definiuje podstawowe (BRE) i rozszerzone (ERE) wyrażenia regularne oraz klasy znaków takie jak [:digit:][7]. Równolegle rozwinęła się znacznie bogatsza linia perlowa: Perl pod koniec lat 80. przejął i rozbudował bibliotekę Henry’ego Spencera, a w 1997 roku Philip Hazel opracował zgodną z Perlem bibliotekę PCRE, wykorzystywaną m.in. przez PHP i serwer Apache[8]. Wyrażenia regularne weszły także do składni języka ECMAScript (JavaScript) i bibliotek standardowych większości współczesnych języków programowania, m.in. Javy i Pythona[9]. W 2010 roku Google opublikowało otwartą bibliotekę RE2, gwarantującą liniowy czas dopasowania kosztem rezygnacji z referencji wstecznych[10].

Wyrażenie regularne w informatyce teoretycznej

[edytuj | edytuj kod]

Definicja wyrażeń regularnych

[edytuj | edytuj kod]

Wyrażeniem regularnym nad alfabetem nazywamy ciąg znaków składający się z symboli oraz symboli z alfabetu następującej postaci:

  1. (słowo puste) są wyrażeniami regularnymi;
  2. wszystkie symbole są wyrażeniami regularnymi;
  3. jeśli są wyrażeniami regularnymi, to są nimi również:
    • (domknięcie Kleene’ego)
    • (konkatenacja)
    • (suma)
    • (grupowanie)
  4. wszystkie wyrażenia regularne są postaci opisanej w punktach 1–3.

Każde wyrażenie regularne definiuje pewien język formalny. Każdy język definiowany przez wyrażenie regularne jest regularny.

Definicja języka określanego przez wyrażenie regularne

[edytuj | edytuj kod]

Język definiowany przez wyrażenie regularne jest definiowany indukcyjnie. Niech L(w) oznacza język definiowany przez w. Wtedy baza indukcji jest następująca:

  • (zbiór zawierający tylko słowo puste)
  • (zbiór pusty)
  • dla dowolnego z alfabetu

Natomiast do konstrukcji wyrażeń służą 3 symbole:

  • (suma języków)
  • (domknięcie Kleene’ego)
  • (konkatenacja języków)

Gwiazdka wiąże najsilniej, konkatenacja słabiej, suma najsłabiej.

Własności wyrażeń regularnych

[edytuj | edytuj kod]

Wyrażenia są równoważne gdy definiują ten sam język:

  • – suma jest przemienna
  • – łańcuch pusty jest elementem neutralnym konkatenacji
  • – suma jest łączna
  • – konkatenacja również jest łączna
  • – konkatenacja jest rozdzielna względem sumy
  • – domknięcie Kleene’ego jest idempotentne
Przykład 1

Wyjaśnienie reguł:

  • – dowolny ciąg składający się z np. a także pusty
  • – sekwencja, najpierw następnie
  • – alternatywa, albo albo
Przykład 2

Wyrażenie definiuje język zawierający dokładnie dwa słowa: „Wiki” i „wiki”. To samo można wyrazić wprost

Przykład 3

Wyrażenie definiuje język wszystkich słów nad alfabetem które zawierają podsłowo baba.

Wyrażenia regularne a automaty skończone

[edytuj | edytuj kod]

Języki regularne można opisać również za pomocą automatów skończonych:

  1. niedeterministycznego automatu skończonego (NAS) z -przejściami (automat może zmienić swój stan bez podania symbolu wejściowego),
  2. niedeterministycznego automatu skończonego bez -przejść, oraz
  3. deterministycznego automatu skończonego (DAS).
Automat skończony Wyrażenie regularne
Image automat dla pustego języka, nie zawierającego żadnego słowa;
Image automat dla języka akceptującego tylko słowo puste;
Image automat dla języka akceptującego tylko jeden symbol;
Image NAS dla konkatenacji dwóch wyrażeń regularnych:
Image NAS dla sumy dwóch wyrażeń regularnych:
Image NAS dla domknięcia Kleene’ego wyrażenia regularnego:

Jedne z pierwszych praktycznych implementacji wyrażeń regularnych opierały się właśnie na symulacji programowej automatu skończonego. Najpierw budowany jest NAS z -przejściami zgodnie ze schematem pokazanym wyżej, następnie usuwane są -przejścia, kolejnym krokiem jest determinizacja automatu skończonego, czego wynikiem jest otrzymanie DAS, ostatnim zaś etapem jego minimalizacja. Symulowanie DAS jest bardzo proste i szybkie; pierwsze narzędzia systemu Unix używały tej metody, wykorzystuje ją również język AWK, Tcl, a także biblioteki dla języka Haskell. Symulacja automatu niedeterministycznego wymaga czasu proporcjonalnego do iloczynu długości tekstu i rozmiaru wyrażenia, a po determinizacji dopasowanie przebiega w czasie liniowym względem długości tekstu, choć sam automat deterministyczny może mieć wykładniczo więcej stanów[6].

Wyrażenia regularne w praktyce

[edytuj | edytuj kod]

Porównanie składni praktycznej z teoretyczną

[edytuj | edytuj kod]

Na początku zostaną pokazane różnice i części wspólne zapisu teoretycznego i praktycznego.

Informatyka teoretyczna Praktyka Komentarz
brak, "" w praktyce zbioru pustego nie podaje się wprost, w większości języków programowania słowo puste jest oznaczane przez "" (parę cudzysłowów)
(, ) lub \(, \) w niektórych implementacjach symbole specjalne poprzedza się backslashem
| lub \| jw.
[0123abcdefgh] lub krócej [0-3a-h] zakres znaków (dokładny opis – patrz niżej)
. dowolny znak z alfabetu (tutaj małe i duże litery, w praktyce cały zestaw znaków); w teoretycznym zapisie wymaga wyliczenia wszystkich znaków z alfabetu
* lub \* 0 lub więcej wystąpień
e? wyrażenie e występuje 0 lub 1 raz
e+ wyrażenie e występuje 1 lub więcej razy
brak^ metaznak oznaczający początek łańcucha (lub początek wiersza, jeśli przetwarzane są wielowierszowe napisy); w teoretycznych rozważaniach dopasowuje się całe słowa, podczas gdy w praktyce zwykle celem jest znalezienie dopasowania wewnątrz dłuższego tekstu, dlatego dopasowanie do całości wymaga dodatkowych oznaczeń

w zapisie [^e] oznacza negację e

brak$ metaznak oznaczający koniec łańcucha
e{4} określona liczba powtórzeń (tutaj 4); rozszerzenie Perla
e{4,7} określony zakres liczby powtórzeń wyrażenia e (tutaj od 4 do 7); rozszerzenie Perla

Podstawowe elementy

[edytuj | edytuj kod]

Podstawowe elementy wyrażeń regularnych:

  • Każdy znak, oprócz znaków specjalnych, określa sam siebie, np. a określa łańcuch złożony ze znaku a.
  • Kolejne symbole oznaczają, że w łańcuchu muszą wystąpić dokładnie te symbole w dokładnie takiej samej kolejności, np. ab oznacza że łańcuch musi składać się z litery a poprzedzającej literę b.
  • Kropka . oznacza dowolny znak z wyjątkiem znaku nowego wiersza (zależnie od ustawień i rodzaju wyrażeń).
  • Znaki specjalne poprzedzone odwrotnym ukośnikiem \ powodują, że poprzedzanym znakom nie są nadawane żadne dodatkowe znaczenia i oznaczają same siebie, np. \. oznacza znak kropki (a nie dowolny znak).
  • Zestaw znaków między nawiasami kwadratowymi oznacza jeden dowolny znak objęty nawiasami kwadratowymi, np. [abc] oznacza a, b lub c. Można używać także przedziałów: [a-c]. Między nawiasami kwadratowymi:
    • Daszek ^ na początku zestawu oznacza wszystkie znaki oprócz tych z zestawu.
    • Aby uniknąć niejasności, znaki - (łącznik) i ] (zamknięcie nawiasu kwadratowego) zapisywane są na skraju zestawu lub w niektórych systemach po znaku odwrotnego ukośnika, daszek zaś wszędzie z wyjątkiem początku łańcucha. Zasady te mogą być różne w zależności od konkretnej implementacji.
    • Większość znaków specjalnych w tym miejscu traci swoje znaczenie.
  • Pomiędzy nawiasami okrągłymi ( i ) grupuje się symbole do ich późniejszego wykorzystania.
  • Gwiazdka * po symbolu (nawiasie, pojedynczym znaku) nazywana jest domknięciem Kleene’a i oznacza zero lub więcej wystąpień poprzedzającego wyrażenia.
  • Znak zapytania ? po symbolu oznacza najwyżej jedno (być może zero) wystąpienie poprzedzającego wyrażenia.
  • Plus + po symbolu oznacza co najmniej jedno wystąpienie poprzedzającego go wyrażenia.
  • Daszek ^ oznacza początek wiersza, dolar $ oznacza koniec wiersza.
  • Pionowa kreska (ang. pipeline) | to operator OR np., jeśli napiszemy a|b|c, oznacza to, że w danym wyrażeniu może wystąpić a lub b lub c.
  • Znaki \<, \> oznaczające początek i koniec wyrazu (w niektórych implementacjach występuje pełniący podobną funkcję metaznak \b). Np. \<al znajdzie wszystkie wyrazy zaczynające się na al. et\> znajdzie wyrazy które kończą się na et.
Przykład 1

Polski kod pocztowy składa się z sekwencji następujących elementów:

  • dwóch cyfr ([0-9][0-9]),
  • dywizu (-),
  • trzech cyfr ([0-9][0-9][0-9]).

Ostatecznie wyrażenie regularne, które opisuje kod pocztowy [0-9][0-9]-[0-9][0-9][0-9] lub wykorzystując opisane niżej rozszerzenia perla: [0-9]{2}-[0-9]{3}.

Przykład 2

Anglosaski napis reprezentujący liczbę rzeczywistą składa się z następujących elementów:

  • opcjonalnego znaku ([+-]?);
  • przynajmniej jednej cyfry ([0-9]+);
  • opcjonalnej części ułamkowej (?), która składa się z kolei z:
    • kropki dziesiętnej (\.);
    • przynajmniej jednej cyfry ([0-9]+).

Wyrażenie regularne opisujące taki napis: [+-]?[0-9]+(\.[0-9]+)?.

Przykład 3

Wyrażenie sprawdzające poprawność adresów poczty elektronicznej (w wersji uproszczonej, opisujące adresy w najpopularniejszej formie): ^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]{1,})*\.([a-zA-Z]{2,}){1}$.

Rozszerzenia Perla

[edytuj | edytuj kod]

Rozszerzenia Perla to między innymi:

  • Negacja zestawu (wszystko, co nie należy do zestawu).
    • cyfry są zastępowane znakami \d (dowolna cyfra) i \D (wszystko co nie jest cyfrą)
    • znaki „białe” \s i \S (przeciwieństwo)
  • Rozszerzony zapis przedziałów, wprowadzenie klas znaków np.:
    • [:digit:] oznacza dowolną cyfrę
    • [:alpha:] literę
    • [:alnum:] literę lub cyfrę
  • Możliwość precyzyjnego określenia liczby wystąpień danego wyrażenia
    • wyrażenie {N} oznacza dokładnie N wystąpień
    • wyrażenie {N,} co najmniej N wystąpień wyrażenia
    • wyrażenie {,M} co najwyżej M wystąpień wyrażenia
    • wyrażenie {N,M} od N do M wystąpień wyrażenia
  • Referencje wsteczne, czyli możliwość odwoływania się do odnalezionych podciągów zgrupowanych poprzez nawiasy. Np. w wyrażeniu (.*)\1 referencją wsteczną jest „\1” i oznacza powtórzenie ciągu znalezionego w ramach pierwszej grupy nawiasów. To rozszerzenie pozwala definiować języki, które nie są regularne.

Wyrażenia zachłanne

[edytuj | edytuj kod]

Kwantyfikatory w wyrażeniach regularnych dopasowują tak wiele znaków, jak to możliwe, są to więc tzw. wyrażenia zachłanne (greedy – z ang. zachłanne, łapczywe). Może to być znaczącym problemem. Przykładowo, aby dopasować pierwszy element, znajdujący się w podwójnych nawiasach kwadratowych w tekście:

Kolejna eksplozja wieloryba nastąpiła na [[Tajwan]]ie, [[26 stycznia]] [[1990]].

użytkownik użyłby najchętniej wyrażenia (\[\[.*\]\]), które wygląda poprawnie (nawias kwadratowy powinien być interpretowany jako znak, dlatego poprzedzony jest odwrotnym ukośnikiem), jednak zwróci ciąg [[Tajwan]]ie, [[26 stycznia]] [[1990]] zamiast oczekiwanego [[Tajwan]].

Są dwie metody na uniknięcie tego problemu. Po pierwsze, zamiast określać, co powinno być dopasowane, można określić, co nie powinno być dopasowane. W tym przypadku ] jest znakiem niepożądanym, więc wyrażenie miałoby postać (\[\[[^\]]*\]\]). Jednak nie uda się w ten sposób dopasować ciągu znaków w postaci:

A B C D E F G

Drugą, bardziej współczesną metodą, jest „zmuszenie” kwantyfikatora, aby nie był typu „greedy”, poprzez dopisanie za nim znaku zapytania (\[\[.*?\]\]). Są to tak zwane „leniwe” (ang. lazy) odmiany kwantyfikatorów.

Silniki dopasowywania i wydajność

[edytuj | edytuj kod]

Praktyczne implementacje wyrażeń regularnych dzielą się na dwie rodziny. Silniki oparte na automatach skończonych (m.in. grep, AWK i RE2) przekształcają wzorzec w automat i gwarantują czas dopasowania liniowy względem długości przeszukiwanego tekstu, nie obsługują jednak konstrukcji wykraczających poza języki regularne. Silniki z nawrotami (backtracking), stosowane m.in. w Perlu, PCRE, Javie i Pythonie, przeszukują możliwe dopasowania rekurencyjnie – dzięki temu obsługują referencje wsteczne i inne rozszerzenia, ale w najgorszym przypadku ich czas działania rośnie wykładniczo[6][10].

Katastrofalny nawrót i ReDoS

[edytuj | edytuj kod]

W silnikach z nawrotami niektóre wzorce, zwłaszcza z zagnieżdżonymi kwantyfikatorami w rodzaju (a+)+$, prowadzą do tzw. katastrofalnego nawrotu (catastrophic backtracking): liczba sprawdzanych kombinacji rośnie wykładniczo wraz z długością niepasującego tekstu. Celowe wykorzystanie tej własności przeciwko aplikacji nosi nazwę ReDoS (regular expression denial of service) i stanowi odmianę ataku blokady usług[11].

Problem ma znaczenie praktyczne: 20 lipca 2016 roku serwis Stack Overflow przestał odpowiadać na 34 minuty, gdy jedno z wyrażeń użytych do formatowania wpisów wpadło w katastrofalny nawrót na spreparowanym poście[12], a 2 lipca 2019 roku globalna awaria Cloudflare – odcinająca na niemal pół godziny znaczną część ruchu w internecie – została spowodowana wdrożeniem do zapory aplikacyjnej reguły z wyrażeniem powodującym nadmierne nawroty[13]. Do środków zaradczych należą silniki o gwarantowanym liniowym czasie działania (jak RE2), limity czasu dopasowania oraz przepisywanie podatnych wzorców[11][13].

Ograniczenia

[edytuj | edytuj kod]

Choć języki regularne obejmują dowolne skończone zbiory słów, składnia wyrażeń regularnych nie oferuje zwięzłego operatora permutacji – dopasowanie wszystkich ciągów „ABC”, „ACB”, „BAC”, „BCA”, „CAB” i „CBA” wymaga jawnego wyliczenia wariantów[14]. Z lematu o pompowaniu wynika ponadto, że wyrażeniami regularnymi (bez rozszerzeń) nie da się opisać języków wymagających zliczania o nieograniczonej głębokości, takich jak język poprawnie sparowanych nawiasów

Zobacz też

[edytuj | edytuj kod]

Przypisy

[edytuj | edytuj kod]
  1. ↑ Stephen C. Kleene, Representation of Events in Nerve Nets and Finite Automata, RAND Corporation, 15 grudnia 1951 [dostęp 2026-08-21] (ang.).
  2. ↑ Hing Leung, Regular Languages and Finite Automata [online], New Mexico State University, 16 września 2010 [dostęp 2026-08-21] (ang.).
  3. ↑ Ken Thompson, Programming Techniques: Regular expression search algorithm, „Communications of the ACM”, 11 (6), 1968, s. 419–422, DOI: 10.1145/363347.363387 (ang.).
  4. ↑ Dennis M. Ritchie, An incomplete history of the QED Text Editor [online] [dostęp 2026-08-21] [zarchiwizowane z adresu 1999-02-21] (ang.).
  5. ↑ Eric S. Raymond, Jargon File 4.4.7: grep [online], 2003 [dostęp 2026-08-21] [zarchiwizowane z adresu 2011-06-05] (ang.).
  6. 1 2 3 Russ Cox, Regular Expression Matching Can Be Simple And Fast [online], 2007 [dostęp 2026-08-21] (ang.).
  7. ↑ Regular Expressions [online], The Open Group Base Specifications Issue 7 (POSIX) [dostęp 2026-08-21] (ang.).
  8. ↑ PCRE – Perl Compatible Regular Expressions [online], pcre.org [dostęp 2026-08-21] (ang.).
  9. ↑ Regular expressions – JavaScript [online], MDN Web Docs [dostęp 2026-08-21] (ang.).
  10. 1 2 Russ Cox, RE2: a principled approach to regular expression matching [online], Google Open Source Blog, 11 marca 2010 [dostęp 2026-08-21] (ang.).
  11. 1 2 Regular expression Denial of Service – ReDoS [online], OWASP [dostęp 2026-08-21] (ang.).
  12. ↑ Outage Postmortem – July 20, 2016 [online], Stack Status, 20 lipca 2016 [dostęp 2026-08-21] [zarchiwizowane z adresu 2022-10-11] (ang.).
  13. 1 2 John Graham-Cumming, Details of the Cloudflare outage on July 2, 2019 [online], Cloudflare, 12 lipca 2019 [dostęp 2026-08-21] (ang.).
  14. ↑ Why is there no permutation in Regexes? (Even if regular languages seem to be able to do this) [online], Computer Science Stack Exchange, grudzień 2018 [dostęp 2026-08-21] (ang.).

Bibliografia

[edytuj | edytuj kod]
  • Jeffrey E.F. Friedl, Wyrażenia regularne, Gliwice: Helion O'Reilly, 2001, ISBN 83-7197-351-9 (pol.).
  • Tony Stubblebine, Wyrażenia regularne leksykon kieszonkowy Wydanie I, Gliwice: Helion O'Reilly, grudzień 2003, ISBN 83-7361-075-8.
  • Tony Stubblebine, Wyrażenia regularne leksykon kieszonkowy Wydanie II, Gliwice: Helion O'Reilly, styczeń 2008, ISBN 978-83-246-1392-2.
  • John E Hopcroft, Jeffrey D Ullman, Beata Konikowska, Wprowadzenie do teorii automatów, języków i obliczeń, Warszawa: Wydawnictwo Naukowe PWN, 2003, ISBN 83-01-14090-9.

Linki zewnętrzne

[edytuj | edytuj kod]