Thursday, February 19, 2015

A klasszikus reguláris operátorokon túl 2.

Többször utaltam már rá, hogy (pl.) a ? operátor mohó avagy greedy: ez annyit tesz, hogy egy R? alakú subregexet először is illeszteni próbálja az R-t, ha illeszkedik, úgy halad tovább, no ezután két eset lehetséges: vagy sikerül az illesztés a regex végéig, ez jó, vagy nem, ekkor pedig (ha az R-t semmiképp nem tudja illeszteni úgy, hogy azt befejezze) elengedi az R-t, és üres stringet illeszt. Ha így sikerül illeszteni a regex végéig, akkor OK, ez lesz az illesztés végeredménye, ha így sem, akkor persze backtrack.
Így például a y(hello|dolly|hell|doll)?d regexnek a dollydollydolly stringen hívott find() metódus eredménye sikeres illeszkedés, mégpedig a félkövérrel kiemelt részen, az opcionális találat a "dolly" szót adja vissza. (Maga a regex egy árnyalattal gyorsabban illeszthető lenne, ha y(hello?|dolly?)y alakba kerülne, de így se rossz.)
Ez a viselkedés megváltoztatható: egyrészt a ? lehet lazy is, ennek jele ??: ekkor pedig a subregex először próbálja illeszteni az üres stringet, és csak ha így nem vezet eredményre az illesztés, akkor próbál ténylegesen betenni valami nemüres stringet, ami illeszkedik R-re. Vagyis pl. az y(hello|dolly|hell|doll)??d regex szintén illeszkedik a dollydollydolly stringre, a jelölt részen.
Azt azért vegyük észre, hogy a greedy vs lazy illesztés időben nem okoz majd különbséget, ha végül a text nem illeszkedik a regexre, mert mindkét esetben kipróbálja az összes lehetőséget, csak a sorrend a különbség (és persze ha többféle illesztési találat lehetséges, akkor mást fog visszaadni az egyik és mást a másik).
A harmadik, a possessive módja a ? operátornak, aminek jele a ?+, a következőt jelenti: először is megpróbál keresni egy illesztést. Ha sikerül, akkor ehhez ragaszkodik és ha a regex további része nem illeszkedik a string maradék részére, és visszajön backtrackkel, akkor se nem próbálja másképp illeszteni a kérdéses subregexet, se nem próbálja kihagyni, hanem dobja tovább a backtracket. Ha viszont a subregex nem illeszkedik ezen a ponton, akkor az üres stringet illeszti és azzal megy tovább. Egy példán: a ^(hello|dolly|hell|doll)?+y regex nem illeszkedik a dolly stringre: a motor balról jobbra próbálgatva az alternatívákat először észreveszi, hogy a "dolly" alternatíva illeszkedik, ezt illeszti a subregexre, majd haladna tovább, ám a string elfogyott, a regex záró "y" karakterére nem fog passzolni semmi. Ha simán egy ? operátorunk lenne, most backtrackkel próbálná tovább a lehetőségeket és a doll illesztése már sikerre vezetne, de mivel most ez egy possessive kérdőjel, így nem próbálkozik tovább, ragaszkodik a dollyhoz mint találathoz és kidobja az egész illesztést az ablakon. Egyébként a ^ metachar nélkül a (hello|dolly|hell|doll)?+y regexet a find metódus megtalálja a dolly stringben: az utolsó y karakteren, ott mivel a csoport belseje nem illeszkedik egyáltalán, üres stringet illeszt rá, és melléteszi az y-t. A possessive tehát könnyen lehet, hogy kevesebb stringre illeszkedik, mint a greedy és lazy párjai, nem-találatnál viszont gyorsabb lehet, mint a másik kettő (hiszen nem fog mindent végigiterálni belül). Technikailag egyébként az R?+ ugyanaz, mint az (?>R?), tehát egy greedy belerakva egy atomic groupba. Egyes nyelvek lehet, hogy nem támogatnak possessive-et, de atomicot igen, ott így csinálunk possessive-et, ha nagyon akarunk.

A greedy / lazy / possessive variánsai persze nem csak a ? operátornak, de a *+ és {m,n} operátornak is megvannak, a lazyt mindig egy plusz kérdőjellel, a possessive-et pedig mindig egy plusz plusszal kapjuk, pl. a *? a lazy csillag (ami a lehető legkevesebb iterációból próbálja megúszni az illesztést). Csak példaképp, egy xml tag belsejét (a <..> közti részt, amiben nincs másik >) a <(.*?)> regex első csoportjában megkapjuk, pl. a hello<b>world</b> stringben a lazy star a kiemelt b-re illeszkedik. A greedy star pedig a b>world</b részre illeszkedne, hiszen benyelne a lehető legtöbb mindent, ami még nem rontja el az illesztést. A possessive star pedig
egyáltalán nem illeszkedik
erre az inputra. Miért? A <.*+> regexet ha a hello<b>world</b> stringre illesztjük (find()), akkor az első < jelen tudja illeszteni az első karaktert. Most jön a .*+, tetszőleges karakter possessive iterálása, ami lényegében egy greedy iteráció, amit nem lehet backtrackelni. Vagyis első próbálkozásképp ez a subregex feldolgozza a b>world</b> (tehát az egész hátralevő) substringet, és rájön, hogy nincs utána >. csak ezután nem backtrackel és engedi el az utolsó karaktert, mint a greedy, hanem visszalép, hogy nincs illesztés. Ekkor a regex első < karakterét próbálja máshova, később illeszteni a motort, illik is a </b>-re, de a possessive star azt is elrontja és nem lesz egyáltalán illeszkedés. (A .*+ egy elég értelmetlen konstrukció, hiszen mindig ezt csinálja: elmegy az input végéig és ha a regex maradék része illik az ottani üres stringre, akkor OK, egyébként fail)
Azt azért ne gondoljuk, hogy pl. a possessive star értelmetlen lenne: a tagbelső bányászást konkrétan én a <([^>]*+)> regex használatával csinálnám: iterálom a nem-> jelet, majd ha > jön, akkor visszaadom a belsőt. Ha viszont itt > helyett vége a stringnek, a greedy star még egyesével visszaadogatná a karaktereket, amiket megszerzett, és mindnél rájönne, hogy nem > jön utána, teljesen fölöslegesen; a possessive star nem backtrackel, hanem szól, hogy itt nem lesz illesztés.

Ennyi kitérő után nézzünk egy eggyel összetettebb taskot: van egy input stringünk, amiben pöttyöket kell betegyünk az előforduló számokba hármasával tizedesjegyenként. Tehát például a
sanyi1234 12345678910 másiksanyi12 142857142
stringből a
sanyi1234 12.345.678.910 másiksanyi12 142.857.142
stringet kéne kapjuk: a két szabadonálló számba kerüljön bele két-három pötty.

Persze ezt is meg lehet csinálni célnyelven, de ha az ember magabiztosan kezeli a regexeket, ezt még talán azzal célszerű is megcsinálni. (Ellentétben pl. a következő post regexeivel, amiket soha, de soha nem ajánlok senkinek :D )
Van pl Javában a Matcher osztálynak egy replaceAll metódusa, ez jó is lesz a célra: ahol illeszkedik a regex, azokat a substringeket kicseréli a paraméterére. Továbbmenve, a captured group-ok eredményét is vissza lehet helyettesíteni, $n jelöli az n. csoport értékét.
Első próbálkozás lehet pl. egy ilyen, amikor is a (\d)(\d{3}) regex ha matchel, cseréljük $1. \$2-re. (Ez legalább pontot tesz be valahova :D ) amint látszik, ideone.com-on futtattam, eredmény:
sanyi1.234 1.2345.678910 másiksanyi12 1.4285.7142
oké, mi is történt, először is megtalálta az egyest, ami után van három számjegy, betett egy pöttyöt, mant tovább. Megtalálta a második egyest, ami után van három számjegy, tett egy pöttyöt. De mivel ekkor már feldolgozta a 4-est, az 5-ösnél folytatta a következő próbálkozást (ami egy számjegy, utána 678, kiírta az 5.678-at), majd mivel a 9-es volt a következő feldolgozatlan karakter, azt nézte (azután nincs három másik, így legközelebb a másik hossú számba tett be pöttyöket.
Ha véggiggondoljuk, ez a legalább négy hosszú számokba tesz be pöttyöket, négy jegyenként, de balra igazítva (az első jegy után jön az első pötty), nekünk meg hármasával kéne jobbra igazítva.

A jobbra igazítás az keményebb diónak látszik. Illeszteni még csak-csak lehet egy számra így: \b(\d+)((?:\d{3})+)\b, vagyis szóhatár, számjegyek, utána hárommal osztható darab számjegy, de ha ezek közé teszünk be pontot, mint az előbb, kód, akkor az eredmény
sanyi1234 12345678.910 másiksanyi12 142857.142
lesz, ami ugyan csak a számokban cserélt dolgokat, és be is tett jobbra igazított pöttyöt, de csak számonként egyet. Miért? Azért, mert a regex az egész számra illeszkedik, a \d+ rész első próbálkozásban lenyeli az egész számot, azután nincs hárommal osztható darab számjegy, szépen elenged egyesével három jegyet, ekkor van egy jó vágása, első csoportba bekerül az 12345678, másodikba a 910, a replaceAll közéjük teszi a pöttyöt, minden szép, csak a következő menetben már az 12345678-ra nem próbál tovább illeszteni, mert azt már feldolgozta és a másiksanyi12-nél folytatja az illesztést.

Valahogy el kellene érni, hogy csak lássuk a szöveget, hogy illeszkedik-e egy mintára, de ne dolgozza fel a motor.

Erre jó a lookahead és lookbehind, közös néven lookaround.
A lookahead csoport szintaxisa (?=...), belül egy teljesen normális regexet írhatunk, ekkor az történik, hogy megnézi a motor, hogy az aktuális pozíción kezdve illeszkedik-e a lookaheadban levő regex; ha nem illeszkedik, akkor backtrackel, de ha illeszkedik, akkor továbbmegy, viszont nem dolgozza fel a megnézett részt! Pl az a(?=aaaa)a regexet ha find()oljuk az aaabaaaaa texten, a vastagított részt kapjuk találatként: először illeszt egy a-t, majd megnézi, hogy még négy a következik-e, ha igen, akkor illeszt még egy a-t. Másik példának az (?=a)b regex nem illeszkedik semmire, mert egyrészt az adott pozíción előrenézve a következő karakter a kell legyen, és ha az, akkor megpróbál ráilleszteni egy b-t, ami nyilván nem fog menni.
Visszatérve a feladathoz, így már könnyű dolgunk van: ha van egy számjegy-sorozatunk, ami után hárommal osztható darab számjegy jön előretekintve, tegyünk mögé pöttyöt: \b(\d+)(?=(?:\d{3})+\b)-t cseréljük $1.-ra. Vegyük észre, hogy a háromdigit ismétlését azért teszem bele egy non-capturing groupba, mert különben valami possessive pontosanháromjegyet kapnék, ami nekem nem túl világos, hogy mit kéne jelentsen :D szóval a "pontosan n darab ismétlés" is lehet szintaktikailag lazy meg possessive, de nem kéne mást jelentsen, a pont n az pont n. na mindegy. Eredmény:
sanyi1234 12345678.910 másiksanyi12 142857.142
hát ez nem sokkal jobb, mint volt. Mi történt? Az, hogy a + greedy, most azért ette meg a szám túlnyomó részét. Ha a lazy plust használjuk:
sanyi1234 12.345678910 másiksanyi12 142.857142
Persze, ez meg a szóhatár miatt nem illeszkedik a következő iterációban: bár csak a 12. kerül kiírásra és a 345...-nél folytatja a motor a feldolgozást, mégse fog illeszteni, mert a 345 az eredeti szövegben nem a szó eleje volt. (Anélkül a feltétel nélkül meg a sanyi1234 is kapna pontot, amit nem akarunk. Ha mégis akarnánk, akkor a (\d)(?=(?:\d{3})+\b) regexet cseréljük $1.-ra és kész.)

Ami most segíthet: a lookbehind, aminek szintaktikája (?<=...), ez akkor illeszkedik egy pozícióra, ha a string közvetlenül megelőző része illeszkedik a regexre (de nem dolgozza fel azt, csak megnézi). Szerencsére a Java támogatja a változó hosszú lookbehindot is, a különböző nyelvek elég változatos képet mutatnak afelől, hogy támogatják-e egyáltalán a lookbehindot, és hogy annak lehet-e a hossza nem konstans.
Mit cseréljünk most mire? Ha találunk egy számjegyet, ami előtt egy szóhatár, majd számjegyek voltak (lookbehind), utána pedig hárommal osztható darab számjegy és egy szóhatár (lookahead), akkor cseréljük ezt a számjegyet önmagára és egy pontra. Azaz (<=\b\d*)(\d)(?=(?:\d{3})+\b)-t cserélünk $1.-ra. Kód itt. Működik!

sanyi1234 12.345.678.910 másiksanyi12 142.857.142

Körbenézésből (ahol támogatva van) van negatív is: a negatív lookahead jele (?!...), a negatív lookbehindé pedig (?<!...), és akkor illeszkednek pozíción, ha az előtte/utána közvetlen lévő részre nem illeszkedik a csoport tartalma.

A pöttyös példát egyébként a Friedl könyvéből szedtem.

Legközelebb megnézünk pár extrém regex overabuse-t. Ilyeneket fogunk csinálni, hogy a{n}b{n}-re matchelünk regexet, meg konjunktív normálformának keresünk kielégítő kiértékelést. De csak azért, hogy lássuk, milyen érdekesen lehet egyszerre alkalmazni a körbenézést a capturing groupokkal és a backreference-ekkel együttesen. Nem azért, hogy bárkit arra biztassak, csináljon ilyet.

sőt.

folytköv

No comments:

Post a Comment