Regulární výrazy (4.) – pokročilé podskupiny

Zatímco předchozí díly seriálu se zabývaly něcím, co bych označil za základní syntaxi regulárních výrazů, víceméně stejnou ve všech implementacích, ve čtvrtém díle se podíváme na konstrukce, které jsou specifické pro PCRE a nedají se použít skoro nikde jinde. To je sice jejich velká nevýhoda, na druhou stranu jde o konstrukce natolik užitečné, že stojí za to je znát. Konečně totiž dojde i na častou otázku, „jak vyhledat řetězec, který neobsahuje zadaný podřetězec?“

Pokud vás zajímá jen odpověď na předchozí otázku a chcete přeskočit všechny ty nudné kecy okolo, tak skočte rovnou na kapitolu Look-around.

Všechny následující konstrukce vycházejí z podskupin, a to jak syntaxí, tak implementací i funkcemi – jenom jednoduchý jazyk podskupin rozšiřují o nové prvky. Jejich zápis má typicky podobu (?příkazregexp): část (? signalizuje začátek speciálního druhu podvýrazu (protože tato sekvence znaků není normálně v regulárních výrazech přípustná), následuje jeden nebo více symbolů, které identifikují význam podvýrazu. Za nimi píšeme libovolný regulární výraz, na který se má speciální chování použít, a celé to ukončuje pravá kulatá závorka. Zdůrazňuji, že za všech okolností záleží v části příkaz na velikosti písmen, tzn. (?m) má úplně jiný význam než (?M).

Modifikátory

Už v druhém dílůu seriálu jsem se u některých symbolů zmínil o tom, že se jejich význam může měnit na základě použitého režimu (např. že tečka znamená „jakýkoliv znak kromě CR a LF“, ale že existuje speciální režim, který přepne tečku do významu „jakýkoliv znak“). Režimy normálně určujeme pro výraz jako celek – například vyhledávací okno v textovém editoru obvykle dává na výběr, jestli se má hledat se zohledněním velikosti písmen (case-sensitive) nebo bez ohledu na velikost (case-insensitive). To za normálních okolností stačí. Potíž však nastane v případě, že a) pro různé části regulárního výrazu potřebujeme různé režimy, nebo b) vyhledávací okno některé režimy nenabízí vůbec. Tady právě přijdou vhod modifikátory přímo uvnitř regulárního výrazu.

Modifikátory zapisujeme jedním ze dvou základních způsobů:

  • (?mod1-mod2) – Od tohoto místa až do konce regulárního výrazu (nebo do použití opačných modifikátorů) zapnout modifikátor(y) mod1 a vypnout modifikátor(y) mod2.

  • (?mod1-mod2:regexp) – Na regulární výraz regexp zapnout modifikátor(y) mod1 a vypnout modifikátor(y) mod2. Zbytku regulárního výrazu (za zavírací závorkou) se změna modifikátorů nedotkne, použije se ten režim, který byl stanoven pro výraz jako celek.

V obou případech je možné buď mod1 nebo mod2 vynechat.

