ugrás a tartalomhoz

Perl alapjai V. - Mintaillesztő kifejezések

Bártházi András · 2004. Szep. 28. (K), 21.00
Perl alapjai V. - Mintaillesztő kifejezések
A Perl egyik erőssége a szövegfeldolgozás, s különösebb hezitálás nélkül állíthatom: ezt legfőképpen a natív, nyelvi szintű mintaillesztő kifejezés (reguláris kifejezés) támogatásának köszönhető. A mai alkalommal erről, illetve a mintaillesztő kifejezésekről általában lesz szó (megjegyezve, hogy a mintaillesztő kifejezésekről a jövőben valamikor tervezünk egy sorozatot, aminek a bevezetését nem fogja a mai cikk helyettesíteni). No de vágjunk bele!

Mintaillesztő kifejezések

A mintaillesztő kifejezések (a "reguláris kifejezés" helyett ezt fogom használni, mivel sokkal jobban fedi a lényeget, és sokkal inkább magyarul van) támogatása, ahogy a bevezetőben is kiemeltem, a Perl egyik erőssége (de nem sajátja, már több nyelv kiegészítéseként is megjelent ezeknek a kifejezéseknek a használatát lehetővé tevő modul), már a nyelv második verziójától a nyelv részét képezik (ez viszont, hogy bele van építve a nyelvbe, az ismertebb nyelvek egyiknél sem fordul elő!). Segítségükkel többsoros kódrészleteket helyettesíthetünk egyetlen kifejezéssel, s sok esetben jóval elegánsabban oldhatjuk meg problémáinkat, mint használatuk nélkül (és a motorban rejlő trükkös algoritmusoknak köszönhetően van, amikor egy egyszerű algoritmust megvalósító gépi kódú résznél is gyorsabban). A cikk csak egy rövid ízelítőt próbál meg nyújtani arról, hogy mire használhatóak, s milyen lehetőségek rejlenek bennük, a témakör olyan nagy, hogy már több könyv is született, mely ezzel foglalkozik, s mi is ki fogunk térni részletesebben is a lehetőségekre.

Mi is valójában egy mintaillesztő kifejezés? Első ránézésre jelek összevisszasága (második ránézésre is), azonban ha jobban megszemléljük, akkor egy nagyon jól használható eszköz. Röviden arról szól, hogy egy karakterláncban szeretnénk egy részletet keresni. A gyökerei valahol a "joker" karaktereknél vannak, a legismertebb ilyen karakterek a * és a ?, melyek a DOS (vagy akár Windows, UNIX) kapcsán váltak híressé. Ha ezeket a jeleket használjuk, akkor * esetén bármennyi és bármilyen, a ? esetén pedig egy bármilyen karaktert értünk alatta. Példával illusztrálva a *.txt alatt az összes .txt kiterjesztésű állományt értjük, a hell?.txt kifejezés esetén pedig nem a pokolra kérdezünk rá angolul, hanem a hello.txt-t, a hella.txt-t és társait keressük, vagyis azokat a fájlokat, melyeknél a ? helyén bármely karakter szerepelhet.

Illesztő operátor

A legegyszerűbben talán egy példával lehet rávilágítani a mintaillesztő kifejezések működésére. Ennek örömére az illesztő operátorral is meg fogunk ismerkedni, melyet korábban a skalár műveleteknél kihagytunk, pedig egy a Perlben gyakran használt operátorról van szó. A feladat nagyon egyszerű lesz, egy tömbből fogjuk kiválogatni azokat az elemeket, melyek tartalmazzák az "egy" szórészletet. Erre kell gyártanunk egy olyan kifejezést, mely illeszkedni próbál majd a tömb elemeire, s az értéke igaz lesz, ha ez az illeszkedés sikeres. Íme a forráskód, mely ezt elvégzi:

@tomb=('teljes','egyetem','begy','lehet','stb');
foreach(@tomb)
  {
  if (/egy/)
    {
    print "A(z) $_ szó tartalmazza az 'egy' részletet\n";
    }
  }
Amennyiben elolvastuk a korábbiakban a Perl alapjai cikkeket, sok újdonsággal nem fogunk találkozni. Először egy tömbnek adunk értéket, majd a tömb elemein végigmegyünk egy foreach ciklussal (az aktuális elem mindig a $_ helyi változóban lesz elérhető), valamit csinálunk az "egy" szóval, majd ha ez a valami igaz, akkor kiírjuk hogy az aktuális szó tartalmazza az "egy" karaktersorozatot. A hangsúly azon a bizonyos valamin van, azaz az if feltételén. A /-ek közé bizony, bizony, egy mintaillesztő kifejezés van írva, ez kerül illesztésre, s amennyiben benne van az adott változóban, akkor igaz az értéke, ha nincs, hamis. Az aktuális változó, ha nem adjuk meg, akkor a $_ rejtett, ún. "téma" változó. Megadni a következőképpen tudtuk volna (a $_ helyére értelemszerűen bármely skalár változó kerülhet):

