Kompleksowa obsługa informatyczna

            Nowoczesne rozwiązania serwerowe

Wyrażenia regularne

Poradniki

Chcesz sprawdzić poprawność adresu IP? Próbujesz wyciąć z tekstu znaczniki html? A może planujesz stworzyć listę firmowych adresów e-mail mając imiona i nazwiska wszystkich pracowników? We wszystkich tych zadniach pomogą ci wyrażenia regularne!

Czym są wyrażenia regularne?

Wyrażenia regularne (ang.: regular expressions, w skrócie regexp lub regex) są wzorcami, które opisują ciąg znaków.
W tym artykule nie będziemy bardziej zagłębiać się w teorię. Zamiast tego postaramy się pokazać zasady budowy i użyteczne przykłady użycia regexpów.

Proste prykłady

Załóżmy, że dany mamy plik wejściowy o następującej treści:
Ala ma kota.
Gosia ma kanarka.
Marek ma psa.
Wojtek ma kota.	
Bardzo prosto możemy znaleść wszystkie osoby, które mają kota:
$ grep kota /tmp/regexp
Ala ma kota.
Wojtek ma kota.	
Co jednak zrobić gdy chcemy wyszukać osoby, które posiadają kota lub psa? Możemy oczywiście zastosować 2 przebiegi programu grep:
$ grep kota /tmp/regexp 
Ala ma kota.
Wojtek ma kota.
$ grep psa /tmp/regexp 
Marek ma psa.

Rozwiązanie to ma jednak co najmniej dwie wady. Po pierwsze cały plik wejściowy odczytywany jest dwukrotnie (co może nie ma znaczenia w naszym prostym przykładzie, ale zapewniam, że będzie miało znaczenie przy pliku zawierającym kilka milionów linii), po drugie zaś - linijki w otrzymanym wyniku nie występują w kolejności, w jakiej występowały w pliku wejściowym, co niejednokrotnie jest poważnym problemem. Sprobujmy więc wykorzystać prościutkie wyrażenie regularne:

$ pcregrep '(kota|psa)' /tmp/regexp 
Ala ma kota.
Marek ma psa.
Wojtek ma kota.	

Co tak naprawdę się stało? znak | oznacza logiczne 'lub'. Nakazaliśmy więc grepowi wyszukiwanie słowa 'kota' lub 'psa'. Wynik jest zgodny z oczekiwanym. Pewnie zauważyłeś, że wywołany został program pcregrep zamiast zwykłego grep. Wynika to z faktu, iż grep domyślnie korzysta z najprostszych wyrażeń regularnych, które na potzreby tego artykułu nie będą wystarczające. Program pcregrep korzysta z wyrażeń perlowych, które są bodaj najbardziej rozwinięte. Ten sam efekt uzyskamy uruchamiając zwykłego grepa z parametrem -P.

Opis znaków specjalnych.

Zanim przejdziemy do bardziej zaawansowanych przykładów, warto przyjrzeć się poniższej tabeli kwantyfikatorów i znaków specjalnych:

znakznaczenie
\traktuj następny znak, jako znak zwykły, nie specjalny
^początek linii
$koniec linii
.dowolny znak
|logiczne 'lub'
()grupowanie
[] klasa znaków
*wystąpienie 0 lub więcej razy
+wystąpienie 1 lub więcej razy
?wystąpienie 0 lub 1 razy
{n}wystąpienie dokładnie n razy
{n,}wystąpienie co najmniej n razy
{n,m}wystąpienie co najmniej n i co najwyżej m razy
Na głębszy opis zasługują nawiasy kwadratowe, które mogą mieć różne znaczenie.
  • [0-9] - oznacza dowolną cyfrę z zadanego przedziału. Podobnie możemy określać przedziały liter np.: [b-d], co oznacza b, c lub d
  • [[:digit:]] - podanie nazwy klasy (w tym przypadku cyfry, czyli równoważne do [0-9]). Kompletny spis dostępnych klas można znaleźć w internecie
  • [AB5f] - oznacza dowolny z wymienionych znaków, czyli w tym przypadku pojedynczą literę A, B, f lub cyfrę 5
  • [^ ] - znak ^ na początku nawiasu klamrowego oznacza zaprzeczenie. W tym przypadku będzie to więc dowolny znak za wyjątkiem spacji

Załóżmy więc, że dane mamy wyrażenie:
^A.*B{2}(C|D){2,3}
Oznacza ono: dopasuj łańcuch znaków, który:
  • ^A - na pierwszej pozycji zawiera literę A
  • .* - po czym występuje dowolny ciąg znaków
  • B{2} - dokładnie dwie litery B
  • (C|D){2,3} - litera C lub D powtórzona 2 lub 3 razy

Sprawdźmy jak to działa:
1. $ echo ABBCC |pcregrep '^A.*B{2}(C|D){2,3}'
ABBCC

2. $ echo AABBCC |pcregrep '^A.*B{2}(C|D){2,3}'
AABBCC

3. $ echo ABBBCD |pcregrep '^A.*B{2}(C|D){2,3}'
ABBBCD

4. $ echo ABBBCDDDD |pcregrep '^A.*B{2}(C|D){2,3}'
ABBBCDDDD

5. $ echo ABBBCDDDD |pcregrep '^A.*B{2}(C|D){2,3}$'

6. $ echo BABBBCDDDD |pcregrep '^A.*B{2}(C|D){2,3}'

7. $ echo BABBBCDDDD |pcregrep 'A.*B{2}(C|D){2,3}'
BABBBCDDDD
  • Pierwszy przykład chyba nie wymaga komentarza. Wynika wprost z powyższej rozpiski wyrażenia.
  • Drugi przykład również pasuje do wzorca, ponieważ we wzorcu określiliśmy, iż po literze A ma nastąpić 'dowolny ciąg znaków'. Oznacza to zatem, że międzyliterami A i B możemy wstawić cokolwiek, a nasz łańcuch nadal będzie pasował do wzorca.
  • Z tego samego powodu zadziałał przykład trzeci.
  • Lecz co stało się w przykładzie czwartym? We wzorcu wyraźnie określiliśmy, że litera C lub D powinna wystąpić nie więcej, niż 3 razy. Czy zatem grep się pomylił? Wszystko jest w należytym porządku. Co prawda we wzorcu określiliśmy maksymalną ilość wystąpień C lub D, nie zaznaczyliśmy jednak, że po ich wystąpieniu łańcuch powinien się zakończyć. W praktyce więc po 3 literach C lub D może wystąpić dowolny ciąg znaków, a nasz łańcuch i tak zostanie dopasowany do zadanego wzorca.
  • Przykład piąty pokazuje jak pozbyć się tego problemu. Poprzez dodanie na końcu wzorca znaku $ dajemy znać grepowi, że nie chcemy, aby po naszym zdefiniowanym wzorcu występowało cokolwiek dodatkowego.
  • Przykład szósty nie dopasował łańcuch do wzorca, gdyż we wzorcu ustaliliśmy, że pierwszą literą ma być litera A. Kluczowy był tu znak ^, który oznacza 'dopasuj od początku łańcucha'.
  • W przykładzie siódmym znak ten został usunięty, co z grubsza oznacza 'znajdź podciąg znaków spełniający nastepujące warunki w dowolnym miejscu zadanego łańcucha'. Nasz tekst został więc poprawnie dopasowany mimo obecnoścli litery B na początku.

Wyrażenia zachłanne i nie zachałnne (greedy i non-greedy)

Aby nie zaciemniać sytuacji, nie wspominaliśmy wcześniej o zachłanności wyrażeń. Domyślnie wyrażenia regularne dopasowywane są w sposób zachłanny. Oznacza to, że następuje próba dopasowania tak dużej ilości danych, jak jest to możliwe. Parsowanie nie zachłanne oznacza, aby poprzestać dopasowywanie, gdy tylko mamy już pasujący tekst.

Dla przejrzystości spróbujmy spojrzeć na poniższy przykład (parametr -o oznacza dla grepa wypisanie tylko pasujących podciągów):

1. $ echo 10:23:45 |pcregrep -o '.*:'
10:23:
2. $ echo 10:23:45 |pcregrep -o '.*?:'
10:
23:

Chieliśmy otrzymać samą godzinę z ciągu zawierającego czas w formacie hh:mm:ss. W przykładzie pierwszym grep użył algorytmu zachłannego, a więc dopasowywał tak wiele znaków, jak jest to możliwe. poprzestał dopiero na ostatnim znaku :, ponieważ zadaliśmy warunek, iż taki znak powinien wystąpić na końcu podciągu.

W przykładzie drugim dodaliśmy do wzorca znak ? po .*. Oznacza on wymuszenie algorytmu niezachłannego, a więc grep porzestaje parsować łańcuch jak tylko znajdzie najmniejszy pasujący podciąg. Efektem tego jest wypisanie dwóch pasujących, lecz krótszych niż w przykładzie pierwszym podciągów.

Wcześniej napisaliśmy, że znak ? oznacza '0 lub jedno wystąpienie'. Wszystko się zgadza - ma on podwójną rolę i możesz mi zaufać - po nabraniu doświadczenia z regexpami, stwierdzisz, że nie jest to żadnym problemem;)

Użyteczne zastosowania

Nadszedł czas, abyśmy wykorzstali zdobytą wiedzę na konkretnych zadaniach.


Przykład 1 - adresy IP

Załóżmy, że mamy ciąg znaków, z którego chcemy wyodrębnić wszystkie poprawne adresy IP. Adres IP zdefiniujemy jako poprawny, jeślijest postaci: XXX.XXX.XXX.XXX, zawiera same cyfry (oraz rozdzielające kropki) i na każdej z czterech pozycji ma co najmniej jedną i co najwyżej 3 cyfry.

