Regulární výrazy v PHP pro začátečníky II.

29. července 2010

V minulém díle jsme probrali základy syntaxe a nejjednodušší způsob použití regulárních výrazů. Nyní se podíváme na syntax Perl-compatible výrazů.

Také si probereme další způsoby jejich použití. Vše, co jsme se naučili minule na POSIX výrazech, platí i pro Perl, takže vysvětlíme pouze rozdíly a přidáme několik nových věcí.

Perl-compatible výrazy

Prvním a opticky nejpodstatnějším rozdílem oproti standardu POSIX je potřeba uzavřít celý výraz mezi oddělovače. Oddělovačem může být libovolný znak vyjma alfanumerických znaků, bílých znaků a zpětného lomítka. Často se používají znaky jako '/', '#', '~', apod. Místo 'výraz' budeme tedy psát '#výraz#', '/výraz/' nebo třeba '%výraz%'. Znak oddělovače nesmí být použit uvnitř výrazu. Pokud bychom ho přesto potřebovali do výrazu zařadit, musíme před něj přidat znak zpětného lomítka (přitom samozřejmě nesmíme zapomenout na pravidla pro zápis řetězců).

Zpětné lomítko

Znak zpětného lomítka má v Perl-compatible výrazech širší význam než v POSIXu. Jeden způsob použití jsme již rozebrali v minulém článku. Pokud je zpětné lomítko následováno jiným než alfanumerickým znakem, je tento znak chápán jako obyčejný znak (nikoli jako metaznak). Druhý způsob použití nám dovoluje vyjádřit netisknutelné znaky ve výrazech. Zde je přehled nejpoužívanějších:

sekvence význam
'\t' tabulátor (0x09)
'\n' zalomení řádku (0x0A)
'\r' návrat vozíku (0x0D)
'\f' zalomení stránky (0x0C)
'\a' zvonek (0x07)
'\e' znak escape (0x1B)
'\xHH' znak, jehož hexadecimální zápis ASCII hodnoty je HH
'\ddd' znak, jehož osmičkový zápis ASCII hodnoty je ddd

Třetí způsob použití zpětného lomítka nám dává možnost používat základní třídy znaků, podobně jako například výčtové množiny []. Následující konstrukce je možné použít téměř kdekoli ve výrazu (tj. vně i uvnitř výčtové množiny).

sekvence význam
'\d' libovolná číslice (ekvivalentní s '[0-9]' nebo '[[:digit:]]')
'\D' libovolný znak, který není číslice (ekvivalentní s '[^0-9]' nebo '[^[:digit:]]')
'\h' horizontální bílé znaky (např. mezera, tabulátor)
'\H' vše kromě horizontálních bílých znaků (doplněk '\h')
'\s' všechny bílé znaky
'\S' vše kromě bílých znaků (doplněk '\s')
'\v' vertikální bílé znaky (např. zalomení řádku)
'\V' vše kromě vertikálních bílých znaků (doplněk '\v')
'\w' znaky, které se vyskytují ve slovech (písmena, číslice a podtržítko)
'\W' znaky, které se nevyskytují ve slovech (doplněk '\w')

Existují ještě další způsoby použití zpětného lomítka. O něco později ukážeme ještě jeden z nich, a to v sekci Zpětné reference. Ostatní detaily můžete nalézt v dokumentaci PHP.

Modifikátory

Modifikátory výrazu představují asi největší změnu proti POSIX regulárním výrazům. Seznam modifikátorů se uvádí za samotný výraz (proto je také výraz uzavřen mezi oddělovače, aby bylo jasné, kde končí výraz a začínají modifikátory) a tyto modifikátory přímo ovlivňují některé aspekty vyhodnocování. Pokud bychom tedy použili jako oddělovač znak '#', celý výraz bude vypadat takto: '#výraz#modifikátory'. Každý modifikátor je reprezentován jedním písmenem, viz tabulka níže. Pokud nechceme měnit průběh vyhodnocování, není třeba uvádět žádné modifikátory.