@tomb=('teljes','egyetem','begy','lehet','beetet','stb');
foreach(@tomb)
  {
  if ($_=~/egy/)
    {
    print "A(z) $_ szó tartalmazza az 'egy' részletet\n";
    }
  }
Kérdezheti az olvasó, hogy jó, jó, de hol van itt a beígért bonyolultság, meg az olvashatatlan karakterek első ránézésre? Ha akarjuk, egyszerűen beírjuk tehát / jelek közé amit illeszteni szeretnénk, és jó lesz, nem kell ezt annyira túlmisztifikálni. Ez az egész olyan, mint a substring tömb, csak egy kicsit rövidítettünk, illetve másképp írtuk le. Hát, valóban nem kell túlmisztifikálni a dolgot. Ez így nagyon egyszerűnek tűnik, s az is. De hogy ne legyen egyszerű, inkább gyorsan el is kezdjük bonyolítani. A feladat legyen az, hogy ki kell választani azokat a szavakat, amelyekben legalább kettő "e" betű van. Nos, ez már egy substring eljárással sem tűnik olyan egyszerűnek, de meg fogjuk látni, hogy egy mintaillesztő kifejezéssel ez nem is olyan bonyolult. A következőképpen tudjuk ezt megtenni:
@tomb=('teljes','egyetem','begy','lehet','beetet','stb');
foreach(@tomb)
  {
  if (/e.*e/)
    {
    print "A(z) $_ szó legalább két e betűt tartalmaz\n";
    }
  }
Az általunk alkalmazott trükk az volt, hogy egy "e", majd "bármilyen karakterek" és megint egy "e" részletre kerestünk rá. Ha volt ilyen, akkor két "e" betű van a szóban. A kifejezésben (a / jelek között) a . egy bármilyen (kivéve a soremelés, de esetünkben ez mindegy) karaktert jelöl, a * pedig azt, hogy az előző karakterből bármennyi (0 vagy több) lehet, azaz pontosan azt jelenti, ami nekünk is kellett. Ha együtt látunk egy .* párost, akkor az azt jelenti, hogy bármennyi (akár nulla is), s majdnem bármilyen karakter szerepelhet az adott helyen. Olyan, mintha a joker karakterek esetén egy *-ot írtunk volna. Ha jobban megnézzük, a . pedig a joker karaktereknél a ? kérdőjelet helyettesíti. De többet nem fogjuk emlegetni a jokereket.

Visszatérve a jelen kifejezésünkre, elemezzük, nézegessük egy kicsit. Ha azokra a szavakra szeretnénk rákeresni, melyek egy "e"-t, egy (pontosabban pontosan egy) "bármilyen" karaktert és egy másik "e"-t tartalmaznak, akkor egyszerűen el kell hagynunk a '*'-ot, a kifejezésünk e.e-re módosul. Ha két egymás melletti "e"-t keresünk, akkor a korábbiakhoz hasonlóan csak ee lesz a kifejezés, stb. Vannak még más karakterek is, melyeket ismer a Perl, ezekről íme egy lista:

.   - bármely karakter
*   - 0, vagy több az előző karakterből
+   - 1, vagy több az előző karakterből
?   - 0, vagy 1 az előző karakterből
^   - sor (karakterlánc) eleje
$   - sor (karakterlánc) vége
Ha a fenti karakterekre szeretnénk rákeresni, azaz szeretnénk, ha például a . valóban egy .-ot jelölne, akkor használhatjuk a \-t, írjuk a karakter elé: \.! Ugye, hogy kezd bonyolódni?
Vegyünk egy bonyolultabb példát, a fentiek szemléltetésére:

foreach (@emailcimek)
  {
  if (/^.+\@.+\....?$/)
    {
    print "A(z) $_ cím egy szabályos e-mail címnek tűnik\n";
    }
  }
