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 kod pocztowy. Kod taki można opisać w następujący sposób: na początku są 2 dowolne cyfry, potem znak myślnika, po czym występują trzy dowolne cyfry (np. 02-380). Powyższy opis kodu pocztowego to właśnie wzorzec.
Powiedzmy, że w jakimś tekście chcielibyśmy znaleźć takie kody pocztowe. Problem w tym, że nie wiemy jakie one mają numery... I tutaj właśnie pomogą nam wzorce.

Wszelakie zabawy z wyrażeniami regularnymi warto przeprowadzać w jednym z narzędzi testujących jak to, czy to. Zresztą wystarczy w wyszukiwarkę wpisać "reg tester"...

Na temat wyrażeń regularnych jest masa super artykułów. Pod adresem https://flaviocopes.com/javascript-regular-expressions/#how-does-a-regular-expression-look-like znajduje się jeden z fajniejszych tekstów na ten temat.

Przykładowy wzorzec może mieć np. postać:


/^[a-zA-Z]{2,}\s[a-zA-Z]{2,}$/

Aby w Javascript korzystać z wyrażeń regularnych, musimy utworzyć obiekt RegExp(wyrażenie, flaga), który przyjmuje 2 argumenty: wyrażenie, którym będziemy testować, oraz dodatkowe flagi, które poznamy w tym rozdziale.


const reg = new RegExp("pani?" , "gi")

lub

const reg = /pani?/gi

Którą metodę wybrać?
Każda z nich ma swoje wady i zalety. Pierwsza wymaga poprzedzania specjalnych znaków (np znaku zapytania) (które zaraz poznamy) podwójnym ukośnikiem //. W drugiej metodzie specjalne znaki poprzedzamy jednym ukośnikiem, ale całe wyrażenie musimy objąć parą ukośników.

Metaznaki

Każdy wzorzec składa się z meta znaków, czyli specjalnych znaków, które opisują jak mają wyglądać wyszukiwane fragmenty tekstu. Przykładowo wzorzec składający się z meta znaków


p.p.

będzie pasował do słowa "popo", ale równie dobrze będzie pasował do słowa "papi", "pupa", "papa" itp.

Poniżej zamieściłem tabelę zawierającą opis meta znaków.

Metaznak Znaczenie Przykład wyrażenia Zgodne ciągi z wyrażeniem Niezgodne ciągi z wyrażeniem
^ początek wzorca ^za zapałka
zadra
zapłon
zarazek
kazanie
poza
bazar
$ koniec wzorca az$
uraz
pokaz
azymut
pokazy
^.arka$ barka
warka
parkan
. dowolny pojedynczy znak .an.a panda
Wanda
panna
kania
rana
konia
[...] dowolny z wymienionych znaków;
możemy podawać kolejne znaki lub wpisywać zakres - na przykład [a-z] oznacza wszystkie małe litery. Wymieniając escapowane znaki (następna table) nie musimy ich poprzedzać ukośnikiem
[a-z]an[nd]a pana
panda
wanna
Wanda
kania
[a-z][a-zA-Z0-9.-]
pas
mAs
p2p

Bas
bal
balu
mp3
[^...] dowolny z niewymienionych znaków kre[^st] krew
krem
kres
kret
| dowolny z rozdzielonych znakiem ciągów [nz]a|pod|przed
na
za
pod
przed
 
trzynasty|13-ty|13 trzynasty
13-ty
13
(...) zawężenie zasięgu g(ż|rz)eg(ż|rz)(u|ó)łka
gżegżółka
gżegrzółka
gżegrzułka
grzegrzułka
 
(ósmy|8-my|8)(maj|maja) ósmy maja
8-my maj
8 maja
? zero lub jeden poprzedzający znak lub element; elementem może być na przykład wyrażenie umieszczone wewnątrz nawiasów (...) ro?uter router
ruter
 
(ósmy|8(-my)?)maja? ósmy maja
ósmy maj
8-mymaja
8-my maj
8 maja
8 maj
+ jeden lub więcej poprzedzających znaków lub elementów; elementem może być na przykład wyrażenie umieszczone wewnątrz nawiasów (...) [0-9]+[abc]

10a
1b
003c
42334b