echo 192.168.1.2 sad aas 10.4.11.213 ndsd aa.44.ddd.1 1234.53..2 \ 
	|pcregrep -o '(^| )([0-9]{1,3}\.){3}[0-9]{1,3}($| )'
	
Wytlumaczmy jak to dziala:
  • (^| ) - podciąg zaczyna się od spacji lub początku linii (dzięki temu nie zostanie dopasowany adres z więcej niż trzema cyframi na początku)
  • [0-9] - dowolna cyfra z przedziału od 0 do 9
  • [0-9]{1,3} - dowolna cyfra ma wystąpić minimum 1, a maksimum 3 razy (może oczywiście każdorazowo być inna)
  • [0-9]{1,3}\. - po sekwencji cyfr występuje kropka. Znak \ jest tu konieczny, gdyż sama kropka oznacza dowolny znak. Zapis \. oznacza, że kropka ma być interpretowana jako zwykła kropka
  • ([0-9]{1,3}\.){3} - całość ma być powtórzona dokładnie 3 razy. Mymy więc w tym momencie ciąg XXX.XXX.XXX. Czwartej pozycji nie możemy do niego dołączyc w podobny sposób, gdyż po czwartm bajcie w adresie IP nie występuje już kropka
  • [0-9]{1,3} - następnie 3 dowolne cyfry
  • ($| ) - i całość ma się zakończyc końcem linii lub spacją


Przykład 2 - wycinanie tagów html

Wejściowy plik html mamy zamienić na zwykły tekst pozbawiony wszelkich znaczników.

Napiszmy prosciutki plik html:
<html><body>
<a href="jakis_link">link</a>
<p>paragraf
dwulinijkowy
</p>
</body></html>
</div>

W tym przykładzie wykorzystamy program sed, a konkretnie jedną z jego funkcji, jaką jest podmiana podciągu na inny ciąg. składnia tego polecenia, to s/jakis tekst do podmiany/nowy tekst/g. Ponieważ opis seda nie jest przedmiotem tego artykuły, nie będziemy wgłębiać się tu w dalsze szczegóły.

$ sed -r 's/<[^>]*>//g' /tmp/regexp 

link
paragraf
dwulinijkowy



	

Jak to działa? Pierwsza część polecenia (<[^>]*>) określa podciąg do podmiany. Druga (w naszym wypadku pusta) określa na co mamy podmienić.

  • < - zacznij od znaku otwierającego tag htmla
  • [^>]* - następnie występują dowolne znaki, ale nie znacznik zamykający
  • > - znacznik zamykający tag

Całość jest zamieniana na ciąg pusty, czyli w zasadzie kasujemy wszystko od znaku < aż po >. Program sed niestety nie obsługuje niezachłannych wyrażeń regularnych, który uprościłyby tu sprawę. Moglibyśmy wówczas użyć wzorca <.*?> - polecam sprawdzenie samemu jaki będzie efekt działania takiej podmiany.



Przykład 3 - adresy e-mail

Dany jest plik, w którym znajdują się Imiona i nazwiska osób, umieszczone po jednym w każdej linijce.

$ cat /tmp/regexp 
Adam Kowalski
Anna Nowak
Tomasz Adamski

Naszym zadaniem jest zbudowanie listy adresów email postaci: a.nowak@domena.pl, czyli inaczej mówiąc: pierwsza litera imienia, po której występuje kropka, następnie nazwisko i stały ciąg @domena.pl.

Podobnie jak wyżej użyjemy tu programu sed. Dodatkowo wykorzystamy program tr do zamiany dużych liter na małe.

$ sed -r 's/(.)[^ ]* (.*)/\1.\2@domena.pl/' /tmp/regexp |tr [A-Z] [a-z]
a.kowalski@domena.pl
a.nowak@domena.pl
t.adamski@domena.pl

Wspomnieć należy, że sed numeruje kolejne grupy (podciągi umieszczone wewnątrz okrągłych nawiasów), a dostęp do nich uzyskiwany jest przez \n, gdzie n to numer danej grupy. Całe wyrażenie regularne, które będziemy podmieniać zawiera się w ciągu: (.)[^ ]* (.*), gdzie:

  • (.) - pierwsza grupa zawierająca tylko jeden znak (pierwsza litera imienia)
  • [^ ]* - dowolny znak za wyjątkiem spacji powielony dowolną ilość razy - następnie następuje spacja
  • (.*) - i druga grupa zawierająca wszystko, co znajduje się za spacją (a więc nazwisko)
Druga część komendy - \1.\2@domena.pl może być przetłumaczona jako:
  • \1 - umieść pierwszą grupę (czyli pierwszą literę imienia)
  • . - następnie umieść kropkę
  • \2 - następnie umieść drugą grupę (nazwisko)
  • @domena.pl - doklej stałą