Regulární výrazy

Regulární výrazy v PHP umožňují jednoduše hledat, validovat, porovnávat, rozdělovat, skládat a nahrazovat řetězce dle vzoru. Jedná se o velice silný a návykový nástroj pro téměř jakoukoli pokročilejší práci s textem.

Maska

Na začátku je nejprve nutné vymyslet samotný regulární výraz, který budeme spouštět. Zadává se formou textového řetězce, který má hromadu pravidel a možností konfigurace (jde o hodně komplexní techniku).

Pro začátek je důležité si uvědomit, že se regulární výraz vyhodnocuje postupně z levé části do pravé a pokud existuje více způsobů, jak řetězec interpretovat, tak se vždy použije největší možná shoda (chová se tzv. hladově a snaží se zpracovat co nejvíce znaků).

Jednoduché ověření, že je řetězec platný e-mail

Jak jednoduše zjistit, že je řetězec jan@barasek.com platná e-mailová adresa, aniž bychom jej museli složitě rozdělovat na části, nebo procházet znak po znaku?

Regulurní výrazy na to dávají odpověď (uvedený výraz je hodně zjednodušený pro potřeby příkladu a reálná implementace validace e-mailové adresy by měla být o něco složitější):

1 $mail = 'jan@barasek.com';
2 $regex = '/^.+@.+\.(cz|sk|com)$/';
3 
4 if (preg_match($regex, $mail)) {
5     echo 'E-mail je validní';
6 } else {
7     echo 'E-mail je nevalidní';
8 }

Pojďme si výraz /^.+@.+\.(cz|sk|com)$/ rozebrat o něco podrobněji:

Nejprve je nutné celý výraz obalit do dvojice znaků / (na začátku a na konci), které říkají, kde výraz začíná a kde končí. Za / na konci výrazu se dále zapisují případné modifikátory (nastavení režimu zpracování výrazu).

Při zpracování výrazu se postupuje od levé části znak po znaku. Každý má svůj význam, který říká následující tabulka:

Znak Význam Popis Příklad
^ Začátek řetězce Vynutí, že na tomto místě musí řetězec začínat. Vynutí, že řetězec musí začínat sekvencí +420 (vhodné třeba pro validaci čísla): /^\+420/.
$ Konec řetězce nebo řádku Vynutí, že zde musí končit řetězec nebo řádek. Zajištění konce řádku se pak dělá přes \z. Podrobné vysvětlení. Název souboru musí být textový soubor (končít tečkou a pak řetězcem „txt“): /\.txt$/.
. Jakýkoli znak Odchytí úplně jakýkoli znak. Ověří, že řetězec obsahuje právě jeden jakýkoli znak: /^.$/.
\d Číslo Odchytí znaky 0-9 Odchytí telefonní číslo, které neobsahuje mezery a má 9 cifer: /^(\+420)?\d{9}$/.
\s Bílý znak Odchytí mezery, odřádkování a tabulátory. Povolí mezi číslicemi v telefonním čísle mít mezery v trojčíslí: /^(\d{3}\s?){3}$/.
+ Více znaků, ale minimálně jeden Opakuje předchozí podvýraz a snaží se odchytit co největší kus. Podvýraz se musí opakovat minimálně jednou. Odchytí co nejvíc číslic bude možné, minimálně však jedno: /\d+/.
* Více znaků, může být i žádný Funguje stejně jako znak +, akorát umožňuje odchytit i prázdný řetězec (nemusí se hodnota vyskytovat). Odchytí co nejvíc číslic bude možně, pokud žádné nebude existovat, odchytí prázdný řetězec: /\d*/.
( ) Závorky Označují podvýraz. Tímto je možné uzavřít více různých značek a pak nad nimi vyžadovat například opakování, nebo odchytit jejich obsah do proměnné. Rozdělíme si PSČ na 2 části podle mezery, která je nepovinná a může se jich tam vyskytovat dokonce více: /^(\d{3})\s*(\d{2})$/
| Nebo Obsahuje podvýraz, nebo jiný podvýraz. Číslo začíná na +420 nebo +421: /^\+(420|421)\s*\d+$/.
\. Escapování Pokud chceme ve výrazu odchytit znak, který má jinak speciální význam, musíme jej oescapovat stejně, jako se escapují například řetězce v PHP. Odchytí dvojici čísel oddělených tečkou (pokud bychom neescapovali tečku, tak by byla pochopena jako „libovolný znak“): /\d+\.\d+/.

