Wyrażenia regularne – podstawowe informacje

W artykule staram się omówić czym są wyrażenia regularne i wprowadzić czytelnika w podstawy tworzenia obiektów RegExp. Tematykę wyrażeń regularnych podzieliłem na kilka wpisów wprowadzając stopniowo różne elementy składniowe z analizą konkretnych przykładów.

Gdy sam zaczynałem przygodę z językiem JavaScript w wielu poradnikach spotykałem tematykę wyrażeń regularnych, lecz najczęściej ich opisy ograniczały się do tabelarycznego zestawienia całej (lub tylko częściowej) składni i podania kilku stałych, powielanych w internecie przykładów. Wiem z doświadczenia, że taki natłok informacji zniechęca do nauki RegExp, dlatego chcę w moich wpisach po kolei wprowadzać coraz więcej informacji z dokładną analizą różnych przykładów.

Aby móc korzystać z wyrażeń regularnych w języku JavaScript musimy nauczyć się również korzystania z kilku metod obiektu String (search, match, replace, split) oraz obiektu RegExp (test i exec). W tym artykule zajmiemy się dwoma, tj. metodą match i test.

Czym są wyrażenia regularne?

Wyrażenia regularne, najogólniej mówiąc są specjalnie sformułowanymi wzorcami dopasowującymi się do innych ciągów znakowych. W praktyce istnieje wiele sytuacji gdzie wyrażenia regularne znajdują zastosowanie. W programowaniu aplikacji internetowych z wykorzystaniem JavaScript najczęściej wzorce RegExp stosuje się w celu weryfikacji różnych wartości, np. pochodzących z formularzy.

Wyrażenia regularne to jednak nie tylko walidacja danych, ale przede wszystkim pewnego rodzaju „inteligentne” przeszukiwanie ciągów znakowych. Załóżmy na przykład, że mamy ciąg zawierający tekst „Mój numer telefonu to 607-784-441”. Naszym zadaniem jest przeszukanie ciągu i wyodrębnienie z niego tylko samego numeru telefonu.

Nie możemy po prostu wyszukać wartości „607-784-441” gdyż za chwilę otrzymamy inny ciąg znakowy, zawierający inny numer telefonu. Wyrażenia regularne pozwalają na stworzenie uniwersalnego wzorca dopasowania. Spójrzmy na poniższy przykład:

var str = 'Mój numer telefonu to 607-784-441',
    tel = str.match(/(?:\d{3}-){2}\d{3}/); 

tel; // ["607-784-441"]

Na razie nie przejmuj się jeżeli nie rozumiesz składni zastosowanego tu wyrażenia regularnego (omówimy to w kolejnych artykułach). Nigdy nie popełniaj jednak błędu początkujących programistów – nie ucz się wyrażeń regularnych na pamięć!

Jest to bardzo zgubne. Na przykład w naszym wyrażeniu zakładamy, że numer telefonu zawsze składa się z grup po trzy cyfry, które oddzielone myślnikiem. A co jeśli w przeszukiwanym tekście ktoś poda numer bez myślników lub zamiast nich użyje spacji?

W projektach produkcyjnych z reguły mamy do czynienia z dwoma przypadkami. Jeśli zakładamy konieczność podania określonych danych w dokładnie sprecyzowany sposób to moglibyśmy skorzystać właśnie z takiego wyrażenia, lecz użytkownik musiałby zostać poinformowany o sposobie wprowadzania danych (tutaj numeru telefonu). Nie jest to jednak poprawne wykorzystanie wyrażeń regularnych. Dobre wzorce dopasowania powinny pozwalać na pewną elastyczność i przewidywać to, w jaki sposób użytkownik może wprowadzić interesujące nas dane. I właśnie w tym tkwi cała magia wyrażeń regularnych. Dlatego nie należy ich traktować jako sposobu na wyszukiwanie ciągów znakowych lecz jako metody sprawdzającej dopasowania wzorców.

Jak tworzy się wyrażenia regularne w JavaScript?

W języku JavaScript wyrażenia regularne można tworzyć na dwa sposoby: z wykorzystaniem składki literału „/ … /” lub tworząc jawnie nowy obiekt typu RegExp.

var regA = /[A-z]/g,
    regB = new RegExp('[A-z]', 'g'),
    regC = RegExp('[A-z]', 'g');

regA.constructor.name; //'RegExp'
regB.constructor.name; //'RegExp'
regC.constructor.name; //'RegExp'

Wszystkie trzy metody są poprawne, zatem którą formę wybrać? To zależy – jeżeli znasz dokładnie składnię wyrażenia regularnego, które stosujesz (z góry określasz wszystkie elementy wzorca) to lepiej stosować krótszą składnię z literałem „/ … /”. Jawne tworzenie obiektu RegExp jest używane w sytuacjach, gdy wzorzec może się dynamicznie zmieniać, np. jego część jest przekazywana jako parametr funkcji. W tej sytuacji w przypadku użycia konstrukcji RegExp() lub new RegExp() możemy w jego wnętrzu stosować zmienne.

Załóżmy, że mamy jakiś ciąg znakowy str. Naszym zadaniem jest napisanie funkcji, która pobiera jeden parametr – ciąg znakowy do wyszukania w zmiennej str, ale zależy nam, aby funkcja szukała wskazanego tekstu bez względu na to czy jest on napisany z użyciem wielkich czy małych liter alfabetu. Zwrotnie chcemy otrzymać ilość znalezionych dopasowań.

var str = 'To jest kot Ali. Kot jest rudy.';

function searchText(search) {
   var reg = new RegExp(search, 'gi');
   return (str.match(reg) || []).length;
}

searchText('kot'); //2
searchText('pies'); //0

Metoda match() szukająca dopasowań do wzorca RegExp

Jeśli nie korzystałeś nigdy z wyrażeń regularnych to pewnie zdziwił Cię zapis z instrukcji return naszej funkcji. Omówmy zatem krótko co właściwie robi metoda match.

Na wstępnie należy powiedzieć, że jest to metoda obiektu String, a więc musi zostać wywołana na zmiennej str (typeof str === ‚string’), a nie na zmiennej reg.

Metoda match pobiera jeden argument – wyrażenie regularne, które będzie próbowała dopasować do obiektu string, na którym została wywołana (w naszym przypadku na zmiennej str).

Jeśli metoda match nie znajdzie żadnego dopasowania wyrażenia regularnego to nie zwraca pustej tablicy, lecz wartość null. Należy o tym pamiętać, gdyż właśnie z tego powodu w naszej instrukcji return znalazł się zapis:

return (str.match(reg) || []).length;

Właściwość length posiadają ciągi znakowe (string) i tablice, ale nie posiada jej wartość null. Gdybyśmy zastosowali zapis str.match(reg).length to w przypadku nie znalezienia żadnego dopasowania wystąpi błąd TypeError. W naszym przykładzie operator „||” sprawdza, czy lewa strona jest wartością konwertowaną do true i jeśli tak to zwraca ją, a w przeciwnym razie zwróci pustą tablicę ” [] „. Wartość specjalna null zostanie przekonwertowana na fałsz, a więc wywołanie length będzie dotyczyć pustej tablicy, czyli zwrotnie otrzymamy po prostu „0”.

Gdy metoda match znajdzie jedno lub więcej dopasowań to zwróci je w formie tablicy (obiekt Array). Tablica ta posiada wszystkie metody i właściwości z dziedziczenia prototypowego po obiekcie globalnym Array, możemy więc wywoływać na niej metody map, filterforEach itp. oraz odnosić się do właściwości (length).

Tak na prawdę tablica zwracana przez metodę match zawiera również pewne dodatkowe właściwości, które nie są obecne z „zwykłych” tablicach ale nimi zajmiemy się w dalszych artykułach.

Metoda test() sprawdzająca wystąpienie wyrażenia regularnego

Podczas gdy metoda match() umożliwia pozyskanie informacji o ilości dopasowanych wzorców i zwrócenia ich, istnieje również druga, często stosowana metoda test().

Metoda ta wywoływana jest na obiekcie RegExp (wzorcu wyrażenia regularnego) i pobiera jeden argument – ciąg znakowy do przeszukania. Metoda zwraca wartość typu boolean true/false w zależności od tego czy udało się dopasować wzorzec co najmniej jeden raz.

Jest ona często wykorzystywana podczas weryfikacji poprawności danych wprowadzanych przez użytkownika.

var reg = /^\d{2}-\d{3}$/g

reg.test('62-023'); //true
reg.test('63100'); //false
reg.test('abc 62-023'); //false

Naszym zadaniem jest sprawdzenie, czy podany kod pocztowy został zapisany w formacie xx-xxx. Nie dopuszczamy pomięcia myślnika, dlatego drugie wywołanie metody test zwraca false. Ciąg musi zawierać wyłącznie poprawny kod pocztowy bez dodatkowych znaków, więc kolejne wywołanie również zwróci false. Wartość true zwrócona zostanie tylko dla pierwszego wywołania.

Podsumowanie

Celem tego artykułu nie jest nauka tworzenia wyrażeń regularnych, dlatego nie wyjaśniałem tutaj składni stosowanej w omawianych przykładach. Chciałem tylko przekonać Cię, że wyrażenia regularne to na prawdę potężne narzędzie w rękach każdego programisty.

W wielu przypadkach jednolinijkowe wyrażenie regularne potrafi zastąpić rozbudowane funkcje walidujące. Nie są one jednak antidotum na wszystkie problemy, gdyż przeszukiwanie ciągów na podstawie wzorców RegExp jest znacznie wolniejsze niż użycie metod typu includes czy indexOf. W kolejnych wpisach postaram się stopniowo przybliżyć składnię wyrażeń regularnych na konkretnych przykładach, abyś sam był w stanie ocenić, kiedy warto zastosować RegExp, a kiedy pozostać przy innych metodach obiektu String.