a
b
c
z
14
03
12d
1231z
pan+a panna
pannnna
(tam)+ tam
tamtam
tamtamtam
paa
panda
ta
tamta
mat
* zero lub więcej poprzedzających znaków lub elementów; elementem może być na przykład wyrażenie umieszczone wewnątrz nawiasów (...) [0-9]*[abc] 10a
1b
003c
42334b
a
b
c
k
2335
pora*n*a* por
pora
poranna
poraannnaa
pornnna
porada
panna
{4} dokładnie 4 poprzedzające znaki lub elementy [0-9]{4} 8765
8273
2635
12345
234
2123456
{4,} 4 lub więcej poprzedzających znaków lub elementów [ah]{4,} haha
haaaaahaha
ahaaa
haa
ha
hehe
aha
{2,4} od 2 do 4 poprzedzających znaków lub elementów p.{2,4}a piana
pola
polana
psa
poranna

Jeżeli chcemy w wyrażeniu zawszeć znaki, które normalnie mają specjalne znaczenie, musimy poprzedzić taki znak ukośnikiem.

\. znak kropki [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} 128.0.0.2 128-0-0-2
\* znak * \*.+ *nic nic*
nic
\/ znak / ^\/\/$ //  
\? znak ? ^.+\?$ Czy to jest kot? Czy to jest kot
\: znak : ^.+\:$ Oto one: :nic
\. znak . \.+ ......  
\^ znak ^ .*\^ To jest ^ To jest &
\+znak + [0-9]+\+[0-9]+ 928374+29832 23873-32787
238738278
\\ znak \ c\:\\ c:\  
\= znak = [0-9]+\+[0-9]+\=[0-9]+ 11+12=23 11-12=23
\| znak | x \|\| y x || y x -- y

Flagi

Poza wymienionymi meta znakami istnieją specjalne parametry (flagi), które oddziałują na wyszukiwanie wzorców.:


const reg = /[a-z]*/mg
const reg = new RegExp("[a-z]*","g")
znak Flagiznaczenie
ipowoduje niebranie pod uwagę wielkości liter
gpowoduje zwracanie wszystkich pasujących fragmentów, a nie tylko pierwszego
mpowoduje wyszukiwanie w tekście kilku liniowym. W trybie tym znak początku i końca wzorca (^$) jest wstawiany przed i po znaku nowej linii (\n).

Klasy znaków

Dodatkowo Javascript udostępnia specjalne klasy znaków. Zamiast wyszukiwać wszystkie litery za pomocą [a-zA-Z_] możemy skorzystać z klasy znaków \w.

Klasa znakówznaczenie
\sznak spacji, tabulacji lub nowego wiersza
\Sznak nie będący spacją, tabulacją lub znakiem nowego wiersza
\wkażdy znak będący literą, cyfrą i znakiem _
\Wkażdy znak nie będący literą, cyfrą i znakiem _
\dkażdy znak będący cyfrą
\Dkażdy znak nie będący cyfrą

Zastosowanie metody test()

Zacznijmy od najprostszych rzeczy czyli od sprawdzenia, czy w danym tekście występuje nasz wzorzec.

Metoda test() służy do sprawdzania, czy dane wyrażenie znajduje się w tekście:


const text = "cat dog";
const reg = /cat/;
reg.test(text) === true //bo cat znajduje się w tekście

const reg2 = /^cat$/;
alert(reg2.test(tekst)); //false - bo wzorzec zaczyna się z początkiem i kończy z końcem tekstu (znaki ^ i $) - jedyny pasujący tekst to "cat"

const text = "Turlal goryl po Urlach kolorowe korale...";
const reg = new RegExp("[A-Z]{1}[^\s]+");
console.log(reg.test(text)) //true - bo w tekście jest wyraz zaczynający się z dużej litery, po którym następują małe litery (np. Turlal)

[A-Z]{1} - jedna duża litera
[^\s]+ - plus oznacza jeden lub więcej znaków, a [^\s] oznacza znak nie będący spacją - czyli razem: jeden lub więcej znaków nie będących spacją


const text = "Turlal goryl po Urlach kolorowe korale...";
const reg = new RegExp("[0-9]+");
console.log(reg.test(text)) //false - bo w tekście nie ma żadnej cyfry

const reg = new RegExp("[0-9]*");
console.log(reg.test(text)) //true - w tekście nie ma żadnej cyfry, ale * oznacza 0 lub więcej

Zastosowanie metody exec()

Metoda excec() przeszukuje dany ciąg znaków, a następnie zwraca tablicę zawierającą składowe pierwszego wyszukanego fragmentu.

Jeżeli metoda nic nie znajdzie, zwróci null.
Jeżeli metoda coś znajdzie, wtedy element 0 zawiera ostatni pasujący ciąg, natomiast elementy o indeksie 1 - n zawierają poszczególne części wyszukiwania (patrz tutaj). Poniższy przykład pokazuje składowe takiego wyniku:


const re = /d(b+)(d)/ig;
const result = re.exec("cdbBdbsbz");
ObiektWłaściwość/IndexOpisPrzykład
result [0] Ostatni znaleziony pasujący ciąg dbBd
[1], ...[n] Kolejne Atomy pasującego ciągu [1] = bB
[2] = d
index początkowy indeks od kąd zaczyna się znaleziony kawałek tekstu 1
input Oryginalny tekst w którym szukamy cdbBdbsbz
re lastIndex Indeks odkąd rozpocznie się następne szukanie 5
ignoreCase Wskazuje flagę "i" - nie branie pod uwagę wielkości liter true
global Wskazuje flagę "g" - wielokrotne wyszukiwanie w tym samym ciągu true
multiline Wskazuje flagę "m" - wyszukiwanie w wielu liniach false
source Wzór wyrażenia regularnego d(b+)(d)

console.log(result[0]) //dbBd
console.log(result.index) //1
console.log(result.input) //cdbBdbsbz

console.log(re.lastIndex) //5
console.log(re.multiline) //false
console.log(re.ignoreCase) //true
console.log(re.source) //d(b+)(d)

Jeżeli użyjesz flagi "g", wtedy będziesz mógł użyć metody exec() do wielokrotnego wyszukiwania w tym samym tekście:


const myRe = /ab*/g;
const str = "abbcdefabh";
const myArray;

while ((myArray = myRe.exec(str)) !== null) {
    const msg = "Znaleziono: " + myArray[0] + "    ";
    msg += "Następne wyszukiwanie rozpocznie się od: " + myRe.lastIndex;

    console.log(msg);

    //Znaleziono: abb - następne wyszukiwanie rozpocznie się od: 3
    //Znaleziono: ab - następne wyszukiwanie rozpocznie się od: 9
}

Match()

Obiekt String posiada metodę match(), która spełnia tę samą funkcję co metoda exec() obiektu RexExp, jednak zwraca od razu wszystkie pasujące fragmenty.


const text = "Numer1, Numer2, Numer3, NumerB, Numer5, NumerD";
const reg = /Numer[1-4A-C]/g;
console.log(text.match(reg)); //Numer1, Numer2, Numer3, NumerB

const reg = /d(b+)(d)/ig;
const result = "cdbBdbsbz".match(reg);

if (result.length) {
    console.log(result.join('-')); //dbBd
}

Metoda search() obiektu RexExp działa tak samo jak metoda indexOf() obiektu string, czyli zwraca indeks pierwszego wystąpienia podciągu w ciągu:


const text = "Fantomas robi masę - marchewkowo-marcepanowa";
const reg = /at/gi";

console.log("Search: " + text.search(reg));
console.log("Index of: " + text.indexOf("at"));

Zastosowanie metody replace()

Obiekt string posiada metodę replace(), która służy do zamiany jednego ciągu na drugi. Przy jej stosowaniu możemy używać wyrażeń regularnych:


const text = "Kolorowy kolor nie jest kolorowy?...";
console.log(text);

const reg = /lor/g //nasze wyrażenie
console.log(text.replace(reg,"ral")); //Wyszukujemy w tekście wszystkie wystąpienia "lor" i zamieniamy je na pogrubione "ral"

Jako drugi argument tej metody możemy podać funkcję. Pobiera ona jeden argument - znaleziony fragment tekstu, oraz zwraca tekst, który zastąpi znaleziony fragment:


const text = "Super Samson jest fajny.";
const reg = /fajny/;
const textEnhanced = text.replace(reg, function(match) {
    return "super" + match;
});

console.log(textEnhanced); //Super Samson jest super fajny

Czy liczba

Chcemy sprawdzić, czy użytkownik wpisał numer. Jak widać w tabeli zamieszczonej powyżej, Javascript udostępnia nam klasę \d, która oznacza dowolną cyfrę:


const text = "909384758699";
const reg = /^\d+$/
if (reg.test(text)) {
    console.log("To jest liczba")
} else {
    console.loge("To nie jest liczba...")
}

//w sumie jest lepszy sposób
if (!isNan(Number(text))) ...

Sprawdzanie Kodu Pocztowego

Aby wyszukać w tekście kod pocztowy użyjemy wyrażenia:


const codeReg = /[0-9]{2}-[0-9]{3}/g;

//lub

const codeReg = /[\d]{2}-[\d]{3}/g;

