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 podkatalogu regexp/vendor. Dodatkowo stworzy dwa pliki composer.json i composer.lock.

  • W katalogu regexp stwórz plik index.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:
      cd Desktop
      cd regexp
      php index.php   # uruchom program
      
      Powinieneś zobaczyć napis "Hello, world" w konsoli.
    • Uruchomienie servera php oraz otworzenie pliku index.php przez przeglądarkę:
      cd Desktop
      cd regexp
      php -S localhost:8080   # uruchom Server na adresie "localhost:8080"
      
      Powinieneś zobaczyć napis:
      [Sat Mar 14 23:38:14 2020] PHP 7.4.3 Development Server (http://localhost:8080) started
      
      Teraz możesz otworzyć dowolną przeglądarkę i otworzyć adres 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.

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łu 0 - 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) znaczy xx ALBO yy ALBO zz. W naszym przypadku znaczy to md ALBO mp[34] ALBO jpg. Co oznacza mp[34]? Oznacza litery mp oraz jedną z cyfr 3 lub 4. Czyli mp[34] może oznaczać mp3 lub mp4. 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.

Zaawansowane podmienianie - replace()->by()->map()

Czasem zdarza się, że podmiana ma polegać na istniejącej wartości, ale te wartości są stałe - przypisanie 1 do 1. Wtedy pomocna okaże się funkcja by()->map() z biblioteki T-Regx.

Przykładowo, chcemy podmienić wszystkie długie rozszerzenia (yaml, mpeg4) na krótkie (yml, mp4):

$words = 'Some methods receive a callback that accepts `Match` details object. These methods are:
match-first.md, match-find-first.yaml. They are spoken of in files: sound.mpeg3, video.mpeg4 and picture.jpeg.';

$result = pattern('\.(yaml|mpeg[34]|jpeg)')->replace($words)-->by()->group(1)
    ->map([
        'yaml'  => '.yml',
        'mpeg3' => '.mp3',
        'mpeg4' => '.mp4',
        'jpeg'  => '.jpg'
    ])
    ->orThrow();

A oto wynik, znajdujący się w $result:

Some methods receive a callback that accepts `Match` details object. These methods are:
match-first.md, match-find-first.yml. They are spoken of in files: sound.mp3, video.mp4 and picture.jpg.

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

PHP

0 komentarzy