Kihagyás

2. gyakorlat

Pozícióra illeszkedő regex elemek és karakterosztály shorthandek

Az alábbi gyakori karakterosztályok a legtöbb regex motor nyelvén elérhetőek valamilyen rövidítésekkel:

  • számjegyek: \d ("digit"), a [0-9] osztályt rövidíti
  • alfanumerikus karakterek: \w ("word"), a latin ábécé betűi, számjegyek és az underscore illeszkedik rá.
  • whitespace: \s ("space"), szóköz, \t tab, \n újsor és pl. a \r carriage return is illeszkedik rá.
  • \D, \W és \S: az előzőek komplementerei.

Vannak továbbá karakterek közötti "pozíciókra" illeszkedő horgony ("anchor") jelek is:

  • ^: az első karakter előtti pozícióra illeszkedik
  • $: az utolsó karakter utáni pozícióra illeszkedik
  • \b: szóhatárra illeszkedik. Ez többféleképp történhet:
    • két karakter között, ha az egyik alfanumerikus, a másik whitespace
    • az első karakter előtt, ha alfanumerikus
    • az utolsó karakter után, ha alfanumerikus

Note: egyes regex motorok nem kezelik teljesen szinkronban a \b és a \w, \s kapcsolatát, pl. Javában a \w nem támogatja az Unicode karaktereket, de a \b igen -- ezt mindenképp érdemes tesztelni az aktuális munkanyelvünkben, hogy éppen az ékezetes vagy extended unicode karakterek hogyan viselkednek a worddel és a bounddal szemben.

Search and replace: sed

Nagyon gyakran nem csak arra van szükség, hogy filterezzünk, hanem hogy kezdjünk is valamit a matchelő sorokban az illeszkedésekkel. A legegyszerűbb ilyen igény, amikor egy search-and-replace-t szeretnénk végrehajtani, amire egy unix terminálban a sed parancs biztosít egy simple interface-t:

1
2
3
4
5
6
7
8
9
$cat 01-alma.txt | sed -E 's/alma/szilva/g'
szöveges file
nem tartszilvaz semmi érdekes információt
nincs hatszilva semmi fölött
Alma nagybetűvel is szerepelhet benne
vagy akár kiálthatjuk is, hogy ALMA
hosszan is: ALMAAA
persze a körte az egész mást jelent
többször is szilva előfordulhat az szilvalma a sorban szilvalmszilva
A sed (stream editor) parancsól a következőket érdemes tudni:

  • ugyanúgy a -E opciót adjuk meg neki, mint a grepnek is, ha ugyanolyan reguláris kifejezésekkel szeretnénk dolgozni.
  • az input streamről várja az inputot, ezért pipeoltuk bele az előző parancsnál a 01-alma.txt file tartalmát cattel. Persze egy sed 's/alma/szilva/g' <01-alma.txt hívás is pontosan ugyanezt tette volna, shell paraméterben átirányítva a standard inputot. Ha viszont közvetlen file feldolgozást szeretnénk tenni, átirányítás nélkül, sed 's/alma/szilva/g' 01-alma.txt is ugyanez történik.
  • a syntax alapvetően: s/regex/mire/, ami a regex regex-re talált illeszkedéseket kicseréli mire, erről, hogy mire is lehet cserélni, egyelőre maradjunk annyiban, hogy nem lehet akármilyen regexet odaírni (hiszen egyértelmű kéne legyen, hogy mire cseréli ki), de ahogy látjuk, egyszerű szóra ki lehet cserélni az illeszkedő substringeket.
  • ezzel a syntaxszal csak az első találatot cserélné le minden sorban:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ sed -E 's/alma/szilva/' 01-alma.txt
    szöveges file
    nem tartszilvaz semmi érdekes információt
    nincs hatszilva semmi fölött
    Alma nagybetűvel is szerepelhet benne
    vagy akár kiálthatjuk is, hogy ALMA
    hosszan is: ALMAAA
    persze a körte az egész mást jelent
    többször is szilva előfordulhat az almalma a sorban almalmalma
    
    a g mint "global" kapcsoló a végén jelenti, hogy ne replaceFirst, hanem replaceAll módban kérjük a cseréket. Azonban érdemes megfigyelnünk, hogy csak átlapolás nélkül fogja illeszteni és cserélni, ugyanúgy, mint a grep is! így pl. az almalmalma szóból nem a szilvalmalma, majd szilvszilvalma, végül szilvszilvszilva lesz, hanem az eredeti szövegben átlapolás nélkül megtalált almákat cseréli szilvára, így kapjuk a fenti kimenetben is szereplő szilvalmszilva szót.

A sed is támogatja, szintén az i opció megadásával a case insensitive illesztést:

