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 |
|
sed
(stream editor) parancsól a következőket érdemes tudni:
- ugyanúgy a
-E
opciót adjuk meg neki, mint agrep
nek 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 egysed '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 aregex
regex-re talált illeszkedéseket kicserélimire
, 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:
a1 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
g
mint "global" kapcsoló a végén jelenti, hogy nereplaceFirst
, hanemreplaceAll
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 agrep
is! így pl. azalmalmalma
szóból nem aszilvalmalma
, majdszilvszilvalma
, végülszilvszilvszilva
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 |
|
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, aholi>0
, pedig a regexbeni
-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 sed
del¶
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 sed
del 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 |
|
<
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 git
ben 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 akettő 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!