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 Googla szukając frazy "coś test regexp".

Bardzo też przydają się tutaj narzędzia takie jak: https://regex101.com/, czy http://regexr.com/. Zresztą wystarczy w wyszukiwarkę wpisać "reg tester"...

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
`;

//chce sprawdzić 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
`;

//chce sprawdzić czy za Anią coś jeszcze jest
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*"

^

Znak ^ oznacza, że szukany fragment musi znajdować się na początku badanego tekstu


        console.log( /^kot/.test("kot lubi ryby") ) //true "*kot* lubi ryby"
        console.log( /^kot/.test("ten kot jest gruby") ) //false
        console.log( /^kot/.test("kot i pies") ) //true "*kot* i pies"
        console.log( /^kot/.test("pies i kot") ) //false
    

$

Znak $ oznacza, że szukany fragment musi znajdować się na końcu badanego tekstu


        console.log( /kot$/.test("pies i kot") ) //true "pies i *kot*"
        console.log( /kot$/.test("kot i pies") ) //false
        console.log( /^kot$/.test("kot") ) //true "*kot*"
        console.log( /^kot$/.test("kot i pies") ) //false
    

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

*

Znak * oznacza 0 lub więcej wystąpień poprzedzającej grupy lub znaku.


    console.log( /pan.*/.test("pan") ) //true
    console.log( /pan.*/.test("pani") ) //true

    console.log( /.*/.test("dowolny-tekst") ) //true
    console.log( /ab*cd/.test("abcd") ) //true
    console.log( /ab*cd/.test("acd") ) //true
    console.log( /ab*cd/.test("acc") ) //false
    console.log( /[a-z]*007/.test("abc007") ) //true
    console.log( /[a-z]*007/.test("007") ) //true
    

+

Znak + oznacza 1 lub więcej wystąpień poprzedzającego znaku lub grupy.


        const req = /pani+/
        console.log( req.test("pani") ) //true
        console.log( req.test("paniiiiiii") ) //true
        console.log( req.test("pan") ) //false
        console.log( /[a-z]+007/.test("abc007") ) //true
        console.log( /[a-z]+007/.test("007") ) //false
    

{}

Wewnątrz klamer podajemy liczbę poprzedzających znaków. Możemy tutaj podać konkretną wartość {4} lub minimalną liczbę znaków {4,}:


        const req = /^pani{1}/
        console.log( req.test("pan") ) //false
        console.log( req.test("pani") ) //true
        console.log( req.test("paniii") ) //false

        console.log( /^a{1,5}/.test("aaa") ) //true
        console.log( /^a{1,5}/.test("aaaaaa") ) //false
        console.log( /[0-9]{4}/.test("007") ) //false
        console.log( /[0-9]{1,}/.test("007") ) //true
    

?

Znak ? oznacza 0 lub 1 wystąpienie poprzedzającego znaku lub grupy.


        const req = /pani?/
        console.log( req.test("pan") ) //true
        console.log( req.test("pani") ) //true
        console.log( req.test("pakuś") ) //false

        const reg3 = /<div( class=".*")?><\/div>/
        console.log( reg3.test(`<div class="element"></div>`) ) //true
        console.log( reg3.test(`<div></div>`) ) //true
        console.log( reg3.test(`<h2></h2>`) ) //false
    

W przypadku użycia tuż za znakami ., + lub {} sprawia, że wyszukiwanie przechodzi w tryb wyszukiwania niezachłannego. Oznacza to, że pasujący ciąg skończy się na najmniejszym pasującym fragmencie, a nie największym


        const txt = `<div class="example">Tytuł książki to "Harry Potter"</div>`;

        /div class=".*"/.test(txt) //true <*div class="example">Tytuł książki to "Harry Potter"*</div>
        /div class=".*?"/.test(txt) //true <*div class="example"*>Tytuł książki to "Harry Potter"</div>
    

Zbiory znaków

[...]

Między nawiasy kwadratowe możemy wstawić znaki, które będą stanowić zbiór, który weźmie udział w teście


        console.log( /[abc]+/.test("ala") ) //true
        console.log( /[abc]+/.test("zuzy") ) //false
        console.log( /[abc]+/.test("aaaaabbbbbbccccc") ) //true
        console.log( /[abc]+/.test("baca") ) //true
        console.log( /[abc]+/.test("kury") ) //false
        console.log( /[abc]+/.test("kura") ) //true
        console.log( /[abc]+/.test("kac") ) //true
        console.log( /[123]{3}/.test("111") ) //true
        console.log( /[abc]+-[abc]+/.test("aaa-bbb") ) //true
        console.log( /[abc]+-[abc]+/.test("25-aaa") ) //false
    