1
2
3
4
5
6
7
8
9
$ sed -E 's/alma/szilva/gi' 01-alma.txt
szöveges file
nem tartszilvaz semmi érdekes információt
nincs hatszilva semmi fölött
szilva nagybetűvel is szerepelhet benne
vagy akár kiálthatjuk is, hogy szilva
hosszan is: szilvaAA
persze a körte az egész mást jelent
többször is szilva előfordulhat az szilvalma a sorban szilvalmszilva
Itt tehát két opciót adtunk meg a search funkcióhoz: g, hogy minden találatot cseréljen le, i pedig, hogy az illesztés case insensitive történjen.

Capturing groupok

A zárójeleknek a legtöbb regex motor esetében nem csak csoportosító, hanem capturing szerepe is van: amennyiben az input string illeszkedik a regexre, úgy (rendszerint 0-tól indexelt) csoportokba menti ki az input string egyes substringjeit. Mégpedig:

  • a 0. csoport (rendszerint) a teljes illeszkedő string (tehát teljes illesztés esetén az egész input) lesz,
  • az i. csoport, ahol i>0, pedig a regexben i-edik nyitójellel kezdődő részkifejezésre illeszkedő substring lesz.

Így például a ^\s*(.*)$ regex esetében a nulladik csoport a teljes input stringet fogja visszaadni, az első csoport pedig az input string, kivéve az elejéről letrimmelt whitespaceket.

A legtöbb programozási környezetben egy matches függvény hívása után, amennyiben a match értéke true, elkérhetjük a csoportok tartalmát, ez egy nagyon jól használható módszer információ kinyerésére strukturált szöveges infóból, és annak a cseréjére másra.

Backreference

Az eddigi konstrukciók mind "regulárisak" voltak abban az értelemben, hogy lehet hozzájuk készíteni véges automatát, mely hatékony és gyors elemzést tesz lehetővé. Azonban ahogy a regexek tudásával szemben nőtt az igény, megjelentek a - formális nyelvek elmélete nevezéktana szerint - nem reguláris műveletek is (ezért is próbálom ezen a kurzuson elválasztani az elméleti "reguláris kifejezés" fogalmát a "regex"-től), az egyik első ilyen a backreference támogatása volt: reguláris kifejezés illesztésekor már menet közben illeszteni az inputot az egyik korábbi, capturing grouppal elkapott substringre. Ennek jele sok nyelvben az \1..\9 jelzetek (csak az első 9 capturing groupra támogatja a backreferencet, emiatt is fontos, hogy ahol nem kell kimentsük a substringet, szokjunk rá a non-capturing syntaxra, ha elérhető a regex motorunkban olyan, lásd később).

Egy példával segítve a megértést: a ^.*\b(\w+)\b\s*\b\1\b.*$ regex olyan stringekre illeszkedik, melyekben van szóismétlés:

  • a ^.* rész mindenre illeszkedik a string elején,
  • majd következik egy \b word boundary, egy capture-ölt szó a \w+ részen, egészen egy következő \b szóhatárig, tehát egy teljes szót elfogunk az első csoportba,
  • majd jöhet esetleg némi whitespace,
  • eztán szóhatár (ez itt most redundáns), és a \1 azt mondja, hogy itt megpróbálja illeszteni az eddig elkapott első csoportot
  • ha illeszkedik és vége is lesz a szónak (\b˙szóhatár), akkor utána már .*$ bármit elfogadunk.

Ez így segíthet kiszűrni a pontos szóismétléseket -- egyelőre nem alkalmas a "Nagyon nagyon", eltérő kapitalizációval leírt szavak ismétlésének detektálására, de alakul.

Ha például veszünk egy generált lorem ipsum filet és ebben greppeljük ki a szóismétléses sorokat:

$ grep -E '^.\b(\w+)\b\s\b\1\b.*$' 02-lipsum.txt

(kiír 30 sort pirossal)

Persze ha magukat a szóismétléseket szeretnénk pirossal látni a konzolon, akkor az elejéről és végéről leszedve az anchorokat azokat kapjuk meg:

$ grep -E '\b(\w+)\b\s*\b\1\b' 02-lipsum.txt

(kiír 30 sort, benne a szóismétléseket pirossal)

Észrevehetjük, hogy így pl. a szövegbeli "Pharetra pharetra" részt nem detektálja, hiszen nem ugyanaz a két substring; a case insensitive kapcsolóval azt is elérjük, hogy a backreference is case insensitive vizsgálja az illesztést:

$ grep -E '\b(\w+)\b\s*\b\1\b' -i 02-lipsum.txt

(kiír 36 sort, benne a szóismétléseket pirossal)

Captured group visszaírása seddel

Ha például az a célunk, hogy automatikusan kitöröljük az ismétlődő szavakból (mondjuk) a másodikat, akkor ezt a seddel megtehetjük:

$ sed -E 's/\b(\w+)\b\s*\b\1\b/\1/gi' 02-lipsum.txt > 02-lipsum-removed.txt

$ diff 02-lipsum.txt 02-lipsum-removed.txt

