regexp kiértékelési érdekesség

Fórumok

A következő érdekes dologgal találkoztam, és nem tudom megmagyarázni. Remélem, van köztetek olyan, aki jobban ért ehhez, mint én, és elmondja, mi miért.

Szóval van egy reguláris kifejezés: a+(ab)*(abc)*

Van egy minta: aaaababababcabcabc

A reguláris kifejezés, amennyire értem, úgy kéne működjön, hogy legalább 1, de lehetőleg minél több "a" betűt kéne találjon, aztán 0 vagy több "ab" kombinációt, aztán 0 vagy több "abc" kombinációt.

Pl. ezen az oldalon azt írják, hogy "the plus is greedy. That is, the plus causes the regex engine to repeat the preceding token as often as possible. Only if that causes the entire regex to fail, will the regex engine backtrack."

Ennek alapján az a+-nak 4 darab a-t kéne találnia, és ezután az ab 0-t talál, abc 0-t talál. A kifejezés illeszkedik, hiszen 0 ab és 0 abc az elegendő.

C#-ban ez állítólag így is működik, meg állítólag a Visual Studio search boxában is.

Viszont pl. ha linux alatt az egrep parancsot használom, akkor nem.
$ echo aaaababababcabcabc | egrep --color a+
az első 4 a betűt beszínezi,
echo aaaababababcabcabc | egrep --color a+(ab)*(abc)*
viszont beszínezi az egész mintát.

Szóval arra lennék kíváncsi, hogy ez miért van.

Hozzászólások

Az egrep lesz ott slendrián a színezésével.

Itt kizárólag az 'a'-betűket színezi végig, ami ép elmével értelmezhető úgy, hogy számos helyen talált legalább egy a-t, amit az a-k mohó felzabálása után már nem követett semmi a kifejezés hátrelévő részéből.

Az első verziód csak az első négy a-t színezi be?
Mert nálam az összeset.

A második példádban meg a teljes stringre illeszkedik a megadott regexp, miért gond, hogy beszínezi az egészet?
Én nem értek/látok valamit?

Mondjuk az furcsa, hogy pl. a python re modulban a második mintára egyetlen egyezést sem talál, de ez lehet, hogy valami pythonos galádság.

Aki tudja, csinálja, aki nem tudja, tanítja... Hm... igazgatónak talán még jó lennék. :)

Az metakarakterek mohóságának kérdése, hogy az eredmény helyes vagy sem.
Ha +-nak mohónak kell lennie, akkor az egész kifejezés úgy illeszkedik, hogy a bevezető a-kon kívül semmi más.
Ennek mond ellent, ha a legteljesebb egyezés van kifestve.

Sajnos most messze enerváltabb vagyok annál, mint hogy végiggondolhatnám az RE-politikákat és következményeiket.

Mostanra tökéletesen elvesztettem ama bizonyos fonalat.
Ahány környezetben kipróbáltam, annyiféle eredményt kaptam. :)
Úgyhogy ettől kezdve csak érdeklődőként vagyok jelen.

Mondjuk az angol idézethez talán a pythonos variációm passzol inkább.
Az általam tapasztalt shell-beli viselkedés és a pythontól elvárt, de nem teljesülő meg az én emlékeimhez illene.

Aki tudja, csinálja, aki nem tudja, tanítja... Hm... igazgatónak talán még jó lennék. :)

A színezés mond valamit, mert ha az egész minta nem illeszkedik, akkor nem színezne semmit. Én úgy értettem, hogy mivel az egész ki van színezve, ez bizonyítja, hogy a .* nem fal fel mindent, vagyis épp hogy megteszi, de a backtrack miatt mégis visszaáll a .* az "11" re, hogy utána a többi rész is tudjon illeszkedni. Jó fej.

Kezdjük előlről. A te példád esetében igazad van, az egész sor színezése azt jelenti, hogy teljes illeszkedés van. Én másra próbáltam utalni. Ez volt az egyik első példa:

$ echo aaaababababcabcabc | egrep --color a+
aaaababababcabcabc

Ez jól láthatóan :-) minden a-t kiszínez, holott gyakorlatilag az összes hagyományos RE-t kezelni képes UNIX-eszköz a vizsgált tartományban pontosan az első találatig dolgozik, és a soron belül a későbbieket csak külön kérés hatására dolgozza fel. Lásd az általam többször is javasolt sed-et:

$ echo aaaababababcabcabc | sed -E 's/a+/X/'
Xbabababcabcabc
$ echo aaaababababcabcabc | sed -E 's/a+/X/g'
XbXbXbXbcXbcXbc

De tudtommal ugyanígy működik az ed-ben, a vi-ban, a perlben. Az awk match() fv-e az első előfordulást keresi, a sub() az elsőt cseréli (és csak később jelent meg a gsub() fv, ami ott is az összes előfordulást cseréli). Az expr parancs az első előfordulást keresi és adja vissza (vagy adja vissza a hosszát). Szóval az, hogy a grep valami érthetetlen oknál fogva mindegyik előfordulást színezi, az a kivétel. Ráadásul pont nem jól mutatja a tipikus regexp működést.

Az első példa egrep esetén kiszínezi az összes a betűt, mert a regexp-hez képest az összes egyezést megkeresi. Az alább az összes egyezést mutatja
echo aaaababababcabcabc | egrep -o --color "a+"
aaaa
a
a
a
a
a

A helyes találat csak aaaa lenne. A mohóság csak + jelre értendő.

A második pedig így meccsel:
a ->1*a 0*ab 0*abc
...
aaaa -> 4*a 0*ab 0*abc
aaaab -> 3*a 1*ab
...
aaaababababc -> 3*a 3*ab 1*abc
stb.

Ne keverdjen a kettő!

A grep tényleg az összes lehetőséget keresi (hacsak meg nem mondod neki, hogy az első után békén is hagyhatja a keresés helyét).

Hogy az összes lehetőség egyes darabjait milyen megszállott bejárással keresi, és ebből a végén itt mi jön ki színesen, az a másik terület.

Mármint abba tudja hagyni az első sor után, de olyan hol van benne, hogy az első "regexp-darab" után? Meg hát nincs is benne 2 féle regexp megvalósítás szerintem, úgyhogy nem hinném, hogy az illeszkedést úgy venné, hogy 4 "a" betű, aztán megvagyunk, színezésnél meg 3 "a" betű és a többiek...Ez a maximális hosszra illesztés amúgy a "normális", pl. a flex, sed, awk, stb. is így csinálja.

Pedig jót szóltam, csak rosszkor és rosszul. :(
A példa arról szólt, hogy egyes esetekben elegendő a match() logikai értéke és érdektelen a "mi match" kérdésre adott válasz, ha az elsőre igen. A color opcióval szemben elég az első mintát megtalálni. Ha az egész sort kell valamiért vizsgálni, akkor folytatni kell az első minta után ugyanzzal, vagy másik mintával. Példa: Találjuk meg a házszámot: Május 1 utca 17-19/B 1. emelet 8.

Utóbbi - értelmezésem szerint.

a+(ab)*(abc)*

Esetén ha valamiben van 'a', akkor az illeszkedik. Erre írta vki, hogy 4db 'a'-t elég lenne kiszinezni, fölösleges a teljes illeszkedést vizsgálni. Szerintem meg, ha egy pattern akár teljesen is illeszkedhet, akkor azt kell jelölni, még akkor is, ha a pattern szigorú része már előzőleg teljesült. És a grep így is csinál.

--
PtY - www.onlinedemo.hu

Nekem személy szerint az a bajom, hogy:

echo aaaabababcabc | grep -E --color 'a+'

mindegyik különálló "a"-t kiszínezi, holott (az én) definícióm szerint amikor megtalálja az egymást követő 4 db. "a"-t, akkor a keresést akár be is fejezhetné - logikusan nekem az jönne ki belöle, hogy mivel az aaaa lesz a találat, azt színezze ki.

Nem olyan nehéz ez. Az elképzelhető legtágabb illeszkedés fog érvényre jutni. Habár az 'aaaa' illeszkedik az 'a+'-ra, elegendő csupán 3 'a'-ra azt mondani, hogy illeszkedik, utána csak az 'ab'-kat nézni. Ily módon illeszkedik az 'abababab' az '(ab)*'-ra, de ismét elegendő csupán 3 'ab'-ra ráhúzni a regexet. Az utána következő 'abc'-k már az '(abc)*'-ra fognak illeszkedni.

A következő ábra alapján már triviális lesz:


a+ | (ab)* | (abc)*
aaa| ababab| abcabcabc

Látható, hogy volt lehetőségünk a teljes sort kijelölni vele, tehát garantáltan az elképzelhető legtágabb illeszkedés jutott érvényre. A regexek világa gyönyörű! Nagyon szeretem.

A reguláris kifejezés az nem egy algoritmus leírás, amit balról-jobbra olvasva hajt végre valami interpreter, hanem egy halmaz leírása.

Az illesztés olyan szövegrészletet választ ki, ami benne van halmazban. Mivel ilyenből több is lehet, ezért röviden szólva a balszélső leghosszabbat választja ki.

Amikor már megvan az illeszkedés, akkor lehet elkezdeni foglalkozni a részletekkel, hogy a regexp melyik részlete, az illeszkedés pontosan mely betűihez illeszkedjen.

Szerintem.

A kérdés az, hogy az a+ az négyre _kell_, hogy illeszkedjen, vagy elég 3-ra is, mivel van még keresendő egyezés.

Ha az a{4}(ab)*(abc)* is kiszínez mindent, akkor kezdj el aggódni szerintem :)

--
PtY - www.onlinedemo.hu

zamboriz meglehetősen velős hozzászólásában van a lényeg. Egy regexpnél a szabály az, hogy az első előfordulást fogja megtalálni. Kedves példa rá:

$ echo zabaabaaab | sed -E 's/a*/X/'
Xzabaabaaab
$ echo zabaabaaab | sed -E 's/a+/X/'
zXbaabaaab

Ellenben ha adott pozícióban többféle hosszúságban lehet találni, akkor ebből a pozícióból kiindulva a lehető leghosszabbat fogja kiválasztani. Az, hogy a RE egyes részei hol fejeződnek be és hol kezdődnek a megtalálásnál, az csak a legvégén dől el. Ezt grep-pel még színezéssel sem igazán lehet kimutatni (mert kicsit másént viselkedik, mint az néhányaknak logikus lenne), de pl. sed-del igen.

$ echo aaaababababcabcabc | sed -E 's/a+/X/'
Xbabababcabcabc
$ echo aaaababababcabcabc | sed -E 's/a+(ab)*(abc)*/X/'
X
$ echo aaaababababcabcabc | sed -E 's/a{4}(ab)*(abc)*/X/' # Pty javaslata
Xbabababcabcabc

Erre kategorikus választ csak az tud adni, aki az MS-féle regexp definícióját ismeri. Amit én(*) mondtam, az - tudtommal - a UNIX/Linux világban használatos, az OpenGroup.org -on szereplő (általában POSIX-szabványként aposztrofált) leírás - szerintem - rövid, emberi nyelvre fordított verziója. De ha valaki mutat egy pár soros C# kódot, ami a kezdő felvetésben szereplő szringekkel és regexpekkel operálva kiírja az eredeti sztringet, majd a RE-vel meghatározott részt ebből a sztringből lecseréli X-re és azt is kiírja (ahogyan mondjuk itt én a sed-del tettem), akkor könnyedén eldönthető, hogy ugyanazt az eredményt kapjuk-e.

(Ja, ha valaki esetleg eltűnődött volna: a sed alapból BRE-t, és nem ERE-t használ, viszont van a GNU-sednek van egy dokumentált -r opciója az ERE-hez. Itt pedig azt is leírtam, hogy miért használom ennek ellenére következetesen a -E opciót erre. Mert hordozhatóbb :-) )

(*) itt konkrétan zamboriz mondta legfrappánsabban

Szerk: tényleg, milyen jó lnne egy C#-ban megírt sed (nyilván bőven elég lenne a sed s-parancsát implementálni)


PS C:\> "zabaabaaab" -replace "a*","X"
XzXXbXXbXXbX
PS C:\> "zabaabaaab" -replace "a+","X"
zXbXbXb
PS C:\> "aaaababababcabcabc" -replace "a+","X"
XbXbXbXbcXbcXbc
PS C:\> "aaaababababcabcabc" -replace "a+(ab)*(abc)*","X"
XbXbXbXbcXbcXbc
PS C:\> "aaaababababcabcabc" -replace "a{4}(ab)*(abc)*","X"
Xbabababcabcabc
PS C:\>

Lehet elemezni. :)

Az elsőre látszik rajta, hogy valami mucsajröcsögei nyelvjárást beszél, nem POSIX-et. Ez amúgy powershell akar lenni, vagy mi a szösz? Esetleg nincs neki -replace -eager vagy -replace -not-so-eager verziója? Esetleg az -eager mellett mondjuk -only-the-first opciója? Mindenesetre a *-ot eléggé nem a *X-világban megszokott módn értelmezi. Esetleg megmutatod ugyanezt "a{3}(ab)*(abc)*" mintával is?
Szerk:

PS C:\> "zabaabaaab" -replace "a*","X"
XzXXbXXbXXbX

némi fejvakarás után ez a legelső példa minősül a legdurvábbnak. Egyszerűen nem értem, a végére miért tesz még egy "X"-et. Meg az első 2 "a"-t miért 2 "X"-re cseréli, ha utána a 3 db "a"-t szintén. Mi lenne 4 db "a"-ból? És ha beleraksz egy plusz "c"-t a legvégére, akkor lesz "X" a "b" és a "c" között? Megannyi gyötrő kérdés, de azt így látatlanban le merném fogadni, hogy aki implementálta, az nem értette a POSIX-regexpeket :-) - vagy csak simán te szórakozol.

Tényleg powershell, és ördögöd van, a végére bökött c hatására az eredmény vége bXcX.

Azt is jól sejted (illetve kérdezed elképedve), hogy tényleg mindig az összes előfordulást (sőt, itt még az előfordulatlanságot is) cseréli-e. Legalábbis a 2.0-ásban, amikor utoljára használni mertem a -replace operátort, még mindent-vagy-semmit volt a jelszó. Ha jól emlékszem, amikor az ember visszafogottságra vágyik, akkor közvetlenül kell példányosítania a .NET regex osztályából (vagy tán annak a szülőosztályából), mert a PS-be kötött része el van cseszve as designed sajátos.

A fenti hajmeresztést egyébként még nem szúrtam ki, de tényleg létezik. És non-greedysítve se normálisabb:

PS C:\> "zabaabaaab" -replace "a*?","X"
XzXaXbXaXaXbXaXaXaXbX

Magánvéleményem: ahol a MS magának találhatta ki a funkciókat, egész korrekt a PS, ahol szabályt kellett követnie, ott a partner szkriptinggájok vért izzadnak, hogy úgy tudják megfogalmazni az oktatóanyagokat, hogy véletlenül se kerüljön beléjük az alapvető hiba és a következmények. Az MS-only kollégák pedig csak elvétve veszik észre/teszik szóvá, hogy valami itten hiányzik, vagy mintha nem így kéne működnie.

Itt írja, hogy milyen opciók vannak. Powershell replaceből (ami egy operátor amúgy) csak az inline modifiers érhetőek el.

Azt a mintát konkrétan végigvittem fejben, szerintem valahogy úgy értelmezi, hogy keressük meg az első match-t, csináljunk replace-t, folytassuk az előző match végétől amíg a string végére nem érünk. Azaz (a # jelöli az a{0}-t, a | hogy honnan keresi a következő match-t):

"zabaabaaab" = "|#z#a#b#a#a#b#a#a#a#b#" -> "X|z#a#b#a#a#b#a#a#a#b#" -> "XzX|#b#a#a#b#a#a#a#b#" -> "XzXX|b#a#a#b#a#a#a#b#" -> "XzXXbX|#b#a#a#a#b#" -> "XzXXbXX|b#a#a#a#b#" -> "XzXXbXXbX|#b#" -> "XzXXbXXbXX|b#" -> "XzXXbXXbXXbX|" = "XzXXbXXbXXbX"

http://www.phpliveregex.com/

Itt nézegettem, a + és a * a kiértékelés sorrendjén változtat szerintem.

(a+)((ab)+)((abc)+)

Itt olyan mintha jobbról balra menne, abc-t, ab-t, a-t keresne, és így az egészre illeszkedik:


Array
(
    [0] => aaaababababcabcabc
    [1] => aaa
    [2] => ababab
    [3] => ab
    [4] => abcabcabc
    [5] => abc
)
(a+)((ab)*)((abc)*)

Ez meg balról jobbra, a-t, ab-t, abc-t keresne, ezért nem illeszkedik:


Array
(
    [0] => aaaa
    [1] => aaaa
    [2] => 
    [3] => 
    [4] => 
)
(a+)((ba)*)((bca)*)

Viszont értelemszerűen illeszkedik (a, ba, bca a sorrend):


Array
(
    [0] => aaaababababcabca
    [1] => aaaa
    [2] => bababa
    [3] => ba
    [4] => bcabca
    [5] => bca
)

A miértre én sem tudom a választ.

(Szerk.: most látom hogy neked pont nem így van, akkor ez maga a kérdés volt, sorry. Az egrep mindkettőt végig kiszínezi, de akkor talán a jobbról balra dolog miatt lehet?)