Práce s regulárními výrazy
Kdysi jsem podstoupil diskusi s příznivci nejmenovaného graficky orientovaného OS (pro upřesnění, se špatnou příkazovou řádkou) na téma vizualizace a zkoušení regexpů. Tito příznivci vychvalovali jakýsi na GUI postavený program, který barevně zobrazoval části v řetězci, které odpovídaly zadanému regulárnímu výrazu. Naproti tomu můj názor na tento problém byl, že člověk zručný v unixovém programování si takovou věc napíše do dvou minut sám. Bez ohledu na to, jak diskuse dopadla, a jelikož dnes budeme experimentovat s regulárními výrazy, bude vhodné mít takový program po ruce.
Prvním problémem je získání regulárního výrazu (dále jen regexpu) od uživatele a následná kontrola tohoto regexpu. V nejjednodušším případě můžeme využít interpolaci proměnných uvnitř operátorů pracujících s regexpy. Takto by vypadala jakási miniaturní obdoba známého programu grep
(nazveme jej třeba Mandarinka):
my $re = shift; while (<>) { print if m/$re/; }
Přičemž v případě chybného regexpu dostaneme vynadáno na řádku s operátorem m//
. Efektivnějším a prozíravějším řešením je použití operátoru qr//
. Tento operátor nahlíží na svůj vnitřek jako na regexp a vrátí referenci na objekt typu regexp. S modifikátorem /o
můžeme nechat regexp okamžitě přeložit a tak ušetřit procesorový čas při dalších použitích tohoto regexpu. Případné chyby při kompilaci můžeme odchytit pomocí bloku eval
.
my $input = shift; my $regexp = eval { qr/$input/o }; die "Pattern error: $@\n" if $@;
Jak bylo řečeno, proměnná regexp
se chová jako reference na regexp (lze ověřit pomocí print ref $regexp
). Lze s ní tedy zacházet jako s každou jinou referencí, zejména předávat do funkcí a uchovávat v datových strukturách (což není totéž jako uchovávání řetězce obsahujícího regexp). Jedinou výjimkou je, že dereferencování provádí Perl sám, tudíž pro použití $regexp
píšeme pouze $regexp
bez dalších znaků.
while (<>) { print if m/$regexp/; }
Modernější odrůdy programu grep
umí barevně zvýraznit nalezený řetězec v rámci vypsaného řádku. V Perlu můžeme za tímto účelem použít proměnné $`
(obrácený apostrof), $&
a $'
(obyčejný apostrof). Proměnná $&
obsahuje nalezený řetězec, zbylé dvě část před, respektive za ním. Pomůcka pro zapamatování je psaní uvozovek v anglickém jazyce.
use Term::ANSIColor qw/:constants/; while (my $line = <>) { print $`, BOLD, RED, $&, RESET, $' if ($line =~ m/$regexp/); }
Pokud umíme takto pěkně zobrazit nalezený řetězec, nabízí se otázka, zda takto lze zobrazit i zachycené podřetězce v závorkách. Pro normální použití jsou tyto podřetězce v proměnných $1
, $2
, atd. Pro náš výpis však narážíme na dva problémy. Jednak neumíme říci (alespoň bez těžkého vrtání se v Perlovské tabulce symbolů), kolik takových proměnných bylo zachyceno. Druhý problém, a ten je horší, je, že máme k dispozici pouze nalezené řetězce a nikoliv obsah původního řetězce před a za nimi.
Oba problémy lze vyřešit použitím polí @-
a @+
. Tato pole obsahují pro každý nalezený podřetězec
pozici jeho začátku a konce (přesněji, znaku za koncem)
v původním řetězci. To, co bylo nalezeno v první závorce, je
v původním řetězci na pozicích $-[1]
až $-[1] -
1
, podobně pro druhou a další závorku. Pro naše pohodlí jsou
$-[0]
a $+[0]
pozice pro řetězec
$&
.
while (my $line = <>) { chomp $line; if ($line =~ m/$regexp/) { for my $i (0 .. $#+) { my ($start, $stop) = ($-[$i], $+[$i]); print q/$/ , $i ? $i : q/&/ , q/: / , substr($line, 0, $start) , BOLD, RED , substr($line, $start, $stop - $start) , RESET , substr($line, $stop) , qq/\n/; } } }
Pokud máte rádi use English
, lze samozřejmě všechny zmiňované proměnné najít pod anglickými názvy (dohledání necháme na čtenáři).
Důvodem, proč pozice v poli @+
ukazují za a nikoliv na poslední znak, je, že některé nalezené podřetězce mohou mít nulovou délku. Například $1
v regexpu (^|a)hoj
bude mít délku jednoho znaku pro řetězec ahoj!
a nulovou délku pro hoj!
. Ve vizualizačním programu můžeme tuto situaci ošetřit například tak, že na pozici shody s nulovou délkou zobrazíme nějaký oddělovací znak a navíc jinou barvou.
… while / if / for … print q/$/ , $i ? $i : q/&/ , q/: / , substr($line, 0, $start) , BOLD, , ($stop - $start > 0) ? (RED, substr($line, $start , $stop - $start)) : (BLUE, q/|/) , RESET , substr($line, $stop) , qq/\n/;
Tímto máme program pro experimentování s regulárními výrazy hotový. Pokud se chceme na vlastní oči přesvědčit, co se v nějaké části regexpu (ne)zachytává, stačí příslušný kus ozávorkovat. Doplňkem k tomu je použití modulu YAPE::Regex::Explain
, který převede regexp do řetězce s vysvětlivkami.
perl -e 'use YAPE::Regex::Explain; print YAPE::Regex::Explain->new(qr/(?i:^|a)hoj(\s+)(?#\p?$)(?!de(?:ti|cka))(?>.*?\1!+)/)->explain;' # výstup The regular expression: (?-imsx:(?i:^|a)hoj(\s+)(?#\p?$)(?!de(?:ti|cka))(?>.*?\1!+)) matches as follows: NODE EXPLANATION ---------------------------------------------------------------------- (?-imsx: group, but do not capture (case-sensitive) (with ^ and $ matching normally) (with . not matching \n) (matching whitespace and # normally): ---------------------------------------------------------------------- (?i: group, but do not capture (case- insensitive) (with ^ and $ matching normally) (with . not matching \n) (matching whitespace and # normally): ---------------------------------------------------------------------- ^ the beginning of the string ---------------------------------------------------------------------- | OR ---------------------------------------------------------------------- a 'a' ---------------------------------------------------------------------- ) end of grouping ---------------------------------------------------------------------- hoj 'hoj' ---------------------------------------------------------------------- ( group and capture to \1: ---------------------------------------------------------------------- \s+ whitespace (\n, \r, \t, \f, and " ") (1 or more times (matching the most amount possible)) ---------------------------------------------------------------------- ) end of \1 ---------------------------------------------------------------------- (?! look ahead to see if there is not: ---------------------------------------------------------------------- de 'de' ---------------------------------------------------------------------- (?: group, but do not capture: ---------------------------------------------------------------------- ti 'ti' ---------------------------------------------------------------------- | OR ---------------------------------------------------------------------- cka 'cka' ---------------------------------------------------------------------- ) end of grouping ---------------------------------------------------------------------- ) end of look-ahead ---------------------------------------------------------------------- (?> match (and do not backtrack afterwards): ---------------------------------------------------------------------- .*? any character except \n (0 or more times (matching the least amount possible)) ---------------------------------------------------------------------- \1 what was matched by capture \1 ---------------------------------------------------------------------- !+ '!' (1 or more times (matching the most amount possible)) ---------------------------------------------------------------------- ) end of look-ahead ---------------------------------------------------------------------- ) end of grouping ----------------------------------------------------------------------
Modifikátory regexpových operátorů
Zřejmě nejpopulárnějším užívaným modifikátorem je /g
(global match – nezastavuj se po první shodě), a to v kombinaci s operátorem s///
. V kombinaci s m//
můžeme zachytit více výskytů hledaného řetězce.
# primitivní překladač cest win-to-unix $cesta =~ s{\\}{/}g; # vypište z článku … @ditini = m/\b\w*[dtn]i\w*\b/g;
Modifikátor /e
umožní v operátoru s///
použít na pravé straně perlovský výraz namísto nahrazujícího výrazu. Toto je velmi užitečná funkce, zvláště když si uvědomíme, že uvnitř výrazu může být normální kód, v kterém můžeme použít opět regexpové operátory (a cokoliv dalšího). Kupříkladu uvažme následující problém. Na vstupu máme text, v kterém se může vyskytovat prostrkávané slovo ve formátu text text _S_L_O_V_O_ text
a chceme v něm toto slovo nahradit normálním řetězcem SLOVO
. V levé části operátoru s///
budeme hledat prostrkávaný text, v pravé části ho pomocí dalšího operátoru s///
jednoduše zbavíme prostrkávacích znaků.
perl -pe 's/\b_([A-Z]_)+\b/do { my $a = $&; $a =~ s,_,,g; $a; }/ge'
Modifikátor /o
jsme již zmínili. Jeho použití přeloží regexp okamžitě, čímž se výrazně urychlí jeho následné používání. Nevýhodou je, že takto přeložený regexp pak nebude interpolovat proměnné a další dynamický obsah (respektive stane se tak pouze jednou, a to při prvotním překladu).
Dále je tu svatá čtveřice /imsx
. Při použití složitějších regexpů (někteří doporučují dokonce vždy) je vhodné regexp strukturovaně formátovat, podobně jako bychom to učinili v případě normálního zdrojového textu programu. K tomu slouží modifikátor /x
. Při jeho použití bude Perl ignorovat prázdné znaky a vše za znakem #
do konce řádku (tj. komentáře). Chceme-li v regexpu uvést takový znak, stále jej můžeme uvodit pomocí zpětného lomítka, nebo použít skupinu pro prázdné znaky, např. \s
.
m/'(?:\\[\\']|[^\\'])*'/; # čitelnější zápis m/ ' # úvodní apostrof (?: \\[\\'] # escapovaný apostrof nebo zp. lomítko | # NEBO [^\\'] # neescapovatelný znak )* # to celé libovolně-krát ' # ukončující apostrof /x;
Při takovémto komentování je potřeba dávat pozor, abychom do komentáře nenapsali ukončující znak operátoru, jako například takto:
m/ ' # úvodní apostrof (?: \\[\\'] # escapovaný apostrof / zp. lomítko …
Perl totiž v rozporu s očekáváním nejprve najde hranice operátoru m//
a až potom se zabývá tím, co je uvnitř. V tomto případě bychom dostali přes čumák za neuzavřenou závorku uvnitř regexpu. Pokud by náhodou byl regexp v pořádku, Perlu bude zřejmě připadat divné řetězcové spojení neexistujících funkcí zp()
. lomítko()
, a tak dále. Do jisté míry lze tomuto problému předejít pomocí používání závorek na ohraničení regexpu. Perl ctí vnoření závorek, takže následující zápis je v pořádku.
m{ \{ # začátek bloku: { [0-9]+ # první posloupnost cifer (?:, # další posloupnosti, oddělené čárkami [0-9]+ )* \} # konec bloku: } }x;
Nicméně pokud bychom porušili symetrii závorek, nastane stejný problém. V tom případě nezbývá nic jiného než escapovat.
Modifikátory /m
a /s
ovlivňují chování regexpu vůči více řádkům. Pomocí /m
dovolíme symbolům
^
a $
nalézt začátek a konec řádku
(řádky jsou rozděleny pomocí \n
) namísto začátku
a konce celého řetězce. Začátek a konec řetězce pak lze stále
najít pomocí \A
a \z
(malé zet,
\Z
je něco mírně jiného). Modifikátor /s
dovolí symbolu .
nalézt také znak \n
, což
normálně nemůže. Obvyklé použití je dohromady jako /ms
,
čímž pracujeme s regexpem jako s víceřádkovým a navíc
můžeme pomocí tečky „sežrat“ i konce řádků.
# umaže rozloučení a vše pod ním $dopis =~ s/^S pozdravem.*//ms;
Konečně, modifikátor /i
slouží pro case-insensitive hledání, tj. bez rozdílu mezi malými a velkými písmeny.
Závorky bez ukládání
Pokud se divíte, co znamená sekvence ?:
na začátku závorek, vězte, že je to neukládací závorka. Z hlediska seskupování funguje úplně stejně jako normální závorka, ale neukládá podřetězec. Její zpracování je díky tomu poněkud rychlejší než u obyčejné závorky. Navíc zbytečně neplníme proměnné řetězci, které nás nezajímají (například pokud výsledek regexpového operátoru m//
přirazujeme do pole).
Mezi znaky ?
a :
lze vložit modifikátory ze svaté čtveřice imsx
. Můžeme tak například v rámci jednoho regexpu hledat s ohledem i bez ohledu na velikost písmen ( (?i:P)erl
namísto [pP]erl
). Pokud chceme uvnitř závorky danou vlastnost zrušit, můžeme to udělat pomocí znaku minus. Odtud pochází i -imsx
na začátku regexpu z příkladu na YAPE::Regex::Explain
.
Look-arounds
Jak již bylo řečeno, některé symboly nalézají řetězce nulové délky. Takovýmto symbolům se obecně říká assertions (předpoklady). Například ^
standardně nachází hranici mezi začátkem řetězce a jeho prvním znakem. Symbol \b
nachází hranici mezi slovem a ne-slovem. V Perlu je možné i z nenulové množiny znaků vyrobit shodu o délce nula znaků. Jedná se o tzv. look-arounds. Existují čtyři druhy look-around symbolů:
- Pozitivní dopředný look-around
(?=)
- Negativní dopředný look-around
(?!)
- Pozitivní zpětný look-around
(?<=)
- Negativní zpětný look-around
(?<!)
Pomocí těchto předpokladů můžeme učinit předpoklad o textu před nebo za aktuální pozicí. Přitom ale nesežereme tento text z řetězce, look-around má nulovou délku. Předpokládejme, že chceme nalézt jméno Douglas
nebo James
, ale pouze pokud je za ním jméno Adams
. Klasické řešení by bylo:
/(Douglas|James)\sAdams/
Přičemž nalezený řetězec by byl buď prázdný (nic nenalezeno), nebo celé jméno včetně příjmení. Křestní jméno bychom pak extrahovali z proměnné $1
. S použitím dopředného look-around se tomuto můžeme vyhnout:
/(?:Douglas|James)(?=\sAdams)/
V tomto případě bude nalezeným řetězcem pouze křestní jméno, ale jen v případě, že za ním následuje správné příjmení. Pokud bychom chtěli naopak příjmení, můžeme použít zpětný look-around.
/(?<=James\s)(?:Adams|Bond)/
Poznamenejme, že zpětný look-around s proměnnou délkou nelze použít (?<=Douglas|James)
, neboť toto zatím Perl neumí. Mezi dopředným a zpětným look-around tedy existuje jistá asymetrie.
Abychom úplně pochopili, co se děje v případě look-around, rozebereme si ještě několik případů. U všech použijeme jako vstup řetězec Douglas Adams
.
/(?=.*\sAdams)Douglas/ # najde shodu /(?=\sAdams)Douglas/ # nenajde shodu
V prvním případě požadujeme, aby na začátku nalezeného výrazu bylo slovo Douglas
, ale jenom pokud za tímto začátkem nalezeného výrazu následoval libovolný počet libovolných znaků (vyjma \n
), mezera a slovo Adams
. Toto je v případě našeho vstupu splněno.
Ve druhém případě požadujeme, aby na začátku nalezeného výrazu bylo slovo Douglas
, ale jenom pokud za tímto začátkem nalezeného výrazu je mezera a slovo Adams
. To znamená, že začátek nalezeného výrazu musí být před mezerou ve vstupním řetězci:
Douglas| Adams
Za tímto začátkem však není splněno, že následuje slovo Douglas
, shoda tedy není nalezena. Look-around má nulovou délku, a proto druhým regexpem neprojde ani vstup Douglas
AdamsDouglas
. Začátek nalezeného výrazu bude ukotven před mezerou před slovem Adams
a look-around s ním díky nulové délce nepohne.
Prakticky to znamená, že pokud chceme za znaky, které nalezl dopředný pozitivní look-around, nalézt ještě něco dalšího, musíme nejprve tyto znaky extra sežrat. Pro negativní look-around toto samozřejmě neplatí, neboť negativní look-around hledá řetězec, který ve vstupu není.
/James\s(?!Bond)(\w+)/ # najde shodu pro 'James Adams' # nebo 'James Brown' # ale nikoliv pro 'James Bond'
Závěr
V dnešním průvodci regulárními výrazy jsme si ukázali některé Perlovské speciality pro práci s nimi. Nebyl by to ale Perl, kdyby již neobsahoval na CPANu (moduly pod Regexp::Common
) spousty předdefinovaných a parametrizovatelných regexpů na všemožné použití. Takže ve svém kódu se soustřeďte především na vynalézání regexpů specifických pro váš problém a nikoliv na vynalézání kola (zejména pokusy typu regexp na e-mailové adresy končí velkým překvapením pro naivní a zděšením pro toho, kdo se odváží otevřít příslušné RFC).
Pro další experimentování s regexpy je možno použít (nebo rozšířit) již zmíněný program Mandarinka.