Podając zbiory znaków możemy określać ich zakresy:


    /[a-z]/ //a, b, c, ... , x, y, z
    /[A-Z]/ //A, B, C, ... , X, Y, Z
    /[a-c]/ //a, b, c
    /[0-9]/ //0, 1, 2, 3, ... , 8, 9
    /[a-zA-Z]/ //a, b, c, A, B, C, ..., z, Z
    /[a-zA-Z0-9]/ //a, b, c, A, B, C, ..., z, Z, 0, ... 9
    

    console.log( /kot[0-9]{3}/.test("kot007") ) //true
    console.log( /[a-zA-Z]+[0-9]{3}/.test("ania007") ) //true
    console.log( /[0-9]{2}-[0-9]{3}/.test("kot02-370kot") ) //true
    console.log( /^[0-9]{2}-[0-9]{3}$/.test("02-370") ) //true
    console.log( /^[0-9]{2}-[0-9]{3}$/.test("02-3700") ) //false
    

Zbiory znaków możemy też negować za pomocą znaku ^:


        console.log( /[a-zA-Z]/.test("Ala ma kota") ) //true
        console.log( /^[^0-9]+$/.test("Ala ma kota") ) //true
        console.log( /^[^0-9]+[0-9]+$/.test("Ala ma kota 007") ) //true
    

Możemy też skorzystać z przygotowanych dla nas grup znaków:

Klasa znaków znaczenie
\d każdy znak będący cyfrą. Równoznaczne z [0-9]
\D każdy znak nie będący cyfrą. Równoznaczne z [^0-9]
\w każdy znak będący literą, cyfrą i znakiem _. Równoznaczne z [a-zA-Z0-9_]
\W każdy znak nie będący literą, cyfrą i znakiem _. Równoznaczne z [^a-zA-Z0-9_]
\s znak spacji, tabulacji lub nowego wiersza
\S każdy znak nie będący spacją, tabulacją lub znakiem nowego wiersza
\n znak nowego wiersza
\t znak tabulacji
\uXXXX oznacza znak o danym kodzie Unicode. Do użycia wymaga odpowiedniej flagi
. oznacza dowolny znak nie będący znakiem nowej linii. Żeby oznaczał też nową linię musimy użyć odpowiedniej flagi.

        console.log( /\w/.test("ania") ) //true
        console.log( /^\w$/.test("Jan Nowak") ) //false
        console.log( /\w \w/.test("Ania Nowak") ) //true
        console.log( /[\w]{3}.test("kot") ) //true
        console.log( /^[\w]{3}/.test("kot007") ) //true
        console.log( /\w{3}$/.test("kot") ) //true
        console.log( /\w{3}$/.test("---") ) //false
        console.log( /^[\w]$/.test("Ania.007") ) //false
        console.log( /^[\w]+.[\d]+/.test("Ania.007") ) //true

        console.log( /^.arka$/.test("ta arka") ) //true
        console.log( /^.arka/.test("barka pływa") ) //true
        console.log( /^.arka$/.test("barka pływa") ) //false
    

Dodatkowo mamy tutaj specjalne oznaczenie \b, które oznacza granicę słowa.

Możemy go użyć w trzech kluczowych miejscach:

  • na początku słowa, od którego zaczynają się znaki \w
  • na końcu słowa przed którym są znaki \w
  • w środku słowa gdzie po obu stronach są znaki \w

    console.log( "Hello, Java!".match(/\bJava\b/) ); //Hello, *Java*!
    console.log( "Hello, JavaScript!".match(/\bJava\b/) ); //false
    

Wybór lub

|

Znak | oznacza lub


    console.log( /hey|ho/.test("hey") ) //true
    console.log( /hey|ho/.test("ho") ) //true

    console.log( /p|kara/.test("para") ) //true
    console.log( /p|kara/.test("kara") ) //true
    console.log( /p|kara/.test("sara") ) //false

    console.log( /trzynasty|13-ty|13/.test("trzynasty") ) //true
    console.log( /trzynasty|13-ty|13/.test("13-ty") ) //true
    console.log( /trzynasty|13-ty|13/.test("13") ) //true
    console.log( /trzynasty|13-ty|13/.test("czternasty") ) //false
    

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

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.