Wyrażenia regularne
Wyrażenia regularne stanowią doskonały sposób na badanie i modyfikowanie tekstu. Dzięki swej olbrzymiej elastyczności pozwalają w łatwy sposób pobierać pasujące fragmenty tekstu. To tyle książkowej teorii.
A czym są wzorce w praktyce?
Powiedzmy, że mamy fragment tekstu, w którym znajdują się jakieś kody pocztowe. Kod taki składa się z dwóch cyfr, myślnika, po którym występują trzy cyfry. Jeżeli chcielibyśmy znaleźć w takim tekście wszystkie kody, nie moglibyśmy użyć znanych nam już indexOf() czy includes(), ponieważ nie znamy konkretnej wartości. Znamy wzorzec.
Wyrażenia regularne nie sa domeną Javascriptu. Gdy nauczymy się ich - nawet w podstawowej formie - bardzo ułatwią nam codzienną prace z kodem. Za ich pomocą możemy dla przykładu w każdym porządnym edytorze zamieniać podobne wystąpienia tekstu (np. klasy, daty itp), masowo zmieniać nazwy plików itp. Poniżej przykład takiej zamiany:
Pisanie podstawowych wzorów wyrażeń nie jest jakieś bardzo skomplikowane. Problem pojawia się przy tych bardziej rozbudowanych. Tutaj częstokroć trzeba korzystać z Google szukając frazy "fraza test regexp" i posiłkować się poradami ze Stack Overflow.
Bardzo też przydają się tutaj narzędzia takie jak: https://regex101.com/, czy http://regexr.com/.
Na koniec warto też zerknąć na stronkę https://regexlearn.com, która interaktywnie uczy pisać takie wyrażenia.
Wyrażenia w Javascript
Aby w JavaScript korzystać z wyrażeń regularnych, możemy posłużyć się skróconym zapisem /wzor/
lub użyć konstruktora RegExp(wzór, flagi*)
, który przyjmuje
2 argumenty: wzór, którym będziemy testować, oraz dodatkowe flagi, które poznamy poniżej.
const reg = /pani?/gi
//lub za pomocą konstruktora
const reg = new RegExp("pani?" , "gi")
Gdy już stworzymy wzór, musimy go użyć wraz z jedną z dostępnych metod. Je omówimy sobie w kolejnym rozdziale. W poniższych przykładach będe korzystał głównie z metody test()
, która zwraca prawdę lub fałsz.
const reg = /[0-9]{3}/
console.log(reg.test("Ala")) //false
console.log(reg.test("102")) //true "*102*"
console.log(reg.test("Ala 007")) //true "Ala *007*"
console.log(reg.test("Numer czołgu miał numer 102 bo tak")) //true "Numer czołgu miał numer *102* bo tak"
Flagi czyli dodatkowe opcje dla wyrażeń
Dla każdego wyrażenia regularnego możemy ustawić dodatkowe flagi (opcje), które zmieniają jego działanie:
Przy skróconej składni flagi umieszczamy za wyrażeniem regularnym. Dla obiektu RegExp umieszczamy je jako drugi parametr:
console.log( /Kot/gi );
//lub
const reg = new RegExp("Kot", "gi");
console.log( reg.test(txt) );
znak Flagi | znaczenie |
---|---|
i | powoduje niebranie pod uwagę wielkości liter |
const txt = "kot i pies, pies i kot";
console.log( /Kot/.test(txt) ) //false
console.log( /Kot/i.test(txt) ) //true "*kot* i pies, pies i kot"
znak Flagi | znaczenie |
---|---|
g | powoduje zwracanie wszystkich pasujących fragmentów, a nie tylko pierwszego |
const txt = "kot i pies, pies i kot";
console.log( /kot/.test(txt) ) //true "*kot* i pies, pies i kot"
console.log( /kot/g.test(txt) ) //true "*kot* i pies, pies i *kot*"
console.log( /Pies/gi.test(txt) ) //true "kot i *pies*, *pies* i kot"
znak Flagi | znaczenie |
---|---|
m | powoduje wyszukiwanie w tekście kilku liniowym. W trybie tym znak początku i końca wzorca (^$) jest wstawiany dla każdej linii z osobna. |
const txt = `
To jest Ania
To jest Marcin
`;
//sprawdzam czy Ania jest na końcu pojedynczej linii
console.log( /Ania$/.test(txt) ) //false
console.log( /Ania$/m.test(txt) ) //true
znak Flagi | znaczenie |
---|---|
s | Sprawia, że znak . pasuje także do znaku nowe linii (\n) |
const txt = `
To jest Ania
To jest Marcin
`;
//sprawdzam czy za Anią coś jeszcze jest w tej samej linii
console.log( /Ania./.test(txt) ) //false
console.log( /Ania./s.test(txt) ) //true
znak Flagi | znaczenie |
---|---|
u | Włącza możliwość używania kodów dla znaków unicode |
znak Flagi | znaczenie |
---|---|
y | Włącza tryb sticky . Kolejne wyszukiwania będą rozpoczynać się od pozycji ostatniego szukania, którą definiuje lastIndex |
Pozycja w tekście
Domyślnie szukany fragment może znajdować się w dowolnym miejscu.
console.log( /kot/.test("to jest fajny kot i pies") ) //true "to jest fajny *kot* i pies"
console.log( /kot/.test("kot jest spoko") ) //true "*kot* jest spoko"
console.log( /kot/.test("pies i kot") ) //true "pies i *kot*"
Jeżeli chcemy sprawdzić, czy dany fragment występuje na końcu linii wiele liniowego tekstu, powinniśmy dodać do wyrażenia flagę m
const txt = `
przykladowy-plik.jpg
inny-plik.jpg
jakis-plik-w-formacie-jpg-z-wakacji.jpg
inny-plik.png
`;
//chcę sprawdzić czy jakiś plik kończy się na .jpg
//ten test jest błędny, bo "jpg" może być wewnątrz nazwy
console.log( /jpg/.test(txt) ) //true
/*
przykladowy-plik.*jpg*
inny-plik.*jpg*
jakis-plik-w-formacie-*jpg*-z-wakacji.*jpg*
inny-plik.png
*/
//ten też jest błędny, bo sprawdzi czy cały tekst kończy się na jpg
console.log( /jpg$/.test(txt) ) //false
//ten jest ok, bo sprawdzi czy pojedyncza linia kończy się na jpg
console.log( /jpg$/m.test(txt) ) //true
/*
przykladowy-plik.*jpg*
inny-plik.*jpg*
jakis-plik-w-formacie-jpg-z-wakacji.*jpg*
inny-plik.png
*/
Ilości znaków
Zbiory znaków
Wybór lub
Grupy
Okrywając kawałek wzoru nawiasami robimy z niego grupę, do której możemy się później odwoływać.
W powyższych przykładach używaliśmy metody .test()
, która zwraca wartość boolean.
const reg = /([0-9]{4})-([0-9]{2})-([0-9]{2})/
console.log( reg.test("2020-10-05") ) //true
Przy pracy z grupami warto skorzystać z dodatkowych metod takich jak .match() lub .exec() czy replace(), które zwracają tablicę pasujących fragmentów:
const reg = /([0-9]{4})-([0-9]{2})-([0-9]{2})/
console.log( reg.exec("2020-10-05") ) //["2020-10-05", "2020", "10", "05", ...]
//tutaj uwaga - to metody dla stringów, nie regexp
console.log( "2020-10-05".match(reg) ) //["2020-10-05", "2020", "10", "05", ...]
console.log( "2020-10-05".matchAll(reg) ) //["2020-10-05", "2020", "10", "05", ...]
Do kolejnych znalezionych członów (grup) możemy potem odwoływać się poprzez zapis $1
, $2
itd.
Możemy to wykorzystać w metodzie replace(), ale też podczas pracy w edytorze tekstu.
const reg = /([0-9]{4})-([0-9]{2})-([0-9]{2})/
console.log( "1980-10-05".replace(reg, "Mamy rok $1 miesiąc $2 dzień $3") ); //"Mamy rok 1980 miesiąc 10 dzień 05"
Dopasowywane w zależności
Zapis ?=
pozwala nam dopasowywać dany fragment tekstu, gdy tuż za nim występuje inny fragment tekstu:
console.log( /Pies(?= Szamson)/.test("Pies Szamson jest super szybki")) //true
console.log( /Pies(?= Szamson)/.test("Pies Azor jest super szybki") ) //false
W ES2018 mamy też bardzo podobny zapis ?<=
, który pozwala nam sprawdzać czy dany ciąg poprzedza jakiś tekst:
console.log( /(?<=Pies) Szamson/.test("Pies Szamson jest super szybki")) //true
console.log( /(?<=Pies) Szamson/.test("Kot Szamson jest super szybki") ) //false
Unicode w wyrażeniach regularnych
Domyślnie wyrażenia regularne - tak samo jak reszta tekstów w javascript operuje na pierwszych 1600 znakach z tablicy Unicode. Jeżeli chcielibyśmy działać na niestandardowych znakach (np. Chińskich literach czy ikonach Emoji), powinniśmy do naszego wzoru dodać flagę u
:
console.log( /^.$/.test("a") ) //true
console.log( /^.$/.test("😎") ) //false
console.log( /^.$/u.test("😎") ) //true