Więcej w(y)rażeń i o nich
Składnia wyrażeń regularnych jest znacznie bogatsza niż to przedstawiono w poprzednim poście i bardziej rozbudowana, niż zostanie to ujęte tutaj. Tym niemniej niniejszy wpis zamyka opis ich kluczowej części, poza którą większość użytkowników regexpów nigdy nie wychodzi
Alternatywne dopasowania i nawiasy
Załóżmy, że w znanym nam pliku slowoformy.txt, chcemy odnaleźć wszystkie słowa zawierające przedrostki pochodzenia greckiego, takie jak aero-, andro-, antropo- czy archeo-. Realizacja tego zadania (przy użyciu jednego wyrażenia) wymaga wprowadzenia dotychczas niewpominanego symbolu |, za którym kryje się alternatywa.

Wizualizacja wyrażenia ^aero|^andro|^antropo|^archeo.
Słowa rozpoczęte albo na aero-, albo na andro-, albo na antropo-, albo na archeo- odnajdziemy poleceniem grep -E '^aero|^andro|^antropo|^archeo' slowoformy.txt. Gdybyśmy nie zastosowali znaku ^, oznaczającego – jak pamiętamy – początek dopasowania (zakotwiczenia), to zwrócone byłyby również takie wyrazy jak synantropom czy salamandrowatych.

Wizualizacja wyrażenia ^(aer|andr|antrop|arche)o.
Mimo to, wspomniana powtarzalność doprasza się uproszczenia u każdej osoby, której nauczyciel matematyki krzywił się na widok wyniku 5x + 5y i nakazywał wyciągnięcie wspólnego czynnika przed nawias, tak by działanie kończyło się równoważnym 5(x + y). Tutaj też możemy „wyciągnąć wspólny czynnik przed nawias” i zapisać grep -E '^(aero|andro|antropo|archeo)' slowoformy.txt, a ktoś nadgorliwy mógłby również (ze stratą dla czytelności) ująć tę samą regułę jako grep -E '^(aer|andr|antrop|arche)o' slowoformy.txt.
Jak znaleźć kropkę? O znakach ucieczki (modyfikacji)
Chcąc szukać symboli specjalnych, dla których w formalizmie wyrażeń regularnych zarezerwowano pewne funkcje (takich jak kropka oznaczająca dowolny znak), należy je poprzedzić symbolem \ (mówimy, że jest on znakiem ucieczki lub modyfikacji) i tak:
- by wyszukać linie zakończone kropką zastosowalibyśmy grep -E '\.$' plik.txt, a nie grep -E '.$' plik.txt (to drugie wyrażenie odnalazłoby linie zakończone dowolnym znakiem);
- by wyszukać linie rozpoczęte dwoma znakami ^, użylibyśmy grep -E '^\^\^' plik.txt;
- żeby odnaleźć linie zawierające backslash użylibyśmy grep -E '\\' plik.txt (backslash poprzedzony backslashem).
Podobnie postąpilibyśmy w przypadku znaków takich jak $, *, +, ?, (, ), [, { czy | (o niektórych z nich nie było jeszcze mowy).
Skrócone oznaczenia klas (zbiorów) znaków
W poprzedniej części przedstawione zostały klasy znaków, takie jak [A-Za-z] czy [0-9]. Poza nimi istnieją skrócone oznaczenia pewnych popularnych klas, które przedstawiono poniżej.
- \s odpowiada na ogół klasie [ \t\r\n\f], tj. zawiera różnego typu białe znaki, takie jak spacja, tabulacja czy też znak przejścia do nowej linii.
- \w obejmuje znaki umownie zaliczane do mogących tworzyć słowa (stąd w, od word character) – zawiera w sobie m.in. zbiór, który moglibyśmy zdefiniować jako [A-Za-z0-9_], ale poza nim zawiera także opatrzone diakrytami znaki języków narodowych czy innego typu grafemy niewchodzące w skład łacinki (takie jak polskie [ąćęłńóśżźĄĆĘŁŃÓŚŻŹ] czy niemieckie [äöüÄÖÜß]). Innymi słowy obejmuje on (w przypadku większości implementacji) dowolne wielkie lub małe litery, cyfry oraz znak _.
- \d jest synonimem [0-9] i jest równoznaczny [0123456789], czyli należy go rozumieć jako zbiór wszystkich cyfr arabskich.
Skrócone oznaczenia stosować można w nawiasach kwadratowych, albo bez nich. W tym pierwszym przypadku używać ich można do wyłączenia pewnych znaków ze zbioru, o czym była mowa poprzednio. Przykładowo, [^\d] oznacza dowolny znak, który nie jest cyfrą i jest równoznaczne znanemu już [^0-9], a [^\s] dopasuje się do dowolnego znaku, który nie jest spacją, tabulatorem, znakiem nowej linii lub innym białym.
I te wyłączenia mają jednak zapisy skrócone – przyjęto konwencję, zgodnie z którą wyłączenie danej skróconej klasy oznaczyć można taką samą literą, ale wielką w miejsce małej tak że \S jest równoznaczne [^\s], \D odpowiada [^\d], zaś \W to to samo co [^\w].
Granica słowa
Znak \b wygląda jak skrócone oznaczenie klasy przedstawione wyżej, ale jego zasada działania bliższa jest ^ czy $, które stosowaliśmy do zakotwiczenia dopasowania (rozpoczęcia go od początku i/lub końca).
Załóżmy, że posiadamy plik o poniższej zawartości i chcemy wykorzystać komendę grep do zwrócenia jedynie tych linii, które zawierają słowo mózg w formie podstawowej (podświetlone):
1 2 3 4 5 6 7 8 9 |
Największą powierzchnię kory mózgowej mają ludzie, dalej znajduje się kadra zarządzająca banków, delfiny i nasi kuzyni, małpy człekokształtne. Mózg jest Twoim najważniejszym organem. Według mózgu. Naród może mieć jedną duszę, jedno serce, jedną pierś, którą nadstawia, ale biada, gdy ma tylko jeden mózg. Gdyby ludzki mózg był tak prosty, że moglibyśmy go zrozumieć, bylibyśmy wtedy tak głupi, że nie zrozumielibyśmy go i tak. Bezmózg. |
Zauważmy, że:
![[Mm]ózg[\s\.]](http://www.borchmann.pl/wp-content/uploads/2016/10/4-300x122.png)
Wizualizacja wyrażenia [Mm]ózg[\s\.].
- nie możemy zastosować polecenia grep -E 'mózg' plik.txt, ponieważ zwrócona zostanie m.in. linia 1. (ze względu na słowo mózgowej), której nie chcemy;
- w przypadku użycia grep -E '\smózg\s' plik.txt (słowo mózg między białymi znakami) nie zostanie zwrócona linia 3. (ponieważ słowo Mózg znajduje się na początku linii, a poza tym jest pisane wielką literą), ani 6. (ponieważ po słowie mózg znajduje się kropka);
- zadania nie spełni grep -E '[Mm]ózg[\s\.]' plik.txt, ani grep -Ei 'mózg[\s\.]' plik.txt, ponieważ dopasowana zostanie ostatnia linia ze słowem Bezmózg.
Z pomocą przychodzi wspomniane \b, oznaczające granicę słowa, którą może być początek linii, koniec linii, biały znak, znak interpunkcyjny lub inny nieobecny w klasie (zbiorze) \w. Poprawna komenda to grep -E '\bmózg\b' plik.txt.
Garść przykładów
Zaprezentowana część formalizmu wyrażeń regularnych ma bardzo duże możliwości. Poniżej kilka bardziej złożonych przykładów jej zastosowania:
- ^(19|20)\d{2}$ – rok z przedziału 1900-2099;
- ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$ – adres e-mail (wyrażenie skuteczne w miażdżącej większości przypadków);
- \b(50|51|53|57|60|66|69|72|73|78|79|88)\d[\s-](\d{3}[\s-]\d{3}|\d{2}[\s-]\d{2}[\s-]\d{2})\b – polskie numery telefonów komórkowych wewnątrz tekstu, zapisane na różne sposoby (wyrażenie wykorzystane w moim artykule);
- ^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$ – adres strony internetowej.
Przy ich czytaniu (lub szukaniu błędów w napisanych przez nas wzorcach) można wspomóc się narzędziem do wizualizacji, np. jex.im/regulex/, debuggex.com czy regexper.com, z którego to pochodzą schematy umieszczone w niniejszym wpisie.
Uwagi końcowe
Zachowanie wyrażeń regularnych może się w niewielkim stopniu różnić w zależności od programu czy języka programowania, w którym je wykorzystujemy – powyższe uwagi mogą okazać się więc w jakimś użyciu częściowo nieadekwatne, mimo, że ich zasadnicza część będzie bezwyjątkowo prawdziwa.
Wartościowymi źródłami wiedzy o zaawansowanych wyrażeniach regularnych jest strona internetowa regular-expressions.info oraz książka Mastering Regular Expressions, którą pożyczyć można w niezastąpionym serwisie stworzonym przez rodaków Perelmana.