Jenom pro úplnost uvedu kompletní podobu validačního pravidla pro e-maily tak, jak jej implementuje Nette:

 1 /**
 2  * Finds whether a string is a valid email address.
 3  * @param  string
 4  * @return bool
 5  */
 6 public static function isEmail($value)
 7 {
 8     $atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part
 9     $alpha = "a-z\x80-\xFF"; // superset of IDN
10     return (bool) preg_match("(^
11         (\"([ !#-[\\]-~]*|\\\\[ -~])+\"|$atom+(\\.$atom+)*)  # quoted or unquoted
12         @
13         ([0-9$alpha]([-0-9$alpha]{0,61}[0-9$alpha])?\\.)+    # domain - RFC 1034
14         [$alpha]([-0-9$alpha]{0,17}[$alpha])?                # top domain
15     \\z)ix", $value);
16 }

Preg_match()

Základní funkce pro ověřování formátu a parsování je preg_match(), má 2 povinné parametry a třetím lze určit výstupní pole.

Příklad:

1 $psc = '272 01'; // Kladno
2 
3 if (preg_match('/^(\d{3})\s*(\d{2})$/', $psc, $parser)) {
4     echo 'PSČ je validní [' . $parser[1] . ', '. $parser[2] . ']';
5 } else {
6     echo 'PSČ je nevalidní';
7 }

Kód vrátí: PSČ je validní [272, 01].

Všimněte si jednotlivých závorek, pomocí kterých jsme výraz rozdělili na několik menších částí. Díky tomu je pak možné získat jednotlivé podvýrazy jako položky pole. Celá funkce poté vrací true nebo false podle toho, jestli se řetězec podařilo odchytit.

Někdy je ovšem orientace v číselném pořadí závorek velice náročné, protože se může počet změnit, nebo jich může být zkrátka mooooc. V takovém případě je možné závorky jednotlivě pojmenovat a pak přistupovat ke klíčům pomocí jejich názvů.

Například:

1 $phone = '777 123 456';
2 
3 preg_match('/^(?<operator>\d{3})\s*(?<number>[0-9 ]+)$/', $phone, $parser);
4 
5 echo $parser['operator']; // vrátí 777

Preg_replace()

Pomocí regexů je dále možné řetězce nahrazovat, což je obzvlášť užitečné při různých opravách formátu po uživateli.

Dejme tomu, že chceme do integeru uložit uživatelem vložené telefonní číslo, protože to vyžadujte knihovna třetí strany, ale uživatelé jej mohou vložit v docela dost divokých formátech.

V takovém případě se držím poučky:

„Buď velkorysý v tom, co přijímáš, a striktní v tom, co odesíláš“

Proto si formát automaticky přizpůsobíme. Nejprve využijeme rozparsování řetězce na jednotlivé části a poté jej podle čísel závorek zase složíme:

1 function formatPhoneNumber($phoneNumber) {
2     return (int) preg_replace(
3         '/^(\+\d{3})\s*(\d{3})\s*(\d{3})\s*(\d{3})$/',
4         '$2$3$4',
5         $phoneNumber
6     );
7 }
8 
9 echo formatPhoneNumber('+420 777 123 456');

Sestavení řetězce dle regulárního výrazu

Regexy dávají také skvělý smysl při generování nových řetězců dle složitého vzoru.

Čisté PHP pro toto nemá žádnou oporu, ale můžeme si stáhnout knihovnu třetí strany ReverseRegex, co toto dokáže.

Můžeme chtít například na základě regexu [a-z]{10} vygenerovat sadu hesel a nic nám v tom nebude bránit:

jmceohykoa
aclohnotga
jqegzuklcv
ixdbpbgpkl
kcyrxqqfyw
jcxsjrtrqb
kvaczmawlz
itwrowxfxh
auinmymonl
dujyzuhoag
vaygybwkfm

Použití je následující:

 1 use ReverseRegex\Lexer;
 2 use ReverseRegex\Random\SimpleRandom;
 3 use ReverseRegex\Parser;
 4 use ReverseRegex\Generator\Scope;
 5 
 6 # load composer
 7 require "vendor/autoload.php";
 8 
 9 $lexer = new  Lexer('[a-z]{10}');
10 $gen   = new SimpleRandom(10007);
11 $result = '';
12 
13 $parser = new Parser($lexer,new Scope(),new Scope());
14 $parser->parse()->getResult()->generate($result,$gen);
15 
16 echo $result;

Já si tímto způsobem generuji například matematické příklady v Nette v Presenteru a je to možné s opravdovou lehkostí:

 1 public function actionRegex()
 2 {
 3     $lexer = new Lexer('\d{1,3}[\+\-\*\/]');
 4     $parser = new Parser($lexer, new Scope(), new Scope());
 5     for ($i = 0; $i <= 10; $i++) {
 6         $result = '';
 7         $gen = new SimpleRandom($i);
 8         $parser->parse()->getResult()->generate($result, $gen);
 9         dump($result);
10     }
11     $this->terminate();
12 }

Důležité pro knihovnu je to, že pro stejný vstup generuje stále stejný výstup (i přesto, že by se mohlo zdát, že pro každý regulární výraz může odpovídat mnoho možných řetězců). Pokud chceme měnit vygenerovaný výraz náhodně, je potřeba měnit také seed, podle kterého se výstupní řetězec generuje. K tomu se hodí buď procházení intervalu seedů, nebo třeba funkce rand(1, 1e6).

Odchytávání chyb

V PHP je odchytávání chyb v regexech celkem peklo, ale i tak existuje řešení. Podrobně to vysvětluje článek Zrádné regulární výrazy v PHP od Davida Grudla.

Sponzorované odkazy
Pomohl Vám tento článek?