A fenti kódrészletben egy e-mail címet ellenőriztünk le, azzal a megkötéssel, hogy azt veszünk szabályos e-mail címnek, amiben van egy @, utána valahol egy pont és utána kettő, vagy három karakter a végén (persze ez egyrészt nem biztos, hogy jó e-mail cím lesz, másrészt pedig vannak már négy betűs végződések is, mint például az "info"). A fentiek jelentése rendre: a sztring eleje (^), egy vagy több bármilyen karakter (.+) egy @ (\@), egy vagy több bármilyen karakter (.+), egy pont (\.), kettő vagy három bármilyen karakter (...?), és végül egy sztring vége ($). Hány sor kellene a mintaillesztő kifejezések használata nélkül, hogy megoldjuk ezt a problémát? Nos, valószínűleg jópár (olyan öt-hat utasítással talán elintézhető másképp is).

A mintaillesztő kifejezések terén el lehetne még kalandozni, sok apróságot, lehetőséget kínálnak még. De most nem fogunk továbblépni, hiszen főként nem a mintaillesztő kifejezések megismeréséért, hanem a Perlben használható lehetőségekért vagyunk itt. Lépjünk hát tovább egy másik irányba!

Helyettesítő operátor

A Perl-ben nem az illesztő operátor az egyetlen lehetőség, ahol mintaillesztő kifejezéseket használhatunk. Következőnek ismerkedjünk meg a helyettesítő operátorral, melynek segítségével egy karakterlánc egyes részeit tudjuk lecserélni az általunk megadottakra.

Vegyünk egy egyszerű példát, cseréljük le az "á" betűket "a'" karaktersorra! Ezt az alábbi kódrész tudja elvégezni:

$line="A szamár egy hálátlan málhás.";
$line=~s/á/a'/g;
Mint látjuk a szintakszisa hasonlít az illesztő operátorhoz, azzal a különbséggel, hogy még egy / került a levesbe. A "g" betű egy paraméter, azt adja meg, hogy "globálisan" kell végrehajtani a cserét, azaz meg sem kell állni addig, amíg a kifejezés első része illeszkedik valahova, mindet le kell cserélni, s nem csak az elsőt. Ebből adódik: elhagyásával csak az első illeszkedés esetén cserél. Ha már ennyit beszéltünk róla, nézzük meg végül, hogy mit is csinál a kódrészlet: az összes "á" karaktert kitörli, majd a helyükre beszúrja az "a'" karaktersort. A kifejezés első része (az első két / között levő) illeszkedni próbál, s sikeres illeszkedés esetén, amire illeszkedett, az lecserélésre kerül a második részben (a két utolsó / közötti részben) levő egyszerű karakterláncra (oda mindent írhatunk, amit két idézőjel közé írnánk).

Split parancs

Talán ez a két operátor az, melyet a legtöbbet használhatunk a mintaillesztő kifejezéseknél, de ezen kívül létezik még egy parancs, a split. Segítségével mintaillesztő kifejezések mentén vághatunk több részre egy karakterláncot. A szintaktikája egyszerű, első paramétere a mintaillesztő kifejezés, második a karakterlánc, amit darabolni szeretnénk. Íme egy példa:

@elemek=split(/[ ,]+/,"A web  egy olyan jelenség, mely");
Az @elemek tömbbe hat elem fog kerülni, a hat szó, melyet a karakterláncunk tartalmazott. A szögletes zárójel egy új jel, nem szolgál másra, mint egy karakterhalmaz jelölésére, s közöttük "vagy" kapcsolat létrehozására. Azaz a [ ,] jelentése: vagy egy szóköz, vagy egy vessző. A parancs nem fog más csinálni tehát, mint az egy vagy több vessző vagy szóköz karakterek mentén elvágni a karakterláncunkat, vagyis szavakra tördelni. Persze ez a kifejezés így nem teljes, felkészíthetjük tabulátorok, pont és más karakterek kezelésére is, de ez legyen házi feladat.

Mielőtt a további lehetőségeket vizsgálnánk meg, egy trükköt megemlítenék feltétlenül. Előfordulhat, hogy nem csak darabolni szeretnénk, hanem a kifejezésünk által illesztett elemeket is szeretnénk visszakapni, azaz azt is, ami mentén daraboltunk. Ennek első ránézésre nincs sok értelme, de ha belegondolunk, hogy darabolhatunk például HTML elemek mentén, akkor máris értelmet nyerhet ez a kívánság. Ehhez nem kell mást tennünk, mint a kívánt részt zárójelek közé tenni (tehát lehet az egész kifejezést, de annak részeit is). Lehet több zárójelet is használni, akkor több darabban kerül bele a tömbbe. A legegyszerűbb eset:

@elemek=split(/([ ,]+)/,"A web  egy olyan jelenség, mely");
A tömbünkben a következő elemek lesznek: "A", "_", "web", "__", "egy", "_", "olyan", "_", "jelenség", ",_", "mely". Az érthetőség kedvéért a szóközt aláhúzás jellel jelöltem. S ezzel tovább is léptünk, itt az ideje, hogy megnézzük, hogy mire jó a zárójelezés a Perlben!

Egyéb lehetőségek

Sok egyéb lehetőségünk van, melyek egyrészt a mintaillesztő kifejezések nem ismertetett részeiből adódnak, másrészt pedig kapcsolókból, melyek a kifejezésünk viselkedését befolyásolják. Ezekre itt és most nem fogunk kitérni, de még megnézzük, hogy a zárójelezéssel mit tehetünk.

A zárójelek használata a mintaillesztő kifejezésnél csoportosításra szolgál. Teljes csoportokat zárhatunk egy egységbe, s kezelhetjük ezeket az egységeket egyben, értelmezve rájuk a + és egyéb ismétlődéssel foglalkozó karaktereket. Ezen kívül azonban egy mellékhatása is van a zárójeleknek: ahogy az előbb is láttuk, a Perl az így jelölt illeszkedési részleteket elkezdi nekünk kigyűjteni. Több helyre is. A zárójelezett részek a kifejezés végrehajtása után rendre a $1, $2, $3 stb. változókba kerülnek. Erre egy példa:

if ($szoveg=~/([a-z]+)\@([a-z]+\.[a-z]+)/) {
  print "Van benne (legalább egy) e-mail cím.\n";
  print "Az e-mail cím azonosítója: $1\n";
  print "Az e-mail cím domainje: $2\n";
}
A másik lehetőség a split-hez hasonló, a mintaillesztés során ugyanis a visszatérési értéke lista környezetben a kifejezésnek egy tömb lesz, az összes zárójel közötti résszel:

@emaildarabok=($szoveg=~/([a-z]+)\@([a-z]+\.[a-z]+)/);

Olvasnivaló

A mai anyag biztos vagyok benne, hogy számos kérdést felvet majd az érdeklődőkben. A korábbi Perles cikkekben törekedni igyekeztem arra, hogy mindenre kitérjek, itt ezt meg sem próbáltam, hiszen a téma akkora, amit nehéz lenne egy cikkben körbejárni. Megismerkedtünk a Perl nyelvi lehetőségeivel, de korántsem az összes lehetőséggel, mellyel élhetünk, ha a Perlben mintaillesztő kifejezést használunk.

A témáról több helyen is lehet bővebben olvasni, az érdeklődőknek a Perl dokumentációt tudom javasolni, ahol külön dokumentum szól a mintaillesztő kifejezésekről, a perlre. Ha beírjük Linux alatt a következő parancsot:

perldoc perlre
akkor olvashatunk róla, vagy ha pedig a webről szeretünk olvasni, a perldoc.com címen is megtekinthetjük az írást. Itt már komolyabban és mélyebben is szerepelnek ismeretek a lehetőségekről. A lehetőségekről magyarul is olvashatunk Verhás Péter honlapján, aki egyike a Perlről magyarul elsőként íróknak.

Nos, a mai cikknek a végére érkeztünk, de a cikksorozatnak még nem. A következő cikkekben a Perlben használható fájlkezeléssel, és az ezzel kapcsolatos lehetőségekkel, továbbá alapszinten a Perlben lehetséges adatbáziskezeléssel fogunk megismerkedni.
 
Bártházi András arcképe
Bártházi András
Az Emarsys-nál dolgozik vezető fejlesztőként, és az API-ért, integrációkért felelős termékmenedzserként. Szeret profi csapatban profit alkotni.
1

Confused!

Anonymous · 2005. Dec. 21. (Sze), 13.09

$line="A szamár egy hálátlan málhás.";
$line=~s/á/a'/g;
ezt nem értem ! ~s az mire valo? meg az "a" után egy " ' " !
2

Helyettesítő operátor

Török Gábor · 2005. Dec. 21. (Sze), 13.37
A cikkben a válasz.
A Perlben nem az illesztő operátor az egyetlen lehetőség, ahol mintaillesztő kifejezéseket használhatunk. Következőnek ismerkedjünk meg a helyettesítő operátorral, melynek segítségével egy karakterlánc egyes részeit tudjuk lecserélni az általunk megadottakra.

Vegyünk egy egyszerű példát, cseréljük le az "á" betűket "a'" karaktersorra!