Reguláris kifejezések, mintaillesztés

A Perl nyelv egyik, ha nem a legnagyobb erőssége az igen fejlett mintaillesztési lehetőség, vagy másképp fogalmazva a reguláris kifejezések kezelése. Persze a Perl nyelv előttjóval voltak már UNIX operációs rendszer alatt olyan eszközök, amelyek reguláris kifejezéseket használtak, de a Perl nyelvnél talán mindegyiknél jobb, és teljesebb lehetőségek vannak.

Ha még nem használt soha reguláris kifejezéseket, akkor ennek a fejezetnek az elolvasása előtt ajánlott az m operátor illetve az s operátor fejezetek elolvasása. Ugyanakkor azok teljes megértéséhez szükséges ennek a fejezetnek az elolvasása. Így tehát nem tudom, hogy melyiket érdemes elolvasni először. Feltehetőleg az egyiket, aztán a másikat, és azután néhányszor újra.

Bevezető
Reguláris kifejezésre rendkívül egyszerű példát mutatni. A következő példa a @mail tömb elemeiről próbálja meg eldönteni, hogy jók lesznek-e eMail címnek.

@mail = (
  'verhas@mail.digital.hu',
  'hab.akukk%mikkamakka@jeno',
  );

for( @mail ){
  if( /^.*@\w+\..+$/ ){
    print "$_ jó email címnek látszik\n";
    }else{
    print "$_ Ez nem jó cím\n";
    }
  }
Az első utasítás a @mail tömböt hozza létre. Ennek elemein megy végig a for ciklus, és minden egyes elemet megvizsgál, és megpróbálja eldönteni, hogy jó lesz-e elektronikus levélcímnek vagy sem. Mivela for ciklusnak nem adtunk meg változót, ezért a Perl a $_ változót használja ciklus változónak, és mivel az if utasításban sem adjuk meg, hogy mely változót akarjuk a reguláris kifejezéshez hasonlítani, ezért ez is ezt a változót használja. (Minő véletlen egybeesés.)

A példabeli reguláris kifejezés szavakkal elmondva a következőt jelenti:

A kimenet:
verhas@mail.digital.hu jó email címnek látszik
hab.akukk%mikkamakka@jeno Ez nem jó cím

A reguláris kifejezéseket, mint a fenti példában is látható használhatjuk arra, hogy bizonyos szintaktikus szabályoknak való megfelelést vizsgáljunk füzéreknél. Ehhez a m operátort használtuk a fenti példában. Magát az operátort azért nem kellett kiírni, mert a / jelet használtuk határolónak, a fűzért pedig azért nem, mert az a default változó, a $_ volt. Az if utasításbeli kifejezést írhattuk volna
$_ =~ m/^.*@\w+\..+$/

alakban is.

A másik lehetőség reguláris kifejezés használatára, amikor bizonyos részeket ki akarunk cserélni egy fűzérben. Nézzük példának a következő kis programot:

$text = 'Java szigetén nem használnak JavaScript programokat.';
$text =~ s/Java(?!Script)/Borneo/;
print $text;

aminek a kimenete
Borneo szigetén nem használnak JavaScript programokat.
Az s operátor egy reguláris kifejezést és egy interpolált füzért használ. Végignézi a változót, amely a példában $text és az első olyan részfüzért, amely megfelel a reguláris kifejezésnek kicseréli a második paraméterként megadott füzérre. Így lesz Java szigetéből Borneó.

Érdemes megfigyelni, hogy ezeknél a műveleteknél nem = jelet használunk, hanem =~ operátort. Ezt pedig nem szabad összetéveszteni a ~= operátorral! (A nyelv szépségei!) Ha ugyanis valaki az előbbi példában a

$text = s/Java(?!Script)/Borneó/;

