Regulární výrazy

Regulární výrazy jsou velmi mocným nástrojem sloužícím pro práci s textovými daty. Jsou používány v různých programech a textových editorech, jako awk, sed, vi, ed, emacs a v některých shellech. Regulární výrazy v Perlu tvoří jakousi nadmnožinu pro všechny tyto prostředky. To znamená, že umožňuje všechny operace v nich přípustné (i když třeba způsob zápisu se liší).

Regulární výraz je vzorem, který popisuje obsah řetězce. Říká, co se v řetězci smí a nesmí vyskytovat, jaký je možný počet opakování těchto výskytů, na jaké pozici v řetězci má co být apod. Tento vzor se s řetězcem porovnává a výsledkem porovnání je pravdivá hodnota v případě, že řetězec vzoru odpovídá, v opačném případě je výsledkem hodnota nepravdivá. Konkrétní podoba pravdivé a nepravdivé hodnoty závisí na typu operátoru, který se vzory pracuje. Existuje i operátor, který na základě regulárních výrazů umožní nahrazení některých nalezených částí řetězce jinými řetězci.

Regulární výrazy se uzavírají do dvojice lomítek. Pro nalezení vzoru v řetězci se používá operátor m// a pro nahrazení operátor s///. Pro ohraničení vzoru je možné použít i jiných znaků než pouze /. Se vzorem pro vyhledávání i s řetězci pro nahrazení se obyčejně zachází jako s řetězci v uvozovkách (pokud jako oddělovač není použit apostrof), takže zde probíhá vkládání hodnot proměnných a některé znaky zde mají speciální význam.

Jednoduché vzory

Některé znaky odpovídají samy sobě, některé mají zvláštní význam a reprezentují jiné znaky či upřesňují vlastnosti podřetězeců ať předcházejících, tak i následujících.

Většina znaků použitelná v regulárním výrazu odpovídá sama sobě. Vypadá-li vzor např. takto

/Perl/

pak je prohledávání v řetězeci úspěšné, jestliže kdekoliv uvnitř řetězce je obsažen podřetězec Perl. Nezálěží na tom kde, kolikrát a co se nachází v okolí tohoto podřetězce.

Metaznaky

Některé znaky použité v regulárním výrazu neodpovídají samy sobě, ale mají speciální význam. Nazývají se metaznaky a jsou to tyto znaky:

\ | ( ) ^ $ [ . * + ? {

Pro potlačení metavýznamu použijeme obrácené lomítko.

Tabulka metaznaků

Znak Význam
\potlačuje metacharakter následujícího znaku
nebo s~následujícím znakem tvoří metasymbol
| odděluje možné varianty
( ) seskupuje znaky uvnitř do jedné skupiny
^začátek řetězce nebo začátek řádku
$ konec řetězce nebo znak před začátkem řádku
[ začátek třídy znaků, hledá se jeden z~uvedených znaků
. jakýkoliv jeden znak (mimo znaku nového řádku \n)
*výskyt předcházejícího symbolu 0 nebo vícekrát
+výskyt předcházejícího symbolu 1 nebo vícekrát
?výskyt předcházejícího symbolu 0 nebo 1x
{začátek určení rozsahu pro výskyt předcházejícího symbolu

Metaznaky a vkládání

Je třeba si uvědomit, že u vzorů podléhajících vkládání je nejprve prováděno vkládání hodnot proměnných a potom teprve porovnávání s řetězci. Proto v případě, že některý z metaznaků bude součástí jména proměnné, nemusí být chápán jako metaznak, ale jako součást jména proměnné. V případě, že není jasné, zda se jedná o vkládání nebo o speciální symbol regulárního výrazu, dostává přednost možnost, že se jedná o metasymbol.

/123$promenna/;

Hodnoty proměnných $|, $( a $) nejsou vkládány nikdy. U zápisu $jmeno[...] nelze říci, zda bude považován za hodnotu proměnné $jmeno následovanou třídou znaků, nebo za prvek pole @jmeno.

Může nastat situace, že vkládaná hodnota proměnné obsahuje některý z metasymbolů. Potom jsou tyto znaky považovaány za metasymboly a je tak s nimi pracováno.

$vyraz = '1+2';
/$vyraz/;

# to samé jako /1+2/

Při vkládání hodnot proměnných je tedy třeba dávat pozor na to, co obsahují. Jinak bychom mohli získat neočekávané výsledky.

Metasymboly

Metasymboly jsou sekvence o více než jednom znaku, které mají speciální význam. Pokud znak obráceného lomítka uvedeme před znakem, který není metaznakem, nebo před skupinou znaků, stává se z celé této sekvence metasymbol. Dalším typem metasymbolu mohou být kvantifikátory či rozšířené vzory.

Tabulka metasymbolů

\0 znak ASCII NUL
\nnn znak s ordinálním číslem nnn (zadáno osmičkově, max. do \377
\číslice odpovídá předchozímu n-tému nalezenému podřetězci
\a pípnutí (bell)
\A začátek řetězce
\b znak backspace
\b hranice slova
\B jinde než na hranici slova
\cX znak Ctrl+X
\C jeden byte v jakékoliv znakové sadě (v utf8 může být nebezpečné)
\d číslice
\D jiný znak než číslice
\e znak Escape
\E ukončovací symbol k L, U nebo Q
\f znak nové stránky
\G pravdivé za hranicí posledního nalezení vzoru při použití m//g
\l následující znak malými písmeny
\L následující znaky malými písmeny (do \E)
\n znak nového řádku
\N{NAME} pojmenovaný znak (např. \N{greek:Sigma}
p{VLASTNOST} libovolný znak se zadanou vlastností
P{VLASTNOST} libovolný znak bez zadané vlastnosti
\Q potlačuje metavýznam následujících znaků (do \E)
\r znak návratu vozíku
\s prázdný znak (mezera, tabulátor apod.)
\S jiný než prázdný znak
\t tabulátor
\u následující znak velkým písmenem
\U následující znaky velkým písmenem (do \E)
\w alfanumerický znak [a-zA-Z_0-9]
\W jiný než alfanumerický znak
\x{xxx} znak zadaný hexadecimálně
\X znak v znakové sadě Unicode
\z konec řetězce
\Z konec řetězce nebo pozice před koncem řádku na konci řetězce

Rozdíl mezi $ a \Z a mezi ^ a \A se projeví pouze v případě, že se z řetězcem zachází jako s více řádky (s použitím modifikátoru m u operátoru pracujícího s regulárním výrazem). V tomto případě se ^ shoduje se začátkem řádku a $ s koncem řádku.

Složené závorky u \p a \P jsou nepovinné, je-li vlastnost pojmenovaná jedním znakem.

Závorky u \x jsou volitelné, když má hexadecimální číslo jednu nebo dvě číslice. V opačném případě je třeba pro správné fungování závorky uvést (pro znaky Unicode).

Znak zadaný osmičkově musí být dvouznakový nebo tříznakový, jinak je to považováno za označení předchozího nalezeného podřetězce. Počáteční nula je nepovinná, povinná je pouze pro hodnoty menší než \010. V případě, že bylo doposud nalezeno a zapamatováno n podřetězců, sekvence \n s více než jednou číslicí bude reprezentovat n-tý nalezený podřetězec. V opačném případě tato sekvence bude považována za znak vyjádřený pomocí ordinální hodnoty osmičkově.

Pro metasymbol \b existují dvě interpretace. Uvnitř třídy znaků je považován za znak backspace, jinde jako hranice slova. Je to proto, že uvnitř třídy znaků může být pouze symbol, který reprezentuje nějaký znak. Hranice slova konkrétním znakem není, proto nemůže být součástí třídy znaků.

Použití \N{NAME} je podmíněno použitím pragmatu use charnames, což umožňuje pojmenovávat znaky jejich jmény.

Sekvence \číslice znamená n-tý nalezený a zapamatovaný podřetězec od začátku prohledávání. Pomocí následujícího příkladu lze zjistit, zda se v textu nacházejí stejná dvě po sobě následující slova. Nalezené slovo je díky závorkám zapamatováno a v regulárním výrazu je na něj možné se odvolat pomocí metasymbolu \1.

/(\w+)\s+\1/ig

Je třeba rozlišit mezi symboly \číslice a $číslice.

$x = 'abc';
$x =~ s/(abc)/\1/;  # vypíše varování \1 better written as $1

print \1;           # vitiskne 'SCALAR(0x8060b98)'
print $1;           # vytiskne 'abc'

Pomocí symbolu \X můžeme nalézt znak zadaný v sadě Unicode. Takové znaky jsou větší než jeden byte, a proto jsou reprezentovány několikaznakovými sekvencemi. Chceme-li pracovat s jednotlivými byty takového znaku, použijeme metasymbol \C.

Třídy znaků

V regulárních výrazech je možné používat tzv. třídy znaků, což je způsob, jakým popsat skupinu znaků, která odpovídá příslušné znakové třídě. Vlastnosti třídy znaků se uzavírají do hranatých závorek. Existují čtyři způsoby, jak vlastnosti znaků popsat.

Výčet znaků

Zapisuje se do dvojice hranatých závorek. Ve výčtu lze použít i metasymbolů, ale pouze těch, které odpovídají nějakému konkrétnímu znaku. Lze např. použít \d, \s, \w, ale ne např. \L, \B apod., které jsou pravdivé za určitých okolností nebo ukončují platnost jiného metasymbolu, ale neodpovídají žádnému konkrétnímu znaku. Chálání \b uvnitř třídy znaků. Metaznaky ztrácejí uvnitř hranatých závorek svůj význam a stávají se obyčejným literálem (např. | neznamená variantu, ale znak |).

[abc]         # jeden ze tří znaků a, b nebo c
[a|c]         # jeden ze tří znaků a, b nebo |
[ab|ac|ad]    # jeden z pěti znaků a, b, c, d, |

Uvnitř [ ] je možné použít znaku -, který naznačuje interval znaků.

[a-z]         # jeden ze znaků az
[0-9]         # jedna číslice

Chceme-li, aby znak - ztratil svůj speciální význam, uvedeme před něj obrácené lomítko nebo ho dáme jako první nebo poslední znak uvnitř třídy znaků.

[a\-z] [-az] [az-]     # jeden ze tří znaků a, z a
-

Rozsah je možné uvnitř jedné třídy použít vícekrát.

[a-zA-Z]       # jedno malé nebo velké písmeno

Symbol ^ na začátku třídy znaků říká, že se má hledat libovolný znak mimo znaky uvedené v hranatých závorkách.

[^0-9]         # jeden libovolný znak mimo číslice

Pro jiné než alfanumerické znaky je lepší rozsah zadávat následovně:

[\000-\077]

Perlové třídy znaků

Perl poskytuje sadu metasymbolů, které reprezentují třídy znaků. Metasymbol s velkým písmenem znamená negaci třídy znaků zadané malým písmenem.

Třída znaků Význam Pomocí výčtu
\d číslice [0-9]
\D ostatní než číslice [^0-9]
\s bílý znak [ \f\n\r\t]
\S ostatní než bílý znak [^ \f\n\r\t]
\w znak slova [a-zA-Z_0-9]
\W ostatní než znak slova [^a-zA-Z_0-9]

Narozdíl od výčtů zadaných pomocí rozsahu umějí pracovat s národními prostředími (se specifickými znaky národních sad).

$x = 'č';
$x =~ /\w/;    # nenalezeno
$x =~ /[a-z];  # nenalezeno

use locale;
$x =~ /\w/;    # nalezeno
$x =~ /[a-z];  # nenalezeno

Třídy znaků zadané pomocí Unicode vlastností

Třetí možností je popsat třídu znaků pomocí vlastností znaků Unicode. V tomto případě je třeba použít pragmtický modul utf8. Každá z vlastností má své pojmenování, modul utf8 zajistí, že jsou na místo takové třídy znaků dosazeny všechny symboly odpovídající zadaným vlastnostem (znaky jsou uloženy v souborech, jména tříd jsou mapována na jména těchto souborů). Je-li jméno třídy víceznakové, je nutné pro jeho ohraničení použít složené závorky. Pro jednoznaková jména povinné nejsou. Znaky mající určité vlastnosti jsou pojmenována pomocí \p{VLASTNOST}, zápis \P{VLASTNOST} reprezentuje všechny znaky, které zadané vlastnosti nevyhovují.

Třída znaků zadaná pomocí konstrukce \p{} nebo \P{} je použitelná samostatně, stejně tak jako uvnitř hranatých závorek (u obou případů podobně jako u Perlových tříd).

Perl nabízí jak standardní Unicode třídy znaků, tak třídy znaků definované pomocí standardních Unicode tříd. Programátorovi také umožňuje definovat si vlastní Unicode třídy.

Tabulka: Některé příklady standardních Unicode tříd

Třída znaků Význam, ekvivalent
IsLl malé písmeno
IsLu velké písmeno
IsNd desítková číslice
IsPi otevírací uvozovka
IsPf uzavírací uvozovka
IsSm matematický symbol
IsSc symbol měny
IsZl oddělovač řádků
IsZp oddělovač odstavců
IsZs mezera

Perl definuje vlastní Unicode třídy znaků, které obsahují některé ze standardních tříd znaků. Tyto třídy se pak chovají stejně jako standardní třídy Unicode.

Tabulka: Perlové Unicode třídy zanků

Třída znaků Význam, ekvivalent
IsASCII [\x00-\x7f]
IsAlnum [\p{IsLl}\p{IsLu}\p{IsLt}\p{IsLo}\p{IsNd}]
IsAlpha [\p{IsLl}\p{IsLu}\p{IsLt}\p{IsLo}]
IsCntrl \p{IsC}
IsDigit \p{Nd}
IsGraph [^\pC\p{IsSpace}
IsLower \p{IsLl}
IsPrint \p{IsC}
IsPunct \p{IsP}
IsSpace [\f\n\r\t\p{IsZ}]
IsUpper [\p{IsLu}\p{IsLt}]
IsWord [_\p{IsLl}\p{IsLu}\p{IsLt}\p{IsLo}\p{IsNd}]
IsDigit [0-9a-fA-F]
IsC Ctrl znaky
IsL písmena
IsM znaménka
IsN číslice
IsP interpunkce
IsS symboly
IsZ oddělovače

Třídy znaků pomocí typu písma

Pomocí vlastností Unicode lze rovněž popsat typ písma. Ve složených závorkách je slovo In a pak následuje název typu písma. Exituje řada typů, pro zajímavost např. InArabic, InTibetan, InCurrencySymbols, InGeometricShapes nebo typ písma s nejdelším názvem InUnifiedCanadianAboriginalSyllabics. U těchto tříd znaků se testuje, zda znaky leží v rozdahu příslušného typu písma (tento rozsah je také zadán v souborech).

Definování vlastních Unicode tříd znaků

Pro definici vlastní třídy znaků je třeba definovat funkci stejného jména jako je jméno třídy znaků. Tato funkce se musí být viditelná v balíku, kde chceme třídu znaků použít. To znamená, že funkce buď musí být definována v tomto balíku, nebo do tohoto balíku musí být importováno její jméno. Návratová hodnota funkce musí odpovídat formátu používaném v souborech definujících třídy znaků. Na jednom řádku je buď jedna hodnota, která znamená konkrétní znak nebo jsou tam hodnoty dvě oddělené tabulátorem nebo mezerou. V tomto případě hodnoty znamenají dolní a horní hranici pro ordinální čísla znaků.

sub IsHexa {
     return <<EOF;
61	66
41	46
30	39
2b
2d
EOF
}

Chceme-li použít pro definici nových tříd znaků již existující třídy, musíme je uvést prefixem utf8:: a ještě jedním znakem. Znak + znamená, že do nové třídy mají být zahrnuty znaky s této třídy, znak ! znamená všechny, kromě znaků z této třídy a znak - znamená odebrání znaků ze zadané třídy ze znaků, které byly do nově definované třídy přidány.

sub NovaTrida {
     return <<EOF;
+utf8::JednaTrida
!utf8::DruhaTrida
-utf8::TretiTrida
EOF
}

Třídy znaků podle standardu POSIX

Poslední možností vyjádření třídy znaků je pomocí stylu POSIX. Třída je popsána pomocí konstrukce [:NÁZEV:] a je ji možné použít pouze uvnitř třídy znaků vyjádřené výčtem (tzn. uvnitř dvojice hranatých závorek způsobem [[:alpha:]]). Bez ohraničovacích hranatých závorek je celá interpretace třídy znaků jiná.

/^[:digit:]$/;
      # špatně, ve skutečnosti reprezentuje třídu znaků
      # obsahující pět znaků (':', 'd', 'i', 'g', 't')
/^[[:digit:][:alpha:]_]+$/	
      # správně, reprezentuje alfanumerický znak 
      # (číslice, písmeno, podtržítko)

Tabulka: Třídy znaků podle standardu POSIX

Třída Unicode třída Význam
alnum \p{IsAlnum} alfanumerický znak
alpha \p{IsAlpha} písmeno
ascii \p{IsASCII} znak s ordinálním číslem od 0 do 127
blank \p{IsSpace} bílý znak
cntrl \p{IsCntrl} Control sekvence -- neviditelný znak s nějakým významem (např. nový řádek, backspace atd.), většinou se jedná o znaky s ordinálním číslem menším než 32
digit \p{IsDigit} desítková číslice (\d)
graph \p{IsGraph} alfanumerický nebo interpunkční znak
lower \p{IsLower} malé písmeno
print \p{IsPrint} alfanumerický nebo interpunkční znak nebo mezera
punct \p{IsPunct} interpunkční znak
space \p{IsSpace},
\p{IsSpacePerl}
bílý znak (\s))
upper \p{IsUpper} velké písmeno
word \p{IsWord} alfanumerický znak nebo podtržítko (\w)
xdigit \p{IsXDigit} hexadecimální číslice ([a-fA-F0-9]

Rozšířením Perlu je použití znaku ^ před názvem třídy. To opět znamená negaci uvedené třídy a taková třída znaků bude obsahovat všechny znaky kromě těch, které jsou obsaženy v negované třídě.

[:^digit:]	# všechno ostatní než číslice
[:^space:]	# všechno ostatní než bílý znak

Kvantifikátory

Doposud jsme se seznámili, jakým způsobem specifikovat znak, který se má v řetězci objevit (ať byly znaky zadány jako literály nebo vyjádřeny pomocí třídy znaků). Ve všech případech takovéto vyjádření znamenalo výskyt právě jedenkrát. Kvantifikátory jsou symboly, pomocí nichž můžeme specifikovat počet opakování určitého znaku či skupiny znaků. Je to vhodné proto, abychom nemuseli výskyt určitého znaku explicitně uvádět vícekrát.

Tabulka: Kvantifikátory

Kvantifikátor Význam
* 0 nebo více výskyt předcházejícího znaku
+ 1 nebo více výskyt předcházejícího znaku
? 0 nebo 1 výskyt předcházejícího znaku
{POČET} výskyt předcházejícího znaku přesně POČET krát
{MIN,} výskyt předcházejícího znaku aspoň MIN krát
{MIN, MAX} výskyt předcházejícího znaku aspoň MIN krát a nejvíce MAX krát

Kvantifikátory se vztahují k bezprostředně předcházejícímu znaku, metasymbolu či třídě znaků. Chceme-li, aby se vztahoval k více znakům, je třeba tyto znaky seskupit pomocí kulatých závorek.

/abc*/	    # odpovídá "ab", "abc", "abcc" atd.
/a(bc)*/    # odpovídá "a", "abc", "abcbc" atd.

Kvantifikátory jsou tzv. hladové. To znamená, že se snaží nalézt tak velkou část řetězce, jak je to jenom možné. Chceme-li, aby se našel řetězec o minimální délce, který stále vyhovuje zadanému vzoru, je třeba za kvantifikátorem uvést znak ?.

$html = "<B>Text</B>";
$html =~ /<(.*)>/;
        # maximální hledání
        # proměnná $1 obsahuje řetězec "B>Text</B"
$html =~ /<(.*?)>/;
        # minimální hledání
        # proměnná $1 obsahuje řetězec "B"

Pozice (kotvy)

Na rozdíl od předchozích specifikací znaků, kdy se jednalo o skutečné znaky, pozice znamenají konkrétní místa v řetězci a ne konkrétní znak. Jedná se o tzv. vyhladávání s nulovou délkou.

Hranice slova - \b, \B

Hranice slova je definována jako místo mezi alfanumerickým znakem (včetně podtržítka) a nelafanumerickým znakem (neboli mezi znaky z tříd \w a \W). V regulárním výrazu je reprezentována metasymbolem \b (uvnitř třídy znaků ale znamená znak backspace).

$text = 'Nazev: Vyrobek_10, model 123
Cena: $50
Kontakt: mail@mail.cz';

$text =~ s/\b/|/g;  # hranice slova nahradíme znakem |
print $text;

# vytiskne
|Nazev|: |Vyrobek_10|, |model| |123|
|Cena|: $|50|
|Kontakt|: |mail|@|mail|.|cz|

Symbol \B znamená pozici mimo hranici slova, tzn. mezi dvěma znaky, které oba patří do třídy \w nebo \W.

Začátek řádku a řetězce - \A, ^

Pozice začátku řetězce se vyjádří pomocí symbolu \A. Symbol ^ se chová stejně jako \A mimo případ, kdy použijeme modifikátor m (zacházíme s řetězcem jako s více řádky). V takovém případě symbol ^ navíc odpovídá pozici před znakem bezprostředně následujícím znaku konce řádku.

$text = "první\ndruhý";
$text =~ s/^/<začátek řádku>/g;
$text =~ s/\A/<začátek řetězce>/g;
print $text;

# vytiskne 
<začátek řetězce><začátek řádku>první
druhý

$text = "první\ndruhý";
$text =~ s/^/<začátek řádku>/mg;
$text =~ s/\A/<začátek řetězce>/mg;
print $text;

# vytiskne
<začátek řetězce><začátek řádku>první
<začátek řádku>druhý

Konec řádku a řetězce - \z, \Z, $

Rozdíl mezi těmito pozičními symboly je podobný jako u symbolů pro zápis začátku řetězce a řádku. Symbol \z znamená konec řetězce bez ohledu na to, jak s řetězcem pracujeme. Symbol $ znamená také konec řetězce, ale navíc odpovídá pozici předcházející znaku konce řádku v případě, že použijeme modifikátor m. Symbol \Z znamená pozici předcházející znaku konce řádku na konci řetězce, nebo pozici předcházející konci řetězce, není-li na jeho konci znak konce řádku (je to to samé vyjádření pozice jako pomocí $, není-li použito modifikátoru m).

$text = "první\ndruhý\n";
$text =~ s/\z/<konec řetězce, před \\n>/g;
$text =~ s/$/<konec řádku>/g;
$text =~ s/\z/<konec řetězce>/g;
print $text;

# vytiskne 
první
druhý
<konec řetězce, před \n><konec řádku><konec řetězce>

$text = "první\ndruhý\n";
$text =~ s/\z/<konec řetězce, před \\n>/mg;
$text =~ s/$/<konec řádku>/mg;
$text =~ s/\z/<konec řetězce>/mg;
print $text;

# vytiskne 
první<konec řádku>
druhý<konec řádku>
<konec řetězce, před \n><konec řádku><konec řetězce>

Konec posledního úspěšného nalezení vzoru - \G

Použijeme-li u regulárního výrazu modifikátor g, neprovádí se prohledávání řetězce vždy od začátku, ale od pozice, kde skončilo poslední úspěšné prohledávání. Při neúspěšném prohledávání se ukazatel posune zpět na začátek řetězce (pokud nepoužijeme modifikátor c).

$text = '1abc2x3--4';
while ($text =~ /(\d)/g) {
      print $1;   # vytiskne postupně 1, 2, 3, a 4
}

Pozici, kde vyhledávání skončilo, zjistí funkce pos (znaky v řetězci se počítají od hodnoty proměnné $[).

$text = '1abc2x3--4';
while ($text =~ /(\d)/g) {
      print "nalezeno $1 na pozici ", pos($text)-1, "\n";
}
# vytiskne
nalezeno 1 na pozici 0
nalezeno 2 na pozici 4
nalezeno 3 na pozici 6
nalezeno 4 na pozici 9

Uvnitř vzoru je tato pozice označena metasymbolem \G. Při prvním prohledávání je tato pozice totožná s pozicí \A. Použijeme-li tuto kotvu při dalším prohledávání, můžeme si představit, že prohledávaný řetězec začíná koncem posledního úspěšného prohledávání a že \G opět znamená konec řetězce.

Varianty

K oddělení možných variant slouží symbol |. Jeho platnost je až na hranice nejvnitřnějších kulatých závorek nebo na celý vzor, nejsou-li závorky použity.

/aaa|bbb|ccc/;     # odpovídá řetězci 'aaa' nebo 'bbb' nebo 'ccc'

/aaa|b|c/;         # odpovídá řetězci 'aaa' nebo 'b' nebo 'c'

/aa(a|b|c)/;       # odpovídá řetězci 'aaa' nebo 'aab' nebo 'aac'

/aa(?:a|b|c)/;     # odpovídá řetězci 'aaa' nebo 'aab' nebo 'aac'
                   # nedochází k zapamatování varianty v $1

Je-li v jednom okamžiku možné provádět porovnání s více variantami, toto porovnávání začíná vždy zleva. Proto záleží na pořadí, v jakém jednotlivé varianty uvedeme.

$text = 'aaabbbccc';

$text =~ /b+|b+c+/;   # nalezeno 'bbb'
$text =~ /b+c+|b+/;   # nalezeno 'bbbccc'

V obou případech se hledá buď řetězec pouze se znaky b nebo se znaky b následovanými znaky c. V obou případech je nalezena ta varianta, která je na prvním místě, i když je možné v tom stejném řetězci nalézt varianty obě. V tomto případě neplatí pravidlo, že je vždy nalezena nejdelší varianta.

Priorita

Vyhodnocování částí regulárních výrazů (stejně jako jakýchkoliv výrazů) se děje podle určitých pravidel. Některé části jsou vyhodnoceny dříve, jiné později. Všechno se děje podle následující priority:

závorky   ( ) (?: )
kvantifikátory ? + * {m,n} ?? +? *? {m,n}?
posloupnosti znaků a pozice abc \A \Z ^ $ (?= ) (?! )
varianty |

K změně priority lze použít kulatých závorek. Pak je třeba si uvědomit, že dochází k zapamatování uzávorkovaných částí (v regulárním výrazu do metasymbolů \1, \2 atd., jinak do proměnných $1, $2 atd.) Aby se tomu zabránilo, použije se místo ( ) symbolů (?: ).

/^a|b/       # znak a na začátku řetězce nebo b kdekoliv
/^(a|b)/     # znak a nebo b na začátku řetězce
/a|b{2,}/    # znak a nebo dva a víc znaků b
/(a|b){2,}/  # dva nebo víc znaků a nebo b

Změna priority, seskupování a zapamatování

Pomocí seskupení můžeme změnit pořadí provádění regulárního výrazu (prioritu) nebo provést uložení nalezeného obsahu do proměnné. Seskupování znamená spojení několika částí vzoru do jednoho celku pomocí kulatých závorek. Zapamatování s ním úzce souvisí, probíhá automaticky v okamžiku seskupení, pokud to explicitně nezakážeme.

Seskupování

Pro změnu chování některých speciálních symbolů uvnitř regulárního výrazu je možné použít kulaté závorky. Použijeme je např. pro změnu priority při použití kvantifikátorů (ty se vztahují vždy k bezprostředně předcházejícímu symbolu), variant (platnost od znaku | po kulaté závorky nebo konec vzoru) nebo omezení platnosti modifikátorů jen na určitou část řetězce.

/ab+/              # znamená 'ab', 'abb', 'abbb' atd.
/(ab)+/            # znamená 'ab', 'abab', 'ababab' atd.

/^Brno|Praha$/     # tento vzor odpovídá řetězci 'Brno' na začátku řetězce
                   # nebo řetězci 'Praha' na konci řetězce
/^(Brno|Praha)$/   # tento vzor odpovídá řetězci 'Brno' nebo 'Praha'
                   # (nic jiného řetězec už neobsahuje)

/aa ((?i)b+)/      # na druhé slovo upltňuje modifikátor i
                   # odpovídá 'aa BBB' i 'aa bB' apod.

/aa ((?-i)b+)/i    # na druhé slovo se zakazuje použití modifikátoru i
                   # odpovídá 'aa bb', 'Aa bb', ale ne 'aa B' apod.

Sekvence typu (?i) a (?-i) patří mezi rozšířené vzory.

Zapamatování

Vedlejším efektem seskupení je to, že v případě, je-li některá část vzoru uzavřená do kulatých závorek úspěšně nalezena, je uložena do speciální proměnné. Kolik se najde podřetězců, tolik proměnných bude naplněno.

$text =~ /^(\w+)\s+(\w+)/;

Je-li celý tento vzor nalezen, uloží se do proměnných $1 a $2 obě slova, tak že v proměnné $1 bude první slovo a v proměnné $2 druhé slovo.

$text = 'aa bbb cc';
$text =~ /^(\w+)\s+(\w+)/;
print $1;        # vytiskne 'aa'
print $2;        # vytiskne 'bbb'

Nastavení proměnných $číslice se děje při každém použití regulárního výrazu. Nezůstávají v nich tedy hodnoty z předchozího porovnání.

$text = '123';
$text =~ /(.)(.)(.)/;
# proměnné $1, $2 a $3 obsahují hodnoty 1, 2 a 3
/(.)/;
# proměnná $1 obsahuje hodnotu 1, proměnné $2 a $3
# obsahují nedefinované hodnoty
$text = 'aa bb';
$text =~ /((\w+) (\w+))/;
print $1;               # vytiskne 'aa bb'
print $2;               # vytiskne 'aa'
print $3;               # vytiskne 'bb'

Zpětné odkazy

Uvnitř vzoru je možné se na již nalezené podřetězce odvolávat pomocí tzv. zpětných referencí (odkazů). Pomocí symbolu \1 získáme ve vzoru první zapamatovanou hodnotu, pomocí \2 druhou atd. U více než dvouznakových zpětných odkazů existuje dvouznačnost.

$text = 'aa bb bb cc dd dd';
# chceme vypsat slova, která se vyskytují dvakrát po sobě.
while ( $text =~ /(\w+)\W+\1/g ) {
       print "Duplicita $1\n";
}

Použití $číslice uvnitř vzoru není ekvivalentní použití \číslice.

$text = 'aa bb bb cc aa';
while ($text =~ /\b(\w+)\W+$1\b/g) {
	print "Duplicita: $1\n";
}
# vytiskne
Duplicita: aa
Duplicita: cc

Seskupení bez zapamatování

Pomocí (?:) (patří mezi rozšířené vzory). Tento zápis funguje stejně jako obyčejné kulaté závorky s tím rozdílem, že se nevytvářejí zpětné reference a že ani nalezené podřetězce nejsou ukládány do proměnných $číslice.

$text =~ /(ab)+/;    # hledá se podřetězec obsahující několikrát
                     # po sobě 'ab', je uložen do proměnné $1
$text =~ /(?:ab)+/;  # hledá se podřetězec obsahující několikrát
                     # po sobě 'ab', není uložen do proměnné $1

Je doporučeno používat tohoto zápisu všude tam, kde zapamatování není nutné. Zvýší se tím i efektivita zpracování regulárního výrazu, protože není třeba provádět ukládání hodnot do porměnných.

Modifikátory

Pro změnu chování operátorů pracujících s regulárními výrazy je možné použít tzv. modifikátory (někdy se hovoří o volbě či příznaku). Jedná se o znaky, které se umístí za koncový oddělovač operátoru. Modifikátorů může být zároveň použito více a nezáleží na tom, v jakém pořadí jsou uvedeny.

Modifikátor Význam
i nerozlišuje mezi malými a velkými písmeny
m zachází s řetězcem jako s více řádky
o překládá vzor pouze jednou
s zachází s řetězcem jako s jedním řádkem, umožní to, že . odpovídá i znaku konce řádku
x ignoruje bílé znaky ve vzoru a vzor může obsahovat i komentáře

Použití modifikátoru x:

m/              # vzor pro zjištění, zda řetězec je číslo
   ^
   [+-]?        # počáteční nepovinné znaménko
   ( \d*        # nějaké číslice
     [.,]\d+)?  # desetinná tečka nebo čárka
     \d+        # desetinná místa
   )
   |            # nebo
   ( \d* )      # pouze číslice
   (              # číslo může být v semilogaritmickém tvaru
    [eE][+-]?\d+  # ten se skládá z písmene e nebo E
   )?           # a celého kladného nebo záporného exponentu
   $
/x

Všechny tyto modifikátory jsou společné pro operátory pracující s regulárními výrazy. Konkrétních operátory mohou mít ještě některé další modifikátory.

Proměnné související s regulárními výrazy

Tyto proměnné (mimo $*) jsou ovlivněny výsledkem vyhledávání - v případě úspěchu jsou nastaveny, v případě neúspěchu jsou jim ponechány původní hodnoty.

Proměnná $& ($MATCH) obsahuje celý řetězec nalezený posledním hledáním, proměnná $` ($PREMATCH) řetězec předcházející $&, $' ($POSTMATCH) obsahuje řetězec následující za $&.

$cas = "od 15 do 18 hodin";
$cas =~ /(\d+)(.*?)(\d+)/;   # číslice, pak cokoliv a pak opět číslice
print $&;     # vytiskne '15 do 18' (odpovídá vzoru)
print $`;     # vytiskne 'od ' (od začátku řetězce po začátek vrozu)
print $';     # vytiskne ' hodin' (od konce vzoru po konec řetězce)

Použijeme-li někde v regulárním výrazu kulaté závorky pro zapamatování podřetězce, obsah posledních závorek se bude nacházet v proměnné $+ ($LAST_PAREN_MATCH).

$text = '1 2 3';
$text =~ /(\d) (?:\d) (\d)/;
print $+;   # vytiskne '3'
$text =~ /(\d) (?:\d) \d/;
print $+;   # vytiskne '1' ( (?: ) neprovádí zapamatování
$text =~ /\d (?:\d) \d/;
print $+;   # nevytiskne nic (proměnná $+ má nedefinovanou hodnotu)

Pozice, kde se nachází začátek a konec úspěšného nalezení, a pozice začátků a konců zapamatovaných podřetězců se nacházejí v polích @- (@LAST_MATCH_START) a @+ (@LAST_MATCH_END). První prvek pole @- obsahuje pozici začátku nalezeného podřetězce a první prvek pole @- obsahuje pozici konce nalezeného podřetězce (odpovídá návratové hodnotě funkce pos). Hodnoty $-[1] a $+[1] udávají hranice, kde se v řetězci nalézá obsah proměnné $1 atd.

$text = 'abc 1 2 3 abc';
$text =~ /(\d) (?:\d) (\d)/;
print "celý vzor - začátek: $-[0], konec: $+[0]\n";
for (1..$#-) {
    print "$_. zapamatovaná část - začátek: $-[$_], konec: $+[$_]\n";
}
# vytiskne
celý vzor - začátek: 4, konec: 9
1. zapamatovaná část - začátek: 4, konec: 5
2. zapamatovaná část - začátek: 8, konec: 9

Proměnná $^R ($LAST_REGEXP_CODE_RESULT) obsahuje návratový kód provedený uvnitř rozšířeného vzoru (?{ }) (zmíněno dále v této kapitole). Všechny proměnné kromě $* mají dymanicky vymezenou platnost, takže jejich hodnota je platná pouze uvnitř bloku, kde byla nastavena.

$_ = 'a1';
/(\d)/;
print "nalezeno $&, zapamatováno $1\n";
{
   /(\w)/;
   print "nalezeno $&, zapamatováno $1\n";
}
print "nalezeno $&, zapamatováno $1\n";

# vytiskne
nalezeno 1, zapamatováno 1
nalezeno a, zapamatováno a
nalezeno 1, zapamatováno 1

Použijeme-li modifikátor g, proměnné obsahující nalezené a zapamatované podřetězce jsou vždy číslované od 1 i při opětovném použití vzoru. Není tedy třeba si pamatovat, kolik zapamatování bylo provedeno v předchozích zpracováních vzoru.

Zpracování regulárních výrazů a porovnávání s řetězcem

Ještě před tím, než je regulární výraz (vzor) porovnáván s řetězcem, je potřeba převést ho do interní podoby, se kterou je možné dále pracovat. Zpracování regulárního výrazu je do určité fáze podobné zpracování řetězcových termů.

Převedení vzoru do interní formy

Stejně jako u řetězců je prvním krokem identifikace operátoru a nalezení počátečního a koncového oddělovače. Jsou-li pro ohraničení použity nějakké závorky, jsou přeskočeny všechny vnořené závorky. Obráceným lomítkem uvozené ukončovací symboly jsou v této fázi jsou přeskočeny a obrácená lomítka jsou odstraněna.

Potom následuje vkládání hodnot proměnných a ze speciálních symbolů se zpracovávají pouze sekvence \U, \Q apod. Zatímco u řetězcových termů probíhá i vkládání jiných speciláních symbolů (\t apod.), u regulárních výrazů tomu tak není. Všechno ostatní je totiž považováno za speciální symbol regulárních výrazů.

Rovněž chápání některých symbolů se liší u řetězců a u regulárních výrazů - symboly \číslo, \b.

V regulárních výrazech neprobíhá vkládání hodnot proměnných $|, $) a $(.

Rozdílně může být chápána i konstrukce $x[VÝRAZ]

$x = 'a';
$text = 'a123';

$text =~ /$x[15]/;    # chápáno jako prvek pole, nenalezeno nic
$text =~ /$x[1-5]/;   # chápáno jako třída znaků, nalezeno 'a1'
$text =~ /${x[1-5]}/; # chápáno jako prvek pole, nenalezeno nic

V části nahrazení u operátoru s/// je symbol \1 nahrazen proměnnou $1 atd.

Po této části je provedeno dodatečné zpracování vzoru v případě, že je použit modifilátor x. V takovém případě jsou odstraněny všechny komentáře (sekvence (?# ) nebo od znaku #, před kterým není obrácené lomítko, až po konec řádku nebo vzoru). Taktéž jsou odstraněny všechny bílé znaky.

Je důležité si uvědomit, že tato fáze probíhá až po vkládání hodnot proměnných. Proto v případě, že proměnná bude obsahovat symbol #, bude se příslušná transformace týkat i těchto vložených hodnot.

$x = '\w+#\w+';
$text =~ /\d+$x/x;
# to samé jako
$text =~ /\d+\w+#\w+/x;
                ^^^^ komentář, ignorováno

Bude-li vkládaná hodnota obsahovat bílý znak, bude také odstraněn.

$x = 'a b c';
/$x/x;   # chápáno jako /abc/, mezery jsou odstraněny

Řešením je uvést před bílé znaky nebo # obrácené lomítko nebo na vkládané hodnoty použít sekvenci \Q.
Bude-li komentář přímo zapsaný ve vzoru obsahovat jméno proměnné (znak # nebude vložen jako hodnota jiné proměnné) a použijeme-li modifikátor x, všechny metasymboly až do konce řádku nebudou chápány jako speciální symboly, ale jako znaky, které budou ignorovány. Rovněž zde neprobíhá vkládání hodnot proměnných.

/abc#$x^$[(/;
    ^^^^^^^ ignorováno, neprobíhá zde vkládání,
            metasymboly ztrácejí svůj význam

Jsou-li do vzoru vloženy řetězce \t, \n apod. a jsou-li tyto řetězce chápány jako řetězce nepodléhající vkládání, nedojde k jejich nahrazení za tabelátor či nový řádek, ale ve vzoru zůstanou právě tyto řetězce (vkládání probíhá pouze pro proměnné a pro sekvence \U, l apod.).

/\w+	\w+/x;    # chápáno jako /\w+\w+/
/\w+\t\w+/x;      # chápáno jako /\w+\t\w+/
$tab = "\t";
/\w+$tab\w+/x;    # chápáno jako /\w+\w+/
                  # v řetězci $tab je nejprve provedeno vkládání
                  # a takto vložený bílý znak je potom vložen
                  # do vzoru
$tab = '\t';
/\w+$tab\w+/x;    # chápáno jako /\w+\t\w+/
                  # v řetězci $tab není provedeno vkládání, do
                  # vzoru je vložen řetězec '\t', který nepodléhá
                  # vkládání

Po této fázi následuje převedení vzoru do interní formy, podle které je možné provádět prohledávání v řetězci. Neobsahuje-li vzor žádné proměnné, podoba této interní formy se nezmění za celou dobu provádění programu. V opačném případě je nutné předchozí kroky od vkládání hodnot až po zpracování vzoru s modifikátorem x provést pokaždé, když se změnily hodnoty těchto proměnných. Výjimkou je použití modifikátoru o, který vynucuje zpracování celého vzoru pouze jednou.

Jak vypadá tato interní forma můžeme zjistit pomocí operátoru qr//. Ten provádí to, že zadaný řetězec převede do této interní podoby a s ní je možné dále pracovat jako se vzorem. Výhodou je to, že takový vzor už nemusí být kompilován a je možné pomocí něj rovnou prohledávat.

Vyhledávání v řetězci

Vlastní vyhledávání vzoru probíhá podle určitých pravidel. Podle toho, jaká tato pravidla jsou a jak ovlivňují výsledek hledání, je realizován prostředek na zpracování regulárního výrazu (anglicky označován jako Regex engine). Tento prostředek je programově implementován pomocí automatu. Rozlišujeme tři základní typy automatů pro práci s regulárními výrazy -- deterministický konečný automat (DFA), tradiční nedeterministický konečný automat (NFA) a POSIX NFA. Rozdíl mezi nimi je v tom, jakým způsobem provádí kompilaci vzoru a jaká pravidla používají pro nalezení podřetězce v řetězci a obecně také výsledky vyhledávání. Všechny tři typy mají některá společná pravidla, v některých činnostech se však liší

Společným pravidlem pro všechny typy automatů je to, že je nalezen podřetězec, který se nachází nejvíce vlevo od místa, kde začíná vyhladávání. Znamená to, že když v řetězci ababa budeme hledat písmeno a, bude vždy nalezeno to, které je nejvíce vlevo (v tomto případě první písmeno v řetězci).

Dalším společným pravidlem je to, že kvantifikátory se chovají tzv. hladově. Znamená to, že pohltí tak velkou část řetězce, jak je to jenom možné (s tím, že je snahou nalézt celý vzor).

Perl zpracovává regulární výrazy pomocí tradičního nedeterministického konečného automatu (NFA).

Průběh prohledávání

Chceme-li vyhledat v řetězci nejaký podřetězec podle vzoru, postupuje se vždy zleva od prvního znaku řetězce (přesněji od pozice před prvním znakem). Tento znak je porovnáván s prvním symbolem v regulárním výrazu (nezáleží na tom, zda se jedná o konkrétní znak, pozici, třídu znaků či variantu). Je-li provnání úspěšné, vezme se další znak v řetězci a porovná se s následujícím symbolem ve vzoru tak dlouho, až je dosaženo konce vzoru nebo řetězce. Prohledávání je úspěšné v případě, že jsme došli na konec vzoru a v řetězci byly nalezeny všechny uvedené symboly v tom pořadí, v jakém byly uvedeny ve vzoru.

$text = 'regulární výraz';
$text =~ /výraz/;

Backtracking

I když jsme našli v řetězci první znak, který odpovídá vzoru, neznamená to automaticky to, že se na tomto místě celý vzor s řetězcem shoduje. Nebo v případě použití variant nevíme, která bude v řetězci nalezena, stejně tak při použití kvantifikátorů není jasné, kolikrát se kvantifikovaný symbol v řetězci vyskytne, a to i v případě, že víme, že kvantifikátory jsou hladové (tzn. snaží se pohltit co největší část řetězce).

V takových okamžicích je na výběr několik možností, kterým směrem se ubírat. Automat se pokouší nalézt celý vzor od aktuálně zpracovaného okamžiku a nezajímá ho, že jinde by vzor s řětězcem mohl také souhlasit nebo že by snad mohla být nalezen větší podřetězec. U variant se předpokládá, že bude nalezena první, hladové kvantifikátory pohltí maximlní možnou část řetězce.

Co se ale stane, když cesta, která byla zvolená, nebyla tou správnou? V tom případě se vrátí do bezpostředně předcházejícího stavu, kde bylo třeba učinit rozhodnutí, a pokračuje se jinou možností. Tyto stavy jsou uloženy v zásobníku (tzn. provede se návrat do posledního uloženého stavu) a pokud je možné se někam vrátit, selhání vyhledávání zvolenou cestou nezpůsobí selhání celého prohledávání. Pokračuje se do té doby, než je jedna z možností správná, jinak se vrací stále o úroveň zpět. Vyhledávání je neúspěšné teprve tehdy, kdy je dosaženo konce řetězce, ale není nalezen celý vzor i po vyčerpání všech možností.

$text = 'aaa';
$text =~ /a+\w/;   #     aa|a
                         a+|\w  

$text = 'ababc';
$text =~ /abc/;        #     ab|abc
                           |abc
$text = 'abc123';
$text =~ /\d*/;    #     abc123
                        ^\d* 

Kvantifikátory a hladovost

Pokud používáme hladovou formu kvantifikátorů, snaží se automat najít co největší část podřetězce. To s sebou přináší jistou dávku neefektivnosti. Je-li totiž volba polhtit či nepolhtit část řetězce (vyzkoušet danou variantu nebo ne), Perl se vždy rozhodne pro vyzkoušení. Kvantifikátory tak pohlcují části řetězce bez ohledu na to, co po nich ve vzoru následuje. Proto často dochází k backtrackingu a tím i ke zpomalení zpracování regulárních výrazů. Proto rychlost zpracování často souvisí se zápisem vzoru.

Často ukazovaný příklad je na nalezení podřetězce ohraničeného počátečním a koncovým oddělovačem -- nalezení obsahu HTML tagu či obsah řetězce uzavřeného do uvozovek.

$text = 'aaa "xx" bbb';
$text =~ /"(.*)"/;       # do proměnné $1 se uloží 'xx'

$html = '<B>text</B>';
$html =~ /<B>(.*)<\/B>/; # do proměnné $1 se uloží 'text'

$text = 'a "xx" b "yy" c';
$text =~ /"(.*)"/;       
# do proměnné $1 se uloží 'xx" b "yy'

$html = '<B>text</B> jiný text <B>zase jiný text</B>';
$html =~ /<B>(.*)<\/B>/; 
# do proměnné $1 se uloží 'text</B> jiný text <B>zase jiný text'

Existuje několik způsobů, jak tento problém vyřešit. V případě použití jednoznakového ohraničovacího znaku je možné použít negovanou třídu znaků a hladového kvantifikátoru. Za první uvozovkou se potom hledá maximální počet znaků, které nejsou uvozovka. Chceme-li přeskočit i uvozovky, které jsou uvozeny obráceným lomítkem, musíme najít maximum znaků, které nejsou uvozovky nebo nejsou uvozovky, které před sebou mají obrácené lomítko. Druhá varianta lze zapsat jako \\" a ta první jako [^"].

/"((\\"|[^"])*)"/;

Protože varianta [^"] nejspíš nastane častěji, měli bychom ji uvést na prvním místě. Při dostání se na první uvozovku by přišla na řadu druhá varianta, a sekvence \\" by nikdy nebyla nalezena. Proto je třeba do třídy znaků umístit znak obráceného lomítka, aby se při každém nalezení obráceného lomítka testovala varianta, že je ve spojení s uvozovkou. Aby se přeskočily všechny sekvence s obráceným lomítkem (i jiné než \"), místo znaku " umístíme do vzoru znak tečka, který reprezentuje jakýkoliv znak.

/"(([^\\"]|\\.)*)"/;

Nebo můžeme použít rozšířeného vzoru, zvaného lookbehind (rozšířený vzor}).

/"(([^"]|(?<=\\)")*)"/;

U víceznakových oddělovačů negované třídy znaků použít nemůžeme, protože uvnitř třídy znaků reprezentuje množinu všech uvedených znaků a tyto znaky jsou chápány jednotlivě. V tomto případě musíme použít tzv. línou formu kvantifikátorů.

$html = '<B>text</B> jiný text <B>zase jiný text</B>';
$html =~ /<B>(.*?)<\/B>/; # do proměnné $1 se uloží 'text'

Stejného postupu je možné použít i pro jednoznakové ohraničovací symboly.

$text = 'a "xx" b "yy" c';
$text =~ /"(.*?)"/;       # do proměnné $1 se uloží 'xx'

Rozdíl je v tom, že použití negované třídy znaků je rychlejší, protože při použití .*? je nutné se při každé iteraci dívat, co následuje (jestli je tam "). Metasymbol . také nezachytí znak konce řádku (pokud nepoužijeme modifikátor s).

Operátor m//

Operátor m// slouží k k porovnání skalární hodnoty se vzorem. Skalární hodnotou může být jakýkoliv výraz, nemusí se nutně jednat o l-hodnotu. Implicitním prostorem pro porovnání je proměnná $_ ($ARG). Cheme-li provádět vyhledávání vzoru v jiné hodnotě, použijeme operátor =~ (případně !~).

Ohraničení vzoru

Ovlivňuje chování operátoru.

Návratová hodnota

Modifikátory

- i, m, o, s, x, g, cg

Operátor s///

Operátor split

split operátor m//, zdroj, limit
© 2004, František Dařena

Valid XHTML 1.0! Valid CSS!