Povolené modifikátory jsou (vždy jedno písmeno na modifikátor; lze je skládat do řetězců, které pak zapínají nebo vypínají více modifikátorů najednou):

  • i – režim case-insensitive. Pokud je zapnutý, nezáleží na velikosti písmen, v opačném případě na ní záleží. Pokud není pro celý výraz určeno něco jiného, tak výchozím stavem je, že záleží na velikosti písmen. Hledám-li v textu YouTube Downloader slovo tube, může a nemusí se to podařit, podle toho, jak je nastaven vyhledávací výraz jako celek; použitím modifikátoru i ale můžu nějaké chování vynutit: (?i)tube (resp. (?i:tube) vždy uspěje (protože (?i) zapíná režim, kdy nezáleží na velikosti písmen), zatímco (?-i)tube resp. (?-i:tube) nikdy neuspěje (protože (?-i) zapíná režim, kdy záleží na velikosti písmen, a „tube“ pak není totéž co „Tube“).

  • s – režim „dot-all“. Pokud je zapnutý, tečka má význam „libovolný znak“. Pokud je vypnutý, má tečka význam „libovolný znak kromě CR a LF“. Použití je hlavně pro hledání výrazů, které mohou obsahovat celkem cokoliv a mohou být roztaženy přes několik řádků. V YouTube Downloaderu například běžně používám výrazy typu <title>(.*?)</title> („vrať mi všechno, co leží uvnitř tagu <title>„), kde určitě chci dostat výsledek, i kdyby byl rozložen na víc řádků jako v případě:

    <title>
      titulek
    </title>
  • m – režim multiline. Pokud je zapnutý, tak ^ a $ reagují nejen na začátku resp. konci řetězce, ale také na začátku resp. konci řádku. Pokud je vypnutý, tak oba symboly reagují jen na začátku a konci řetězce.

    Mějme například následující text:

    1. Jan 10
    2. Petr 8
    3. Tomáš 7

    Pak regulární výraz ^[0-9]+ („hledám číslo na začátku řetězce“) najde ve standardním režimu (?-m) jen číslo 1, zatímco v režimu multiline (?m) najde 1, 2 a 3 (protože ^ v tomto režimu platí na začátku každého řádku, ne jen na začátku celého řetězce); čísla 10, 8 a 7 nebudou nalezena nikdy, protože nejsou na začátku řádku ani řetězce.

    Tento modifikátor nemá moc smysl v editorech, protože ty až na naprosté výjimky fungují „samy od sebe“ v režimu multiline a dokonce ho obvykle ani neumožňují vypnout. Uplatní se ale při použití regulárních výrazů v programech.

  • x – režim volných mezer. Pokud je zapnutý, budou v regulárním výrazu ignorovány všechny bílé znaky (mezera, tabulátor, CR, LF – viz druhý díl seriálu, symbol \s) a pomocí symbolu # lze vkládat komentáře. Osobně jsem tento režim nikdy nepoužil, ale v případě složitějších regulárních výrazů v programu by se mohl hodit (v editorech to smysl nemá vůbec). Výraz ^https?://(?:[a-z0-9-]+\.)*sme\.sk/.* by se pak pro stejnou funkčnost, ale lepší srozumitelnost, mohl zapsat takhle:

    ^https?://          # URL začíná protokolem http nebo https
    (?:[a-z0-9-]+\.)*   # Následuje libovolné množství subdomén třetí a vyšší úrovně
    sme\.sk/            # Zbytek cesty už může být libovolný, i prázdný

Jak bylo uvedeno, modifikátory lze kombinovat do řetězců, tj. např. (?ix-s) zapne režimy case-insesitive a volných mezer a vypne režim dot-all.

Look-around („hledání okolo“)

Look-around je skupina výrazů, které dovolí hledat v textu dopředu (look-ahead) nebo dozadu (look-behind), jestli obsahuje (positive) nebo neobsahuje (negative) zadaný regulární výraz, aniž by se pohnul kurzor nebo se pohltily nějaké znaky. To normální regulární výrazy nedělají – co najdou, to pohltí, takže při případném dalším hledání už se to nenajde. Problém však nastane v okamžiku, kdy je třeba v jednom řetězci vyhledat několik slov, která jsou oddělená nějakým netriviálním výrazem. Tam právě přijdou ke cti look-aroundy.

Aby to bylo srozumitelnější, příklad: máme řetězec 100,250,300,450,500,600, který obsahuje posloupnost čísel oddělených čárkou, a chceme z něj postupně dostat jednotlivá čísla. Na to stačí jednoduchý výraz ([0-9]+). Potíž je v tom, že zlomyslný uživatel do toho řetězce může napsat něco, co sice číslice obsahuje, ale číslem není (např. 100,250,l33t, a v tu chvíli jsme s předchozím výrazem nahraní, protože ten najde i to, co nemá (druhá varianta je, že i sám oddělovací symbol bude obsahovat číslice – třeba to nebude čárka, ale %38, což je jeden z běžných způsobů kódování symbolu &). Takže náš výraz upravíme na ,([0-9]+),, který ovšem vyloučí nejen číslice uvnitř slov, ale také číslice na začátku a konci řetězce (to by se ještě dalo vyloučit vhodným použitím symbolů |, ^ a $ – nebo také doplněním čárky na začátek i konec řetězce; to tady nepotřebujeme řešit, v příkladu jde o ty vnitřní čárky) a také každou druhou číslici, protože vždy „sežere“ i tu koncovou čárku, kterou chceme mít současně jako počáteční čárku výrazu následujícího (postupně by se našly zvýrazněné části: 100,250,300,450,500,600).

Look-aheady slouží právě k tomu, abychom mohli „najít čárku a současně ji mít k použití příště“:

  • (?=regexp) – pozitivní look-ahead: dívá se, jestli za aktuální pozicí kurzoru v textu následuje regulární výraz regexp, ale přitom tento výraz nezpracuje. Hodí se právě na problém popsaný v předchozím odstavci; výraz ,([0-9]+)(?=,) právě vyhledá čísla, která jsou z obou stran bezprostředně obklopena čárkami, ale druhou čárku nechá být pro příští vyhledávání. Postupným použitím tohoto výrazu tak najdeme ,250, ,300, ,450, ,500, což už skoro odpovídá zadání – jak jsem psal výše, první a poslední číslici teď zrovna neřešíme; ale pro úplnost: buď na začátek i konec prohledávaného řetězce dáme čárku, nebo použijeme trochu složitější výraz (,|^)([0-9]+)(?=,|$) (tj. před číslem nemusí být jen čárka, ale také konec řetězce, a za koncem může místo čárky být konec řetězce).

  • (?!regexp) – negativní look-ahead: dívá se, jestli za aktuální pozicí kurzoru v textu nenásleduje regulární výraz regexp. Hodí se tehdy, když chci nějaký výraz ze zpracování vyloučit. Pokud v předchozím příkladu s čísly například chci všechna čísla kromě 450, doplním do výrazu ,([0-9]+)(?=,) vhodný negativní look-ahead a je hotovo: ,(?!450,)([0-9]+)(?=,) („na začátku musí být čárka, pak nesmí být 450, a musí být řetězec číslic, následovaný nezpracovanou čárkou“).

    Drobnou modifikací lze dosáhnout vyhledání libovolného výrazu, který nesmí obsahovat zadaný podvýraz. Dejme tomu, že mám v textu najít všechno od začátku až do klíčového slova STOP, které se ale nemá stát součástí nalezeného textu. Vhodný výraz bude (((?!STOP).)*) („Pokud na místě kurzoru není STOP, tak do nalezeného výrazu přidej jeden znak a posuň se na další. To opakuj libovolně mnohokrát, dokud to jde (dokud neskončí text nebo nenarazíš na STOP).“).

  • (?<=regexp) – pozitivní look-behind; funguje podobně jako pozitivní look-ahead, ale nedívá se dopředu, ale naopak zpátky na už zpracované znaky. Příklad s čísly oddělenými čárkou by tak klidně šel z look-aheadového ,([0-9]+)(?=,) přepsat na look-behindový (?<=,)([0-9]+), (tj. „první čárka už byla zpracována při předchozím hledání, ale look-behindem se podívej zpátky, jestli tam skutečně byla“).

  • (?<!regexp) – negativní look-behind také hledá dozadu, ale ujišťuje se, že bezprostředně před aktuální pozicí kurzoru výraz regexp není. Vyloučení čísla 450 ze zpracování pak zařídí ,([0-9]+)(?<!,450)(?=,).

Komentáře

U modifikátoru x jsem se zmiňoval o možnostech jeho využití pro komentování regulárních výrazů. Něco podobného jde udělat i bez modifikátorů – výraz ve tvaru (?#cokoliv) bude celý ignorován, včetně znaků, které by normálně měly speciální význam (tj. je úplně jedno, jestli zavírací závorku předchází třeba zpětné lomitko – komentář končí u první zavírací závorky a hotovo).

Podmíněné podvýrazy

Tady už se dostáváme do oblasti černé magie. Pomocí podmíněných podvýrazů jde zpracování větvit podle určitých podmínek. Možnosti jsou dvě:

  • (?(?=regexp1)regexp2|regexp3 je regulární implementace podmínky if pozitivní_lookahead(regexp1) then regexp2 else regexp3: Pokud je nalezen výraz regexp1, který je hledán jako pozitivní look-ahead (viz výše), použije se pro další hledání regexp2, jinak se použije regexp3. Obdobně lze použít zápisy pro ostatní look-aroundy, tj. (?(?!regexp1)regexp2|regexp3 pro negativní look-ahead, (?(?<=regexp1)regexp2|regexp3 pro pozitivní look-behind a (?(?<!regexp1)regexp2|regexp3 pro negativní look-behind.

    V reálné praxi jsem to ještě nepoužil, ale daly by se s tím vyhledávat atributy tagů v HTML – narozdíl od XML, kde hodnota atributu musí být uzavřena do uvozovek nebo apostrofů (a lze tedy využít výraz typu \snázev=(["'])(.*?)\1 („Hledej název následovaný rovnítkem a potom bude buď uvozovka nebo apostrof, za kterou najdeš cokoliv až do nejbližšího dalšího výskytu použitého symbolu“), v HTML nemusí být atribut uzavřen ničím (a v tom případě končí nejbližším prázdným znakem nebo koncem tagu). Regulární výraz pak je \snázev=(?(?=["'])(['"])(.*?)\1|([^\s>]*)), kde:

    • Na začátku je hledaný název atributu následovaný rovnítkem.

    • Podmínka v podmíněném výrazu (?=["']) je ve tvaru pozitivního look-aheadu a říká, „podívej se, jestli následuje uvozovka nebo apostrof“.

    • Pokud byla podmínka splněna, tj. za rovnítkem je uvozovka nebo apostrof, použije se větev (['"])(.*?)\1, která je totožná jako v případě XML výše.

    • Pokud podmínka splněna nebyla, použije se větev ([^\s>]*), tj. „hledej cokoliv až do nejbližšího bílého znaku nebo konce tagu“.

  • (?(číslo)regexp2|regexp3) je další podmínkou typu IF-ELSE, ale tentokrát není rozhodovací podmínkou zadaný look-around jako výše, ale nalezení n-té (podle hodnoty číslo) podskupiny – pokud byla nalezena, použije se výraz regexp2, jinak regexp3. Hledání atributů z předchozího příkladu by se dalo přepsat na \snázev=(["'])?(?(1).*?\1|[^\s>]*): hledání uvozovky nebo apostrofu se nyní nachází mimo podmíněný výraz a připouští se nenalezení (proto je za (["']) ten otazník). Podmínka se ptá, jestli byla nalezena první podskupina (což je právě ten apostrof nebo uvozovky), a podle odpovědi pak použije XML nebo HTML variantu, stejnou jako výše.

 

To by pro dnešek stačilo, pokračování příště – čeká nás ještě několik zajímavých variant podvýrazů (které masivně využívám v YouTube Downloaderu, narozdíl od těch dnešních, ke kterým se dostanu spíš výjimečně) a také podpora pro Unicode, díky které lze snadno implementovat požadavek „rozeber mi text na jednotlivá slova“ i jazycích, které oplývají písmenky více než anglická abeceda (třeba pro takový obskurní jazyk jako je čeština se to velmi hodí :-)).

Podobné příspěvky:

3 Responses to “Regulární výrazy (4.) – pokročilé podskupiny”

  1. avatar petr napsal:

    Ano. Jinak (musel bych se potom podívat), ale taky „hnusně“ jsem to vždycky řešil (naštěstí to nebylo potřeba tak často) 🙂

  2. avatar pepak napsal:

    To nedokážu říct, protože tyhle skriptovací jazyky zrovna moc neovládám. Záleží na tom, jak jsou implementované.

    Funkce „všechno kromě stringu XY“ se dá udělat i bez look-aroundů, ale je to neskutečný humus a rozhodně bych to nikomu nedoporučil. „Všechno, jen ne pepak“, by se zapsalo takhle (hnusně, ale funkčně snad všude): [^p]|p[^e]|pe[^p]|pep[^a]|pepa[^k]

  3. avatar petr napsal:

    Díky 🙂 Původní dotaz u 2. dílu směřoval spíš na to, jestli je nějaká možnost i u „běžnýho“ použití regulárních výrazů (VBScript – makra…, JavaScript…), protože se mi zdá, že je to častej požadavek, a přitom o jeho řešení jsem neslyšel a nikde se nedočetl, ale PCRE je super věc při programování v Delphi, takže se i tyhle konkrétní příklady týhle specifický knihovny hodí 🙂

Leave a Reply

Themocracy iconWordPress Themes

css.php