[0-9]{2} - powinny znaleźć się 2 cyfry
- - po 2 cyfrach powinien znaleźć się znak -
[0-9]{3} - po którym powinny znaleźć się 3 cyfry
g - mają być zwrócone wszystkie wyszukane pasujące ciągi

Nasz wzór możemy wykorzystać w skrypcie za pomocą metody match (zwracającej pasujące ciągi):


const text = "Moj kod pocztowy to nie 12-323 ani tez 03-400 ... jest po prostu inny.";
const codeReg = /[0-9]{2}-[0-9]{3}/g; //parametr g nakazuje zwrócenie wszystkich znalezionych ciągów (normalnie zwracany jest
tylko pierwszy)
const matchReg = text.match(codeReg);
if (matchReg) {//jeżeli w a znajdują się pasujące ciągi
    for (let x=0; x<matchReg.length; x++) {
        console.log(matchReg[x]);
    }
}

Weryfikacja Imienia i Nazwiska

Wzorzec opisujący poprawność tych danych ma postać:


const nameReg = /^[a-zA-Z]{3,}\s+[a-zA-Z]{3,}$/;

//lub

const nameReg = /^[\D]{3,}\s+[\D]{3,}$/;

/ - od tego znaku muszą się zaczynać i kończyć wszystkie wzorce w JavaScripcie
^ - Wzorzec ma się zaczynać z początkiem tekstu
[a-zA-Z]{3,} - Ciąg musi zawierać przynajmniej 4 litery (imię)
\s+ - Po których znajdą się spacje lub tabulatory (min jeden)
[a-zA-Z]{3,} - Po których znajdą się znowu przynajmniej 3 litery (nazwisko)
$ - Wzorzec ma się kończyć z końcem tekstu

Teraz możemy nasz wzór wykorzystać w skrypcie za pomocą metody test:


const nameSurname = "Marcin Domański";
const nameReg = /^[a-zA-Z]{2,}\s[a-zA-Z]{2,}$/;

if (nameReg.test(nameSurname)) {
    console.log('Imię i Nazwisko jest OK!');
}

Weryfikacja emaila

Jak wiemy, aby adres email był prawidłowy musi spełniać kilka zasad:

  • musi posiadać nazwę konta (składającą się z kiku znaków - poza znakami ?,:*/ i spacji)
  • po dokumencie musi znaleźć się znak @
  • po małpie powinna znaleźć się przynajmniej jedna . i to pomiędzy domeną a końcówką adresu
  • końcówka adresu powinna składać się z przynajmniej 2 liter (pl, com itp.)

Wzór który by opisywał adres mail będzie miał postać:


const mailReg = /^[0-9a-zA-Z_.-]+@[0-9a-zA-Z.-]+\.[a-zA-Z]{2,3}$/

/ - od tego znaku muszą się zaczynać i kończyć wszystkie wzorce w JavaScripcie
^ - Wzorzec ma się zaczynać z początkiem tekstu
[0-9a-zA-Z_.-]+ - Następnie badamy nazwę konta, która może składać się z dowolnych znaków (cyfry, litery, .-_ )
@ - Potem sprawdzamy wystąpienia znaku @
[0-9a-zA-Z_.-]+ - Po znaku @ sprawdzamy domenę, która może składać się z takich samych znaków co nazwa konta oprócz znaku _
\. - Po dokumencie musi wystąpić kropka
[a-zA-Z]{2,3} - Po kropce musi wystąpić końcówka domeny, która może się składać wyłącznie z liter i jej długość musi być od 2 do 3 znaków
$ - Wzorzec ma się kończyć z końcem tekstu

Zamiast za każdym razem wpisywać a-zA-Z, możemy uprościć formularz. Na końcu za znakiem / należy wpisać i, co sprawi, że wielkość liter nie będzie brana pod uwagę. Dzięki temu nasze wyrażenie nieco się uprości:


const mailReg = /^[0-9a-z_.-]+@[0-9a-z.-]+\.[a-z]{2,3}$/i

Sprawdźmy nasz wzór w przykładowym polu tekstowym (za pomocą metody test):


const mail = "mar-dom@wp.pl";
const mailReg = /^[0-9a-z_.-]+@[0-9a-z.-]+\.[a-z]{2,3}$/i

if (mailReg.test(mail)) {
    console.log('Mail OK');
}

Powyższe wyrażenie regularne to tak naprawdę jeden z przykładów (ale działający). Takich wzorów jest masa i na ich temat toczy się nie jedna dyskusja.