modifikátor význam
'i' při vyhodnocování se nebude rozlišovat mezi malými a velkými písmeny
'm' zapíná víceřádkový mód (metaznaky '^' a '$' pak odpovídají začátku a konci řádku, nikoli začátku a konci řetězce)
's' metaznak tečka ('.') odpovídá i zalomení řádku (ve výchozím nastavení odpovídá všem znakům kromě zalomení řádku)
'x' všechny bílé znaky ve výrazu (kromě těch uvnitř výčtových množin) budou kompletně ignorovány, stejně jako všechny znaky mezi '#' a nejbližším zalomením řádku včetně (vhodné např. na uživatelské komentáře uvnitř složitých výrazů)
'e' podvýrazy nahrazované funkcí preg_replace() jsou před nahrazením vyhodnoceny jako PHP kód (na tento modifikátor se podíváme podrobněji v části Nahrazování podřetězců)
'S' nastaví analyzátor, aby věnoval více času na předzpracování tohoto výrazu (může se vyplatit u komplikovaných výrazů, které jsou použity vícekrát)
'U' změní chování kvantifikátorů, aby nebyly hladové (hladovost probereme podrobněji v sekci Podvýrazy)

Lokální použití modifikátorů

V některých případech můžeme chtít měnit chování pomocí modifikátorů pouze pro malou část výrazu. Lokálně vložíme modifikátor(y) přímo do výrazu syntaxí '(?modifikátory)' a tyto změny mají význam od místa uvedení dál. Například výrazu '/ab(?i)c/' odpovídají řetězce 'abc' i 'abC', ale již ne 'Abc'. Je-li lokální modifikátor uveden uvnitř podvýrazu, je jeho platnost omezena pouze na tento podvýraz. Tedy '/(a(?i)b)c/' odpovídají řetězce 'abc' i 'aBc', ale již ne 'abC'.

Lokálně lze modifikátory pouze nastavovat, nikoli zakazovat jejich platnost. Některé modifikátory navíc není možné používat lokálně. Podrobnější informace naleznete v dokumentaci PHP.

Podvýrazy

Podvýrazy jsou části regulárního výrazu uzavřené do kulatých závorek '(podvýraz)'. V minulém díle jsme ukázali, jak podvýrazy použít např. v kombinaci s větvením výrazů ('|') nebo v kombinaci s kvantifikátory ('{2}', '*', '+', atd.). Podvýrazy mají ovšem ještě další poměrně podstatný význam. Části testovaného řetězce se unifikují s podvýrazy, kterým odpovídají, a je možné s nimi dále pracovat (vypsat je, použít je při nahrazování, apod.).

Typickým příkladem použití je výběr různých částí zadaného řetězce. Řekněme, že nám uživatel zadal časový údaj ve formátu hh:mm nebo hh:mm:ss, a my bychom rádi jednak ověřili, že je formát platný a jednak také získaly jednotlivé složky (hodiny, minuty, případně i sekundy) jako čísla. Použijeme k tomu funkci preg_match(), která funguje velmi podobně jako funkce ereg(), avšak pracuje s Perlovskými regulárními výrazy namísto POSIXových.

Stejně jako ereg() dostane funkce regulární výraz a testovaný řetězec. Třetí parametr je předáván referencí a při volání je do něj uloženo pole řetězců, které byly unifikovány s jednotlivými podvýrazy. Položka s indexem nula odpovídá celému výrazu, jednotlivé podvýrazy jsou pak indexovány inkrementálně podle umístění jejich levé závorky ve výrazu.

Pro náš příklad použijeme výraz:

$ptrn = '/^([0-2]?\d):([0-5]\d)(:([0-5]\d))?$/';

Při volání preg_match($ptrn, '19:42', $matches); je do proměnné $matches uloženo tříprvkové pole. Nultý prvek je řetězec, který odpovídá celému výrazu, první a druhý pak odpovídají hodinám, resp. minutám. Tyto řetězce stačí přetypovat na int, nebo použít v numerickém výpočtu (kde se na int přetypují automaticky).

$matches = array(3) {
    [0] => string(5) "19:42",
    [1] => string(2) "19",
    [2] => string(2) "42",
}

Po volání preg_match($ptrn, '19:42:54', $matches); bude proměnná $matches obsahovat rovnou pětiprvkové pole:

$matches = array(5) {
    [0] => string(8) "19:42:54",
    [1] => string(2) "19",
    [2] => string(2) "42",
    [3] => string(3) ":54",
    [4] => string(2) "54",
}

Hladovost

V přehledu modifikátorů jsme narazili na termín hladovost kvantifikátorů. Kvantifikátory určují, kolikrát se může daný atom nebo podvýraz v testovaném řetězci vyskytovat. U kvantifikátorů s větším rozsahem může nastat situace, kdy počet opakování nemusí být jednoznačný. Tento problém nás začne trápit především v okamžiku, kdy potřebujeme podvýraz s kvantifikátory dále použít. Představme si situaci, že máme výraz '/a+/' a řetězec 'aaa'. Řetězec výrazu zcela jistě vyhovuje, ale není jasné, jak dlouhý podřetězec se s výrazem unifikuje.

Ve výchozím nastavení se výrazy vyhodnocují hladově, to znamená, že kvantifikátory se snaží maximalizovat délku řetězce, se kterým jsou unifikovány. V našem případě se s výrazem unifikuje celý řetězec 'aaa'. Pokud bychom ale přidali modifikátor 'U', kvantifikátory přestanou být hladové a podvýraz se unifikuje s nejkratším možným řetězcem (v našem případě pouze 'a'. V praxi bývá lepší na hladovost nespoléhat a psát výrazy tak, aby se vyhodnotily vždy stejně (např. '/^a+$/'). V některých případech tím vyhodnocování dokonce zrychlíme.

Další možností jak ovlivnit kvantifikátory je přidání znaku '?' za kvantifikátor. Ten způsobí, že se kvantifikátor stane líným (tzn. přestane být hladovým). Například výraz '/\d*?/' se pokusí unifikovat nejmenší možný počet číslic, stejně jako '/\d*/U'. Líné vyhodnocování lze samozřejmě použít i v kombinaci s kvantifikátorem '?', např. ve výrazu '/\d??/' má první otazník funkci kvantifikátoru a druhý zakazuje hladové vyhodnocování.

Zpětné reference

Zpětné lomítko má ještě jednu důležitou funkci, kterou jsme prozatím neprobrali — zpětné reference. Ty nám dovolují odkazovat na již unifikované podvýrazy a vynutit tak opakování určitých částí řetězce. Zpětný odkaz má tvar '\id', kde id je index podvýrazu (číslo mezi 1 a 99). Název zpětná reference napovídá, že odkazovat je možné pouze na podvýrazy, které v místě odkazu již existují (tedy zpět). Například:

'/(n|z)ahodil \1ávěs/' odpovídá řetězcům 'nahodil návěs' a 'zahodil závěs', ale již ne 'zahodil návěs'
'/\1(a|b)/' chyba, nelze odkazovat dopředu

(Ne)pojmenované podvýrazy

Jeden výraz může mít nejvýše 99 podvýrazů (jinak by nebylo možné na ně jednoduše odkazovat). V některých případech bychom ale mohli chtít použít závorky (např. v kombinaci s kvantifikátory), avšak nepotřebujeme se na takto vytvořený podvýraz nijak odkazovat. Pro tyto situace jsou zde nepojmenované podvýrazy. Je-li za otevírací závorkou uvedena sekvence '?:', nebude s tímto podvýrazem unifikován žádný podřetězec, tzn. neobjeví se v poli $matches ani na nej nelze zpětně odkazovat. Například výraz '/^(\d+)(?:.(\d+))?/' obsahuje jen dva unifikující podvýrazy '(\d+)'.

Pokud chceme cíleně nastavit modifikátory pro celý nepojmenovaný podvýraz, můžeme místo (?:(?mod)výraz) psát o trochu kratší (?mod:výraz).

PHP navíc nabízí také explicitně pojmenované podvýrazy, jejichž syntax je '(?<název>výraz)' nebo '(?\'název\'výraz)' (zpětná lomítka pouze zdůrazňují, že vkládáme apostrofy do řetězce uzavřeného v apostrofech). Takový výraz se pak v poli $matches objeví dvakrát — jednou pod klasickým číselným označením a jednou pod svým jménem.

Funkce využívající regulární výrazy

Nyní se podíváme na další způsoby využití regulárních výrazů a na související PHP funkce. Zaměříme se již výhradně na Perl-compatible funkce (POSIXové funkce pak uvedeme pouze na závěr pro srovnání).

Porovnáváme, hledáme, vybíráme

V předchozí sekci jsme představili funkci preg_match(), která umí otestovat, zda řetězec vyhovuje danému regulárnímu výrazu, případně vybrat části testovaného řetězce a vrátit je v poli. Přesněji bychom mohli říci, že preg_match() se snaží nalézt v testovaném řetězci část, která jde unifikovat s daným výrazem. Představme si například výraz '/\d+/' a řetězec '6 vajec, 3 lžíce oleje, 250g mouky'. Funkce preg_match() s výrazem unifikuje první číslo, které nalezne, a skončí.

V některých situacích bychom ale rádi nalezli všechny podřetězce, které vyhovují danému výrazu (v našem příkladu třeba všechna čísla). K tomu slouží funkce preg_match_all(), která se chová podobně jako preg_match(), ale nalezne všechny podřetězce odpovídající výrazu. Funkce vrací počet nalezených výskytů a pole $matches obsahuje všechny nalezené výskyty. Vezmeme-li příklad uvedený výše, funkce vrátí hodnotu 3 a pole $matches bude obsahovat:

$matches = array(1) {
    [0] => array(3) {	// unifikace nultého podvýrazu (t.j. celého výrazu)
        [0] => string(1) "6",
        [1] => string(1) "3",
        [2] => string(3) "250",
    }
}

Poslední funkce této kategorie preg_grep() slouží k hromadnému spuštění regulárního výrazu nad polem řetězců. Funkce dostane jako parametry regulární výraz a pole, které přefiltruje a vrátí. Ve výsledném poli zůstanou pouze položky, které vyhovovaly danému regulárnímu výrazu.

Nahrazování podřetězců

Základní funkcí pro nahrazování částí řetězců je preg_replace(). Funkce dostane regulární výraz, jeho nahrazení a řetězec, který má upravit. Všechny výskyty regulárního výrazu jsou vyměněny za náhradní řetězec a výsledek je vrácen jako návratová hodnota. V náhradním řetězci se navíc můžeme odkazovat na podvýrazy regulárního výrazu stejným způsobem, jako fungují zpětné reference ('\id').

$ptrn = '/(\d+)-(\d+)/';
$replace = '\2-\1';		// \1 a \2 jsou odkazy na podvýrazy
$str = '1-2, 3-4 a 5-6';
echo preg_replace($ptrn, $replace, $str);
// Vypíše: 2-1, 4-3 a 6-5

Jak jsme již zmínili v přehledu modifikátorů, funkce preg_replace() může být ovlivněna modifikátorem 'e'. Tento modifikátor způsobí, že každý nahrazovaný podřetězec je před svým vložením do výsledku (ale až po nahrazení zpětných referencí) vyhodnocen jako PHP kód. V následujícím příkladu se tedy pro každé slovo začínající na 'reg' nebo 'Reg' zavolá funkce print().

$ptrn = '/[Rr]eg\w+/e';
$str = 'Regulérní použití regulárních výrazů pro registraci regimentu.';
preg_replace($ptrn, 'print("\n");', $str);

Výstupem bude:

Regulérní
regulárních
registraci
regimentu

Podobně funguje také funkce preg_replace_callback(), ovšem s tím rozdílem, že místo náhradního řetězce očekává funkci zpětného volání. Tato funkce dostane vždy jako první parametr nalezený podřetězec a vrací řetězec, kterým má být nahrazen.

function my_callback($matches)
{
	return ucfirst( strtolower($matches[0]) );
}

$ptrn = '/honzík|pepíček/i';	// nerozlišuje malá a velká písmena
$str = 'honzík a PEPÍČEK si hráli s regulárními výrazy';
echo preg_replace_callback($ptrn, 'my_callback', $str);
// Vypíše: Honzík a Pepíček si hráli s regulárními výrazy

V našem příkladu funkce my_callback() vezme podřetězec, který byl unifikován s celým výrazem ($matches[0]) a opraví ho, aby měl první písmeno velké a všechna ostatní malá. Kód bychom mohli ještě vylepšit použitím anonymní funkce, ale to již jistě zvládne každý čtenář sám.

Zbývající funkce

Trochu netradiční funkcí je preg_split(), která funguje podobně jako explode(). Funkce dostane oddělovač a podle něj rozdělí vstupní řetězec na pole podřetězců (tokenů). Zatím co explode() používá jako oddělovač také řetězec, preg_split() dostane regulární výraz. Následující příklad rozdělí vstupní řetězec na slova, přičemž jako oddělovač bere libovolně dlouhou sekvenci mezer.

$str = 'Zdravíme čtenáře   Intervalu!';
$words = preg_split('/[ ]+/', $str);
echo $words[0];		// 'Zdravíme'
echo $words[1];		// 'čtenáře'
echo $words[2];		// 'Intervalu!'

Z repertoáru funkcí nám zbývají již jen dvě doplňkové. Funkce preg_quote() dostane jako parametr řetězec, který upraví, aby bylo bezpečné jej použít uvnitř regulárního výrazu a vrátí jej. Při této úpravě vloží zpětné lomítko před všechny metaznaky, čímž zruší jejich speciální funkci. Volitelně může funkce dostat ještě znak oddělovače, který pak také ošetří zpětným lomítkem. Poslední zbývající funkce je preg_last_error(), která vrací kód poslední chyby způsobené některou preg_* funkcí.

Srovnání s POSIXem

Základní funkce, které jsme zde probrali, mají svůj ekvivalent pro POSIX regulární výrazy. Zde uvedeme ty nejdůležitější.

Perl-compatible POSIX
preg_match() ereg(), eregi()
preg_replace() ereg_replace(), eregi_replace()
preg_split() split(), spliti()

POSIX funkce mají vždy dvě varianty — jednu, která rozlišuje malá a velká písmena, a jednu která je nerozlišuje. Perl-compatible funkce tyto varianty nemají, ale problém (ne)rozlišování malých a velkých písmen můžeme vyřešit přidáním modifikátoru 'i' do regulárního výrazu.

Jak jsme zmínili na začátku minulého článku, POSIX regulární výrazy jsou nyní označeny za zastaralé a není tedy vhodné je používat v nových projektech. Nicméně i přesto je potřeba mít o nich přehled, protože se s nimi stále můžete setkat při úpravách projektů existujících.

Štítky: Články, php
Martin Kruliš

doktorand na MFF-UK (katedra SW inženýrství); výzkumné zaměření na paralelismus, optimalizace, HPC, GPGPU a sémantický web

Mohlo by vás také zajímat

Nejnovější

5 komentářů

  1. patrik

    Čvc 29, 2010 v 9:37

    Super shrnutí, díky.

    Odpovědět
  2. iarro

    Čvc 29, 2010 v 10:11

    Užitečný článek. O zpětných referencích jsem dosud nevěděl.

    Odpovědět
  3. Jakub

    Lis 29, 2011 v 11:00

    Moje otázka nesouvisí s článkem, ale doufám, že mi někdo poradí. Mám s PHP tento problém (jsem úplný začátečník): Generuji tři řádky HTML v PHP, mám tam nějaké proměnné — vše mi funguje, ale ve výsledném HTML je za každou proměnnou nový řádek. Použil jsem všechny způsoby zápisu a to včetně HEREDOC a nic. Za každou proměnnou se udělá konec řádku. Nevíte prosím co s tím? Na funkčnost to nemá vliv, ale zajímalo by mne to. Děkuji.

    Odpovědět
  4. Petr Kobelka

    Čvn 4, 2013 v 11:57

    Při tvorbě webu už delší dobu nepoužívám ereg+ ale preg_+ a to z toho důvodu, že ere+ je označen jako deprecated a brzy z PHP zmizí.

    Odpovědět
  5. Josef

    Kvě 5, 2015 v 23:16

    a co když chci ve stringu vše co je za určitým znakem zahodit po urcitej znak ? je na to něco ? str replace nejak nejde užít a explode také ne. jedině asi si pro to udělat funkci.

    Odpovědět

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *