Wyrażenia regularne
Riddle
Oczywiście, z wyrażeń regularnych można korzystać w PHP bez żadnej biblioteki. Istnieją do tego funkcje preg_match()
, preg_match_all()
, preg_replace()
, preg_grep()
, preg_split()
, niemniej są one nieco mętnie i chaotycznie zaprojektowane, przez co praca z nimi (również obsługa błędów) może być trudna. Funkcje te nie są do siebie podobne (np wyszukiwanie i podmienianie znaków wykonuje się zupełnie inaczej).
Dlatego też w tym artykule skorzystamy z biblioteki T-Regx – przeznaczonej specjalnie do wyrażeń regularnych.
Przygotowanie środowiska
Żeby skorzystać z biblioteki, musimy zaopatrzyć się w popularny program "Composer" (https://getcomposer.org/download). Jest dostępny na platformy: Windows, Linux/Unix oraz Mac. Composer to program do zarządzania zależnościami. W uproszczeniu, podajemy listę zależności (bibliotek, modułów - i ich wersje), a Composer dba o to by pobrać odpowiednie pliki.
Zanim zaczniemy korzystanie z biblioteki T-Regx, przygotujmy środowisko.
Instalacja Composera
- Instrukcja, jak pobrać Composera, znajduje się na stronie: https://getcomposer.org/download
- Po zainstalowaniu, Composer powinien być dodany do zmiennej środowiskowej
PATH
na Twoim komputerze, po to żebyś mógł po prostu wpisaćcomposer
w konsolę, bez pamiętania o ścieżkach. - Zweryfikuj, czy Composer jest zainstalowany poprawnie:
- Otwórz konsolę (
cmd
na Windowsie,bash
na Linuxach/Macach) - Wpisz komendę
composer -v
- Jeśli zobaczysz napis
Composer version 1.9.3 2020-02-04 12:58:49
, to wszystko okej :)
Przygotowanie projektu
-
Stwórz w dowolnym miejscu (pulpit, dokumenty, Twój folder z projektami itp.) folder o dowolnej nazwie, np.
regexp
. -
Otwórz konsolę oraz przejdź do tego katalogu. Jeśli stworzyłeś folder
regexp
na pulpicie, to:cd Desktop/ # Przejdź na pulpit cd regexp/ # Przejdź do folderu z projektem
-
Skorzystaj z Composera, żeby pobrać bibliotekę:
composer require rawr/t-regx
Composer powinien pobrać najnowszą wersję biblioteki (w trakcie pisania tego artykułu była to wersja
0.9.8
) i umieścić ją w Twoim folderze w podkataloguregexp/vendor
. Dodatkowo stworzy dwa plikicomposer.json
icomposer.lock
. -
W katalogu
regexp
stwórz plikindex.php
i wpisz w nim taką testową treść:<?php require_once __DIR__ . '/vendor/autoload.php'; # To wczyta nam wszystkie biblioteki ściągnięte przez Composera echo "Hello, world\n"; # Wyświetli hello worlda
-
Uruchom plik
index.php
. Możemy to zrobić na dwa sposoby: uruchomić go bezpośrednio w konsoli, lub uruchomić server php i uruchomić go przez przeglądarkę:- Uruchomienie programu z konsoli:
Powinieneś zobaczyć napis "Hello, world" w konsoli.cd Desktop cd regexp php index.php # uruchom program
- Uruchomienie servera php oraz otworzenie pliku
index.php
przez przeglądarkę:
Powinieneś zobaczyć napis:cd Desktop cd regexp php -S localhost:8080 # uruchom Server na adresie "localhost:8080"
Teraz możesz otworzyć dowolną przeglądarkę i otworzyć adres[Sat Mar 14 23:38:14 2020] PHP 7.4.3 Development Server (http://localhost:8080) started
http://localhost:8080/index.php
. Powinieneś zobaczyć napis "Hello, world".
Oba sposoby są równoważne. Możesz korzystać z tego, który Ci bardziej pasuje.
- Uruchomienie programu z konsoli:
Wyrażenia regularne z T-Regx
Jeśli udało Ci się uruchomić plik index.php
, to jesteśmy gotowi!
Edytuj plik index.php
i uzupełnij go o taką treść:
<?php
require_once __DIR__ . '/vendor/autoload.php';
if (pattern("I like (apples|oranges)")->test("I like apples")) {
echo "Correct!";
} else {
echo "Wrong!";
}
echo PHP_EOL;
Już spieszę z wyjaśnieniem. W pierwszej linijce użyliśmy metody test()
do sprawdzenia, czy łańcuch "I like apples"
pasuje do wzorca "I like (apples|oranges)"
, i jeśli tak, to pokażemy tekst "Correct!"
. Gdyby łańcuch nie pasował do wzorca, zobaczylibyśmy "Wrong!"
. Oczywiście łańcuch "I like oranges"
również do niego pasuje.
Znalezienie pierwszego/wielu wystąpień - match()
Łatwo możemy skorzystać z wyrażeń regularnych, żeby wyciągnąć wszystkie linki z jakiegoś tekstu, np:
<?php
require_once __DIR__ . '/vendor/autoload.php';
$bigBang = '<p>
<b>Wielki Wybuch</b> (<a href="/wiki/J%C4%99zyk_angielski" title="Język angielski">ang.</a> <i>Big Bang</i>) –
najwcześniejsze znane wydarzenie w obserwowalnym Wszechświecie, jego najwcześniejsza znana faza (etap) ewolucji, a
jednocześnie nazwa modelu tego procesu. Według tego scenariusza ok.<a href="/wiki/Wiek_Wszech%C5%9Bwiata"
title="Wiek Wszechświata">13,799 ± 0,021</a>mld lat temu<sup id="cite_ref-1" class="reference">
<a href="#cite_note-1">[1]</a></sup> miał miejsce Wielki Wybuch – z bardzo <a href="/wiki/G%C4%99sto%C5%9B%C4%87"
title="Gęstość">gęstej</a> i <a href="/wiki/Temperatura" title="Temperatura">gorącej</a>
materii wyłonił się znany <a href="/wiki/Wszech%C5%9Bwiat" title="Wszechświat">Wszechświat</a>
</p>';
$links = pattern('/\w+/([\w%]+)')->match($bigBang)->all();
var_dump($links); // wyświetl linki
Zobaczymy, że w $links
mamy tylko same linki:
array(5) {
[0] =>
string(26) "/wiki/J%C4%99zyk_angielski"
[1] =>
string(28) "/wiki/Wiek_Wszech%C5%9Bwiata"
[2] =>
string(28) "/wiki/G%C4%99sto%C5%9B%C4%87"
[3] =>
string(17) "/wiki/Temperatura"
[4] =>
string(22) "/wiki/Wszech%C5%9Bwiat"
}
Podobnie możemy napisać również wyrażenie do wyciągnięcia np. samych numerów telefonów:
pattern('\d{3}-\d{3}-\d{3})->match($text)->all();
Oto co oznacza wyrażenie:
-
\d
- znak z przedziału0
-9
(cyfra) -
{3}
- poprzedni znak ma wystąpić dokładnie 3 razy -
-
- pauza
Szczegóły o znalezionych danych
T-Regx udostępnia mnogość szczegółów, jeśli chodzi o znalezione wystąpienia. Możemy:
pattern('/\w+/([\w%]+)')->match($bigBang)->forEach(function ($match) {
$match->offset(); // sprawdzić na jakiej pozycji znajduje się link
$match->index(); // który to link z kolei
$match->group(1)->text(); // odczytać grupy #1 - części linku (w tym wypadku nazwa artykułu)
$match->isInt(); // sprawdzić czy link (lub tylko jego grupa) jest integerem
$match->toInt(); // rzutować na integer
$match->matched(1); // sprawdzić czy część wzoru (tzw. "capturing group") została odnaleziona w tekście
$match->subject(); // na jakim teście został dopasowany wzór
});
Wymieniamy tu tylko niektóre funkcje T-Regx'a; wszystkie są opisane na stronie z pełną dokumentacją: https://t-regx.com/docs/match-details
Podmiana znalezionych wystąpień - replace()
Oczywiście, wyrażenia regularne nie służą tylko do wyszukiwania tekstu, ale też do jego podmieniania. Może się zdarzyć, że chcemy podmienić każdą nazwę pliku w tekście na link do jego pobrania. Spróbujmy najpierw podmienić wszystkie nazwy plików na tekst XXX
- ocenzurujemy go:
$words = 'Some methods receive a callback that accepts `Match` details object. These methods are:
match-first.md, match-find-first.md. They are spoken of in files: sound.mp3, video.mp4 and picture.jpg.';
$censored = pattern('[\w-]+\.(md|mp[34]|jpg)')->replace($words)->with('XXX');
Jeśli zrobimy echo $censored
, powinniśmy zobaczyć tekst $words
, tylko z nazwami plików pozamienianymi na XXX
. A co oznacza samo wyrażenie regularne?
-
[\w-]
- zbiór znaków składający się z\w
oraz-
.\w
to "słowo" (czyli litery, cyfry oraz podkreślnik).-
to pauza. Cały zbiór[\w-]
oznacza: litery, cyfry,_
oraz-
. -
\.
- kropka to znak specjalny w wyrażeniach regularnych. Żeby znaleźć ją dosłownie, należy poprzedzić ją back-slashem. -
(md|mp[34]|jpg)
- konstrukcja(xx|yy|zz)
znaczyxx
ALBOyy
ALBOzz
. W naszym przypadku znaczy tomd
ALBOmp[34]
ALBOjpg
. Co oznaczamp[34]
? Oznacza literymp
oraz jedną z cyfr3
lub4
. Czylimp[34]
może oznaczaćmp3
lubmp4
. Zapis(md|mp3|mp4|jpg)
byłby taki sam, tylko troszkę dłuższy.
Czasem podmiana na stałą daną (np. XXX
) jest przydatna. My jednak chcemy podmienić nazwy na linki, zależnie od tego co zostało znalezione. Nie podmienimy więc linków na stałą wartość (with('XXX')
), tylko wykonamy osobną funkcję podmieniającą, zależnie od tego co zostało znalezione - tzw. "callback".
Posłużymy się do tego pewną sztuczką. Zauważ, że wyrażenie pasujące do rozszerzenia pliku (md
, mp3
, mp4
i jpg
) jest otoczone nawiasami w wyrażeniu regularnym: '[\w-]+\.(md|mp[34]|jpg)'
- tworzą one grupę. Do grupy możemy się odnieść funkcją group()
z biblioteki T-Regx. Na podstawie tej grupy możemy ustalić odpowiedni link do pliku, i tym linkiem właśnie go podmienić.
$result = pattern('[\w-]+\.(md|mp[34]|jpg)')
->replace($words)
->callback(function ($match) { // funkcja "callback()" do dynamicznego ustalania zastąpień
$paths = [ // w "$paths" trzymamy odpowiednie ścieżki plików, na podstawie rozszerzeń
'md' => 'markdown',
'mp3' => 'snd',
'mp4' => 'vid',
'jpg' => 'img'
];
$extension = $match->group(1)->text(); // dzięki funkcji "group()", wyciągamy samo tylko rozszerzenie pliku
$path = $paths[$extension]; // dzięki rozszerzeniu, ustalamy ścieżkę ze zmiennej "$paths"
return "[$match](/$path/$match)"; // zwracamy podmiankę: link ma być zastąpiony ciągiem `[link](/ścieżka/link)`
});
Podmienione nazwy plików na linki w $result
wyglądają tak:
Some methods receive a callback that accepts `Match` details object. These methods are:
[match-first.md](/markdown/match-first.md), [match-find-first.md](/markdown/match-find-first.md). They are spoken of in files: [sound.mp3](/snd/sound.mp3), [video.mp4](/vid/video.mp4) and [picture.jpg](/img/picture.jpg).
Zauważ, że w funkcji callback()
również mamy do dyspozycji $match
, a więc mamy dostęp do wszystkich szczegółów o wystąpieniu, tak samo jak przy funkcji match()
wyżej.
Dane od użytkownika we wzorcach - Prepared patterns
Czasem zachodzi sytuacja, by nie tylko znalezienia/podmienienia wystąpień były dynamiczne, ale żeby samo wyrażenie regularne było dynamiczne (i polegało np. na danych od użytkownika).
Uwaga! Łatwo jednak jest to zrobić w sposób nieprawidłowy, i otworzyć potencjalnym użyszkodnikom tworzenie błędów w naszej aplikacji. Taki kod poniżej jest bardzo niebezpieczny:
pattern('Password: ' . $_GET['pass'] . '$')->replace($input);
albowiem w $_GET
może być cokolwiek, co tylko klient takiej aplikacji nam wysłał (np. ?
lub .*
). Do budowania takich wyrażeń należy skorzystać z Prepared patterns (szerzej opisanych w dokumentacji).
use TRegx\CleanRegex\Pattern;
Pattern:inject('Password: @', [$_GET['pass']])->replace($input);
W tak sporządzone wyrażenie regularne nie ma prawa się nic wkraść. Prepared patterns (np. Pattern::inject()
) zostały stworzone z myślą właśnie o tym by chronić przed takimi potencjalnie szkodliwymi danymi.
Pozostałe funkcje biblioteki T-Regx
Oczywiście biblioteka udostępnia też wiele innych funkcji, jak np.:
- podzielenie tekstu przez wzorzec:
pattern()->split()
- filtrowanie wystąpień:
pattern()->match()->filter()
- walidacja wzorca:
pattern()->valid()
Ale o nich jest mowa dokładniej na stronie z pełną dokumentacją: https://t-regx.com/docs/match-find-first