Inny wzór możesz pobrać np. ze strony http://emailregex.com/

Atomy i referencja powrotna:

Gdy dokonujemy sprawdzenia według wzorca, możemy nasze części wyrażenia objąć w nawiasy tworząc tak zwane Atomy. Wówczas kolejne Atomy będą zawierać kolejne części znalezionego tekstu, do których będziemy mogli się indywidualnie odwołać:


const str = "http://www.webreference.com/js/index.php#piosenka?l=2";
const reg = str.match(/(\w+:\/\/)([^/]+)([^#?]*)([^?]*)\?*(.+)*/);

Wzorzec wyszuka:
(\w+:\/\/) - dowolne litery po których znajduje się ciąg ://
([^/]+) - po których znajduje się ciąg do znaku /
([^#?]*) - po którym znajduje się kolejny ciąg do ? lub #
([^?]*) - po których znajduje się ciąg do ? (czyli jeżeli adres wywołał kotwicę)
\?* - po którym może znajdować się znak ?
(.+)* - po którym może wystąpić ciąg znaków (czyli query string

Po zastosowaniu atomów, możemy się odwoływać do ich treści. Jest on podzielona według naszych atomów, i nazywa się wsteczną referencją. Pierwszym ze sposobów odwoływania do tych treści jest wykorzystanie obiektu RegExp:


const text = "http://www.webreference.com/js/index.php#piosenka?l=2";
const reg = text.match(/(\w+:\/\/)([^/]+)([^#?]*)([^?]*)\?*(.+)*/);

console.log('Badany ciąg: ' + text);
console.log("Wyrażenie z 1 nawiasu: "+ RegExp.$1);
console.log("Wyrażenie z 2 nawiasu: "+ RegExp.$2);
console.log("Wyrażenie z 3 nawiasu: "+ RegExp.$3);
console.log("Wyrażenie z 4 nawiasu: "+ RegExp.$4);
console.log("Wyrażenie z 5 nawiasu: "+ RegExp.$5);

Drugi sposób odwoływania się do poszczególnych Atomów polega na podaniu numeru atomu poprzedzonym ukośnikiem:


const tekst = "FantomasFantomas"
const reg = /(Fantomas)\1/;
text.test(reg);

Powyższy wzór korzysta z tej metody.
Pierwszy Atom pasuje do słowa Fantomas, więc wyrażenie (Fantomas)\1 jest równe "FantomasFantomas".

Ostatnią metodą wykorzystania wstecznej referencji jest skorzystanie z metody replace(), która pozwala na korzystanie z numerów poprzedzonych znakiem dolara, oznaczających numer Atomu:


const text = "2010-10-20";
const reg = /(\d{4})-(\d{2})-(\d{2})/;
const textReplaced = text.replace(reg, "$3 $2 $1"); //20-10-2010

Jeżeli chcemy by nasz Atom nie tworzył wstecznej referencji, wtedy musimy rozpocząć jego treść od "?:"


const reg = /(\d{4})-(?:\d{2})-(\d{2})/;
const zamieniony = tekst.replace(reg, "$2 $1"); //20-10-2010

Podział tekstu na słowa przy pomocy metody split()

To podziału tekstu wykorzystamy metodę split().
Pierwszym parametrem jest tekst wedle którego ma być podzielony tekst i zamieniony na tablicę:


const tekst = "Ala ma kota";
console.log(tekst.split(" ")); //["Ala", "ma", "kota"]

Co ciekawe, parametr ten może być podany w formie wyrażenia regularnego:


const text = "To jest zdanie do podzielenia na pojedyncze słowa"
const tab = s.split(/\W+/); //dzielimy zdanie na słowa (wedle sekwencji nie literowych)
for (let i=0; i<tab.length; i++) {
    console.log(tab[i]);
}

Usuwanie tagów HTML z tekstu

Aby usunąć tagi z jakiegoś tekstu, wystarczy skorzystać z poniższej funkcji, która używa do tego celu wyrażeń regularnych:


function stripTags(str) {
    const reg = /<([^>]+>)/ig;
    return str.replace(reg, '');
}

const body = document.querySelector('body');
console.log( striTags(body.innerHTML) );

Funkcja ta zadziała tylko z poprawną strukturą html. Jeżeli wstawiasz do jakiegoś atrybutu znak < lub > powinieneś takie znaki zakodować za pomocą &lt; i &gt;

Inną możliwością jest użycie textContent, która to właściwość zwraca tekst bez znaczników html.