írná, akkor a helyettesítési operátor (s///) nem a $text változót, hanem a default $_ változót keresné, amelyben nem talána egyetlen Java alfüzért sem, és visszatérési értékként üres füzért ad vissza. Más szavakkal: a program nem írna ki semmit, vagy precízebben semmit írna ki.

Részletesen és precízen az m operátorról, és a s operátorról. Ezeket egy kicsit nehéz megérteni a reguláris kifejezések ismerete nélkül, de vígasztaljon, hogy a reguláris kifejezéseket pedig nehéz megérteni az m és az s operátorok ismerete nélkül.

Amikor a Perl egy füzért megpróbál hozzáilleszteni egy reguláris kifejezéshez mind a füzérben, mind pedig a reguláris kifejezésben balról jobbra halad, egészen addig, amíg el nem ér a reguláris kifejezés vagy a füzér végére. Ha ekkor az egész reguláris kifejezést hozzá tudta illeszteni a füzérhez, akkor a füzér megfelel a reguláris kifejezésnek, ha nem, akkor pedig nem. (Például az m operátor igaz, vagy hamis értéket ad vissza.)

A meta karakter fogalma
Az illesztés során a reguláris kifejezésben minden közönséges karakter, amelyik nem metakarakter (definíció mindjárt) önnmagának felel meg. Ha tehát a reguláris kifejezés következő karaktere egy 'a' betű, akkor a füzér következő illesztendő karaktere is 'a' betű kell, hogy legyen.

A metakarakterek speciális karakterek, amelyek a reguláris kifejezésben speciális jelentőséggel bírnak. Ilyen például a . pont, amely a füzérben bármilyen nem újsor karakternek megfelel. Egy másik speciális karakter a $ jel, amelyik a füzér végének, vagy a füzér végén lévő újsor karakternek felel meg. Hasonlóan a ^ karakter a füzér elejének felel meg. Ezt a viselkedést mind az m, mind pedig az s operátoroknál változtatni lehet az m és s opciókkal. Az m opció esetén a füzért többsorosnak tekinti a rendszer, ami praktikusan annyit jelent, hogy minden sor végét meg lehet találni a $ jellel, ami a reguláris kifejezésben a sor vagy a füzér végét jelenti, és hasonlóan minden sor elejét a ^ jellel. Ennek az ellentéte a s opció, amely esetben a füzért egysorosnak tekinti a Perl, és a $ csak a füzér végét fogja megtalálni, vagy a füzér végén álló soremelést a ^ jel pedig csak a füzér elejét úgy, mintha sem az s sem pedig a m opciót nem adtuk volna meg. Az s opció igazi értelme, hogy ennek megadásakor a reguláris kifejezésekben a . pont karakter megfelel a soremelés karaktereknek is, míg enélkül az opció nélkül ezeknek a karaktereknek nem felel meg ez a metakarakter.

Az egyik legfontosabb metakarakter a \ amelyet ha egy metakarakter elé írunk a füzérben a karakter pontosan önnmagának fog megfelelni, így \. egy pontot jelent a füzérben vagy \\ egy fordított törtvonal karaktert. Ha nem metakarakter elé írjuk a fordított törtvonalat, akkor előfordulhat hogy más jelentést kap a karakter, például \t a tabulátor, vagy \w az betű karaktereknek felel meg, de erről még később.

Zárójelek
A zárójelek is speciális jelentéssel bírnak. Ezekkel a reguláris kifejezés egyes részeit lehet bezárójelezni, és ezekre a részekre lehet a későbbiekben hivatkozni a $1, $2, ... változókkal a reguláris kifejezésen kívül, vagy a \1, \2, ... referenciákkal a reguláris kifejezésen belül. Példaképpen

$text = 'Java szigetén nem használnak szigonyt.';
$text =~ /(Ja(va)).*(szi).*\3(g(e|o))/;
print "$1 $2 $3 $4 $5\n";

kimenete
Java va szi go o

Látható, hogy a számozási sorrend a nyitó zárójelek szerint megy, és nem a záró zárójelek szerint. Ennek csak abban az esetben van jelentősége, ha, mint ebben a példában, egymásba vannak ágyazva a zárójelek.

Lehetőség van arra is, hogy úgy zárójelezzünk be egy részt a reguláris kifejezésből, hogy az ne hozzon létre referenciát. Ehhez a (?: nyitó karakter sort és a szokásos ) zárójelet kell használni, például:

$text = 'Java szigetén nem használnak szigonyt.';
$text =~ /(Ja(?:va|vi)).*(szi).*\2(g(e|o))/;
print "$1 $2 $3 $4\n";

kimenete
Java szi go o

Itt rögtön látunk egy új metakaraktert, a | jelet. Ez a jel választást jelent a bal és a jobb oldalán álló rész reguláris kifejezések közül bármelyik megfelelhet a füzér éppen soron levő darabjának.

Karakter osztály
Még egy érdekes zárójel van a reguláris kifejezésekben, a [ és ]. Ezek kartakter osztályokat fognak közre. Ha egy reguláris kifejezésben azt írjuk, hogy [acfer], akkor az az a, c, f, e és r betűk bármelyikének megfelel. Ugyanakkor rövidíthetünk is karakterosztályok megadásánál. Például [a-f] az összes a és f közötti karakternek felel meg, vagy [a-fA-F] ugyanez a karakter osztály, de a kisbetűk és a nagybetűk is. Ha a legelső karakter a karakterosztály megadásánál a [ karakter után egy ^ karakter, akkor a karakter osztály invertálódik, azaz [^a-f] bármilyen karakternek megfelel, kivéve az a és f közötti karaktereket. Karakterosztály megadásánál a | karakter is közönséges karakternek számít.

Ismétlés
Egy reguláris kifejezés egy részére, vagy az egész reguláris kifejezésre előírhatjuk, hogy hányszor kell egymás után alkalmazni. Ezt a reguláris kifejezések ismétlést előíró modosítóival tehetjük meg, amelyeket mindig a reguláris (rész)kifejezés után kell írni. Ezek:
*nulla vagy többszöri ismétlés
+egy, vagy többszöri ismétlés
?nulla, vagy egyszer
{n}pontosan n-szer
{n,}legalább n-szer
{n,m}legalább n-szer, de legfeljebb m-szer
Ha a kapcsos zárójel bármilyen más környezetben fordul elő, akkor normál karakterként értelmezi a Perl. n és m nulla és 65,535 közötti értékek lehetnek.

Alaphelyzetben ezek az ismétlési módosítók falánkak, és annyira előreszaladnak, amennyire csak lehet. Ezt lehet visszafogni azzal, ha az ismétlési módosító mögé egy ? jelet írunk. Ekkor csak annyit ismételnek ezek a módosítók, amennyit feltétlenül kell. Példaképp egy reguláris kifejezés illesztés * és *? módosítóval

'Java szigetén nem használnak szigonyt.' =~ /Ja(.*)g(?:e|o)/;
print "$1\n";
'Java szigetén nem használnak szigonyt.' =~ /Ja(.*?)g(?:e|o)/;
print "$1\n";

és a kimenete
va szigetén nem használnak szi
va szi

Speciális karakterek
Mivel a reguláris kifejezések mint interpolált füzérek kerülnek kiértékelésre, ezért a következők is használhatók:
\ttabulátor (HT, TAB)
\nújsor, soremelés (LF, NL)
\rkocsivissza karakter (CR)
\flapdobás (FF)
\acsengő (bell) (BEL)
\eescape (ESC)
\033oktális kód megadása (mint a PDP-11 nél)
\x1Bhexa karakter
\c[kontrol karakter
\la következőkarakter kisbetűsre konvertálva
\ua következő karakter nagybetűsre konvertálva
\Lkisbetűsek a következő \E-ig
\Unagybetűsek a következő \E-ig
\Ekisbetű, nagybetű módosítás vége
\Qreguláris kifejezések meta karakterei elé fordított törtvonalat ra a következő \E-ig
Ezeken kívül a Perl még a következőket is definiálja
\wegy szóban használható karakter (alfanumerikus karakterek és aláhúzás)
\Wminden ami nem \w
\sszóköz fajta karakter, szóköz, tabulátor, újsor stb.
\Sminden ami nem \s
\dszámjeg
\Dnem számjegy
Ezek a jelölések karakterosztályok megadásában is használhatók, de nem intervallum egyik végén.

A következők is használhatók még reguláris kifejezésekben:
\bszó határának felel meg
\Bnem szó határnak felel meg
\Acsak a füzér elején
\Zcsak a füzér végén
\Gott folytatja ahol az előző m//g abbahagyta
A \b karakterosztályban használva visszalépés karakternek felel meg. Egyébként mint szóhatár olyan helyet jelöl, ahol az egyik karakter \w és a mellette levő \W. Ennek eldöntésére a füzér elején az első karakter elé és a füzér végén az utolsó karakter utánra egy-egy \W karaktert képzel a rendszer.

A \A és \Z ugyanaz, mint a ^ és a $ azzal a különbséggel, hogy ezek még a m opció használata esetén sem felelnek meg a füzér belsejében levő soremelés karakternek.

Egyéb kiterjesztések
A reguláris kifejezések a Perl nyelvben a szokásos egyéb UNIX eszközökhöz képest további kiterjesztéseket is tartalmaznak. Így reguláris kifejezésen belül lehet használni

(?#megjegyzés)
megjegyzéseket lehet a reguláris kifejezésekben elhelyezni. Amennyiben az x opciót használjuk egy sima # is megteszi.
(?:regexp)
Zárójelezett reguláris kifejezés, amely azonban nem generál referenciát, azaz nem lehet rá hivatkozni a $1, $2, ... változókkal, illetve a \1,\2 ... visszahivatkozásokkal. Ugyanakkor nagyon hasznosak, akkor ha valamilyen alternatívát kell megadni a | metakarakterrel.
(?=regexp)
Nulla hosszúságú előrenézés. Ezzel a konstrukcióval egy olyan rész reguláris kifejezést lehet megadni, amelyet a Perl leellenőriz, de mégsem vesz bele az illesztett listába. Például /\w+(?=\t)/ reguráris kifejezésnek egy olyan szó felel meg, amelyet egy tabulátor követ, a tabulátor azonban nem veszi figyelembe a Perl, csak megnézi, hogy ott van. Ha a helyettesítés operátort használjuk, akkor
$t = 'jamaica rum rum kingston rum';
$t =~ s/([aeoui])(?=\w)/uc($1)/ge;
print $t;
kimenete
jAmAIca rUm rUm kIngstOn rUm
ami azt jelenti, hogy a mintaillesztés során a Perl megnézte, hogy betű áll-e a magánhangzó után, de azt a betűt, már nem tekintette az illesztett, és így helyettesítendő füzérdarab részének. Ennek megfelelően ez a kis programdarab minden olyan magánhangzót nagybetűre cserélt, amely nem szó végén állt.
(?!regexp)
Negatív nullahosszúságú előrenézés. Hasonló a (?= ... )-höz, de akkor fogadja el a mintát, ha a következő füzérdarab nem felel meg a megadott reguláris kifejezésnek. Az előző példát használva és persze módosítva egy kicsit
$t = 'jamaica rum rum kingston rum';
$t =~ s/([aeoui])(?!\w)/uc($1)/ge;
print $t;
kimenete
jamaicA rum rum kingston rum
ami minden olyan magánhangzót cserél nagyra, amelyet nem betű követ. Nagyon fontos megjegyezni, hogy ezekkel a konstrukciókkal csak előre lehet nézni, visszafelé nem. Ha ugyanis azt írjuk, hogy (?!Borneo)Script, akkor az nem azt jelenti, hogy minden olyan Script amely előtt nem Borneo áll. Ez a reguláris kifejezés minden Script-et meg fog találni, a BorneoScript-eket is, illetve annak Script részét, hiszen amikor a (?!Borneo) részt értékeli ki a Perl azt fogja találni, hogy a vizsgált helyen nem Borneo áll, hanem Script. Ez így neki rendben van, megy tovább és ismételten látni fogja, hogy az adott helyen Script áll, ezt most már illeszti is a reguláris kifejezés további részéhez, hiába állt a Script előtt Borneo.
(?imsx)
Reguláris kifejezés értelmezését módosító opció megadása a reguláris kifejezésen belül. Ezeket az opciókat általában az m vagy s operátorok után szoktuk megadni, néha azonban szükség lehet, hogy magába a reguláris kifejezésbe kerüljön bele az opció. Ilyen lehet az, amikor a reguláris kifejezés egy változóban van megadva. Az egyéb opciók, mint az e vagy g nem adhatók meg a reguláris kifejezésen belül, hiszen ezek nem a reguláris kifejezésre vonatkoznak, hanem magára a m és s operátorok végrehajtására. Az opciókat érdemes a reguláris kifejezés elején megadni, mert különben igen érdekes, és meglehetősen nehezen értelmezhető hatásuk lesz, például a
$t = 'jAmAIca rUm rUm kIngstOn rUm';
$t =~ s/(?i)ica/juli/g;
$t =~ s/RuM(?i)/bor/;
print $t;
hatására miért csak az utolsó rUm-ból lesz bor:
jAmAjuli rUm rUm kIngstOn bor
Visszalépéses kiértékelés
Amikor a Perl egy reguláris kifejezést illeszt egy füzérhez az illesztés csak akkor sikeres, ha az egész reguláris kifejezést sikerült illesztenie. Amikor az illesztés során a mintaillesztő algoritmus előre halad, és valahol elakad, akkor még nem adja fel, hanem megpróbál visszalépni és újra próbálkozik. Így, amikor a
$text = 'Java szigetén nem használnak JavaScript programokat.';
$text =~ s/Java(?=Script)/Borneo/;
print $text;

példában elindul a mintaillesztés a Perl eljut egészen az első Java végéig, és illeszti a reguláris kifejezést. Itt azonban elakad, mert a (?=Script) megkövetelné, hogy a Java szó után a Script szó következzen. Ezért visszalép és próbálja a füzér további részéhez illeszteni a reguláris kifejezést, amíg el nem jut a JavaScript szóhoz, amelyhez sikerül az illesztés, és ennek megfelelően megtörténik a csere, így az eredmény
Java szigetén nem használnak BorneoScript programokat.
Egy másik példát megnézve
$t = 'Mi van a jing és a jang között? Talán a Jangce?';
$t =~ /jing(.*)jang/i;
print $1;
az eredmény
 és a jang között? Talán a 
Ennek az az oka, hogy amikor a jing szót megtalálja az illesztés a .* részkifejezésseé addig rohan előre, ameddig csak tud. Először a füzér végéig. Itt azonban elakad, és elkezd visszafelé lépkedni. Egészen a Jangce szóig, aholis sikeres a reguláris kifejezés maradék részének az illesztése is.

Ismét egy példát nézve a

$_ = "Van 1 számom: 53147";
if ( /(.*)(\d*)/ ) {
  print "Ami van: <$1>, a szám pedig <$2>.\n";
  }else{
  print "nincsen számom...\n";
  }

az eredménye
Ami van: <Van 1 számom: 53147>, a szám pedig <>.

aminek az oka, hogy a \d* hozzáilleszthető a füzér végén egy üres füzérhez. Helyesen:
$_ = "Van 1 számom: 53147";
if ( /(.*\D)(\d+)/ ) {
  print "Ami van: <$1>, a szám pedig <$2>.\n";
  }else{
  print "nincsen számom...\n";
  }

az eredménye
Ami van: <Van 1 számom: >, a szám pedig <53147>.


Verhás Péter Home Page . . . . . . Perl röviden, tartalomjegyzék