(kiírja a diff formátumban a két file közti eltéréseket: tényleg ismétlődő szavak hiányoznak a másodikból az elsőhöz képest)

A diff formátumról: pl. egy ilyen rész

1
2
3
4
299c299
< Purus in mollis nunc sed id semper risus in. Dignissim cras tincidunt lobortis feugiat. Ornare massa eget egestas purus viverra accumsan in nisl. Egestas sed tempus urna et pharetra pharetra. Et tortor at risus viverra adipiscing at. Nulla facilisi etiam dignissim diam quis enim lobortis. Eu lobortis elementum nibh tellus molestie. Arcu bibendum at varius vel pharetra. Dui ut ornare lectus sit amet est placerat in egestas. Amet risus nullam eget felis eget.
---
> Purus in mollis nunc sed id semper risus in. Dignissim cras tincidunt lobortis feugiat. Ornare massa eget egestas purus viverra accumsan in nisl. Egestas sed tempus urna et pharetra. Et tortor at risus viverra adipiscing at. Nulla facilisi etiam dignissim diam quis enim lobortis. Eu lobortis elementum nibh tellus molestie. Arcu bibendum at varius vel pharetra. Dui ut ornare lectus sit amet est placerat in egestas. Amet risus nullam eget felis eget.
annyit tesz, hogy az első file 299. sora a második file 299. sorára változott (ha kerülnek be plusz sorok vagy törlődnek, akkor a két szám el fog térni), az első file-ban (a < jelzi, hogy "ez volt") volt a hosszabb sor, majd a --- szeparátor után a > jelzi, hogy a második fileban pedig "ez lett" a sor.

Ha nem csak az érdekel minket, hogy melyik sorok változtak (alapvetően a diff soronkénti egyenlőséget vizsgál azzal, hogy megpróbál minél több sort egyeztetni, így észreveszi a sor beszúrást és törlést is, a legtöbb verziókövető rendszerben, pl. a gitben is egy forrásfile-beli változásokat a diff mondja meg), hanem a soron belül karakter szinten vagyunk erre kíváncsiak, akkor pl. erre jó lehet a ccdiff program, ha elérhető a disztrónkban:

$ ccdiff -r 02-lipsum.txt 02-lipsum-removed.txt

(ugyanazt listázza, mint a diff, de pirossal jelzi a törlést, zölddel a beszúrást a sorokon belül, karakter szinten)

Ezen a ponton megjegyezném, hogy mivel a szóismétlés detektálása nem megoldható automatával, ez a konstrukció - szemben a korábbiakkal - már tényleg kivezet a véges automata generálással megoldható problémák köréből és lényegében csak a backtracking alapú (vagy legalább hibrid) regex illesztő motorok képesek támogatni - melyek viszont sokszor érzékenyek a catastrophic backtrackingre, lásd később.

A -E kapcsoló szerepe

Az -E kapcsoló, amit eddig használtunk, a reguláris kifejezést megadó nyelvet módosítja (a támogatott műveleteket nem): ez jelzi, hogy "extended regular expression"-t adunk meg, enélkül pedig "basic regular expression"-t vár a grep és a sed is. A különbség a doksi szerint annyi, hogy basic regular expressionben alapból a (, ), + stb. karakterek (de nem mind!!) alapból csak az adott literált jelentik és hogy elérjük a speciális funkciójukat, ki kell őket escapelni. Például az előző szóismétlés-kereső kód basic regexül:

$ sed 's/\b(\w+)\b\s*\b\1\b/\1/gi' 02-lipsum.txt > 02-lipsum-removed-basic.txt

$ diff 02-lipsum-removed.txt 02-lipsum-removed-basic.txt

(nem ír ki semmit, nincs különbség, ugyanaz az eredmény)

Figyeljük meg, hogy ki kellett escapelni a (, ) és + karaktereket, de a *-ot nem. Éljen a szabvány. Nézzük meg mindig, hogy mit mivel jelöl az aktuális szintaxisunk.

Amiért érdemes tudnunk, hogy létezik ilyen is: a doksi szerint egyebek mellett undefined eredményt ad, ha olyan extended regexet használunk, melyben van backreference - tehát ha portolható, minden rendszeren működő backreference-et tartalmazó grep vagy sed hívást írunk, akkor az -E kapcsoló nélkül, basic regex használatával írjuk meg.

Feladatok

  • Írjunk regexet, mely első capturing groupjába stripeli a whitespace-t (tehát pl kettő három négy- ből a kettő három négy-et stripeli). Miért nem lesz jó "megoldás" a pl. itt is olvasható ^\s*(.*)\s*$ regex?
  • Módosítsuk a szóismétlést kereső regexünket és a két ismétlődést egyre cserélő sed parancsot úgy, hogy ha három vagy több példányban ismétlődik a szó, abból is csak egyet tartson meg!

Utolsó frissítés: 2021-10-23 18:39:09