Segítség a RegEx használatához
A RegEx vagy RegExp a Regular Expression rövid formája. Magyarul talán a "szabályos kifejezés" lenne a megfelelő fordítás. Ez a leírás azért született, hogy elindítson a technika megismerésében, koránt sem tekinthető teljesnek, a cikk végén található URL-eken lehet folytatni a barangolást a "szabályos kifejezések" világában.
Mindig babonás félelemmel néztem a RegEx mintáira. Volt dolgom egykét nyelnvel, de ez valami egészen bizarr dolog volt. Nem hinném, hogy létezik olyan tapasztalat aminek birtokában felfedezhető a RegEx sajátos logikája, szintaktikája, viszont az operátorok elolvasása után szinte arculcsap a felismerés: "ennyi az egész?" Legalább is ez lenne a cikk célja :)
A RegEx lehetőséget ad szabályok, azaz minták egyszerű leírására. Ezekkel a mintákkal aztán sok hasznos dolgot tehetünk. Kereshetünk rájuk egy stringben, vagy kicserélhetjük őket valamilyen szabály szerint. Használhatjuk adatellenőrzésre vagy szerkezetek (pl. dátum) szétdarabolására, értelmezésére.
Essünk túl a kötelező analógián: a DOS-ból jólismert joker karakterek is kifejezéseket írnak le, amiknek fájlokat feleltetünk meg, vagy van egyezés, vagy nem. A ka*.doc és ka???.doc kifejezések közül a kalap.doc mindkettőnek, még a kapa.doc csak az elsőnek felel meg.
Hogy még egy kicsit rosszabb legyen, mielőtt jobb lesz: DOS-os ka*.doc RegEx megfelelője: ka.*\.doc a ka???.doc peig nem más, mint ka.{3}\.doc
RegEx operátorok
A DOS-os példához hasonlóan a mi mintáink is konkrét karakterekből (szavak, szótöredékek), és speciális jelentésű operátorokból épülnek fel.
Karakter megfeleltetés
- . (pont)
- Bármilyen karakter: A b.ka kifejezésnek megfelel a béka és bika szó is.
- [karakterek]
- A kapcsoszárójelek között felsorolt karakterek valamelyikével megegyező karakter: A b[éa]ka kifejezésnek megfelel a béka és baka szó, a bika viszont nem. A - (minusz) jellel tartományt is megadhatunk. Például [0-9] megfelel bármely számjegynek vagy [a-zA-Z] bármely kis vagy nagybetünek.
- [^karakterek]
- A kapcsoszárójelek között felsorolt karakterek egyikével sem egyező karakter (az előző operátor tagadása): A b[^é]ka kifejezésnek nem megoldása a béka. A baka és bika viszont igen.
Többszörözés
- ?
- A megelőző minta 0 vagy 1 alkalommal fordul elő: A borda.? kifejezés igaz a borda és a bordal szavakra is.
- +
- A megelőző minta 1 vagy több alkalommal fordul elő: A bo.+ka kifejezésnek megfelel a boróka, a boka viszont nem.
- *
- A megelőző minta 0 vagy több alkalommal fordul elő: A bo.*ka kifejezésnek már megfelel a boka és boróka is.
- {m,n}
- Segítségével megadható minimum és maximum vagy pontosan megadott számú előfordulás - {3} pontosan 3 előfordulás; {3,} legalább 3 előfordulás; {2,5} legalább 2 legfeljebb 5 előfordulás; {,10} legfeljebb 10 előfordulás. A d.{,5}ány igaz minden esetben, ha legfeljebb 5 karaktert kell helyettesíteni, például a dolmány esetén. A diszkópatkány viszont már nem akad fenn rajta.
Horgonyok
Az előzőekben nem szemléltettem, de a felsorolt kifejezések akkor is igazak ha a vizsgált string belsejében találhatók meg. A b.ka igaz a bikaviadal mintára is.
- ^
- A minta eleje: Ezzel jelezhetjük, hogy a kifejezést a minta elején keressük. A ^béka kifejezésnek megfelel a békanyál minta, a kecskebéka viszont nem.
- $
- A minta vége: Az előző horgonyhoz hasonlóan a minta végét testesíti meg. A ék$ mintának megfelel minden erre végződő szó (kerék, pék).
Természetesen kombinálhatók is. A ^p.k$ kifejezés csak akkor igaz, ha az input pontosan egy hárombetűs szó. A legpikánsabb nem megoldása, ahogy a pikáns sem. A pék vagy pók viszont jó.
Logika
- |
- Vagylagos egyezés: Két lehetőség közé téve bármelyikkel való egyezés találatot produkál. Gyakorlati példához picit előre kell ugorjunk, a normál (kerek) zárójelekre, jelen felhasználás viszont nem kíván különösebb magyarázatot: ka(lap|bát)
- ( )
- Kifejezések csoportosítása: Nem csak a vagylagos egyezés az egyetlen lehetséges felhazsnálás. Egy csoportot létrehozva elláthatjuk paraméterrel például a (hókusz)?pók segítségével a hókuszpók és pók szavak is megtalálhatók. A csoportokra később hivatkozhatunk is, ez cserénél vagy stringek értelmezésénél lesz hasznos.
Escape-elés
Ezek az operátorok lefednek néhány gyakran használt karaktert is. Ha például egy pont karaktert nem speciális értelmében szeretnénk hazsnálni, hanem egy pontként a C-ból, PHP-ből, Javascriptből megszokott módon \ (backslash) karakterrel tudjuk megfosztani speciális jelentésétől.
Összetett példák
Dátum feldolgozás
[0-9]{4}[^0-9]*[0-9]{2}[^0-9]*[0-9]{2}
Hogy is olvasandó a példa? [0-9]{4} négy darab számjegy (év); amelyet követ [^0-9]* nulla vagy akár több NEM számjegy karakter (esetleges elválasztó); majd [0-9]{2} két számjegy (hónap); utána ismét [^0-9]* jöhet elválasztó; és végül [0-9]{2} két újabb számjegy.
Ezzel még csak félmunkát végeztünk. Tudunk azonosítani egy dátumot, de nem tudunk hivatkozni az egyes tagokra. A kerekzárójellel emeljük ki azokat a csoportokat amik számunkra érdekes adatokat hordoznak.
([0-9]{4})[^0-9]*([0-9]{2})[^0-9]*([0-9]{2})
A használt nyelvnek megfelelő stílusban (általában egy tömbben) kapjuk vissza a megjelölt csoportok tartalmát a RegEx futtatása után. Például PHP-ben:
$datum = "2006. 07. 22.";
ereg ('([0-9]{4})[^0-9]*([0-9]{2})[^0-9]*([0-9]{2})', $datum, $talalat);
var_dump ($talalat);
Eredménye a következő:
array(4) {
[0]=>
string(12) "2006. 07. 22"
[1]=>
string(4) "2006"
[2]=>
string(2) "07"
[3]=>
string(2) "22"
}
A tömb nullás sorszámú rekeszébe kerül az egész kifejezésnek megfelelő minta (látható, hogy a végső pont nincs benne, hiszen a keresett kifejezésünk véget ér a napot jelölő két számjeggyel). Az egyes sorszámtól kezdve a tömbbe kerültek az általunk (kerekzárójellel) megjelölt csoportok. A kiindulási dátum lehetett volna "2006-07-22" vagy "20060722" is, a kifejezés mindet megfejti.
Bizonyos dátumformátumok nem pótolják ki kétszámjegyűre az adatokat. Pédlául "2006/7/22". Ilyenkor az elválasztók jelenléte a döntő. Az elválasztók között mindig van egy vagy több számjegy:
([0-9]{4})[^0-9]*([0-9]+)[^0-9]*([0-9]+)
Nézzünk egy PHP példát a csrére. A következő függvény a tetszőlegesen elválasztott dátumformátumot "-" jellel elválasztottra konvertálja (ahogy például a MySQL szereti).
$datum = "2006. 7. 22.";
$ujdaum = ereg_replace ('([0-9]{4})[^0-9]*([0-9]+)[^0-9]*([0-9]+)[^0-9]*', '\1-\2-\3', $datum);
echo ($ujdaum); // eredménye: 2006-7-22
Itt megtoldottuk a mintát egy [^0-9]* kóddal, hogy lenyeljük az esetleges elválasztó jeleket a dátum végéről. A \1 \2 \3 pedig referenciák a zárójellel kiemelt csoportokra.
E-mail ellenőrzés
^[0-9a-z\.-]+@([0-9a-z-]+\.)+[a-z]{2,4}$
Olvassuk el: ^ a minta elején; egy vagy több alfanumerikus karakter, a pont és kötőjel is megengedett (A pont speciális karakter, egy backslash-sel jelöljük, hogy nem szeretnénk, hogy most értelmezze a RegEx motor. A minusz szintén speciális jelentéssel bír a kapcsoszárójelek között. Az ő esetében a backslash-módszer néhány értelmezőnek gondot okoz, ezért hogy megfosszuk speciális jelentésétől az utolsó kell legyen a záró kapcsoszárójel előtt.) Ezek után egy @ (at) karakter következik; majd [0-9a-z-]+ egy vagy több alfanumerikus karakter; amit \. egy pont követ. A mintát egy [a-z]{2,4} legalább 2 legfejlebb 4 betüből álló rész (TLD) zárja $.
A kifejezés közepén (kiemelt rész) képezünk egy csoportot, aminek engedélyeztük az ismétlődést. Ennek hatására nem csak a "user@domain.tld" felel meg, hanem ugyanúgy a "user@subhost.host.domain.tld" is.
$email = "gipsz.jakab@szerver.szervezet.hu";
$megfelel = eregi ('^[0-9a-z\.-]+@([0-9a-z-]+\.)+[a-z]{2,4}$', $email);
echo ($megfelel); // az eredmény: 1
Most nem értelmezünk, darabolunk, cserélünk, egyszerűen csak ellenőrizzük megfelel-e a vizsgált minta a kifejezésünknek. Az eregi függvény a kis és nagybetüket nem különböztei meg, és nekünk most erre van szükségünk.
Ugyanerre egy JavaScript példa:
A példa forrása:
<script type="text/javascript">
function mailcheck() {
var reg = /^[0-9a-z\.-]+@([0-9a-z-]+\.)+[a-z]{2,4}$/i;
var s = document.getElementById('email').value;
alert(reg.test(s));
}
</script>
<input type="text" id="email"/>
<input type="button" onclick="mailcheck();" value="Email?"/>
A JavaScript a PERL kompatibilis szintaxist használja (PCRE). Ez nem string. Idézőjelek helyett "/" határoló karakterek között (itt más határoló nem használható) található a minta, utána a flagekkel. Itt az i, azaz igonore-case flaget használjuk, azaz kis és nagybetük nincsenek megkülönbeztetve. A reg tipusa tehát nem String, hanem RegExp. Ennek hazsnáljuk a test() metódusát az input ellenőrzésére.
URL formázott kiírása
Tételezzük fel, hogy egy webcímet szeretnénk tetszetős formában kiírni: kiemelni a domainnevet és elávolítani az esetleges / jelet a végéről. Mivel nem tudjuk, hogy konkrétan van-e a végén / (per) karakter szükségünk lesz egy feltételes kifejezésre. Az első kézenfekvő megoldás:
://([^/]+)(.*)(/$|$)
A :// a protokol végét jelöli, semmi speciális. A ([^/]+) kifejezés megtalálja nekünk a hostnevet, mivel az első / előtti összes karaktert visszaadja. Az elérési út többi tagja megfelel a (.*) kifejezésnek. Ezután pedig vagy egy / karakter majd a string vége következik, vagy azonnal a string vége. Logikusan hangzik, mégsem működik.
A probléma az, hogy a (.*) elnyeli a záró / karaktert is. Így az utolsó csopotnak már csak string vége jel jut. A megoldás:
://([^/]+)(.*?)(/$|$)
Ezt nevezik nem-kapzsi (non-greedy) keresésnek. Ilyenkor a lehető legkevesebb karaktert vesz le magának a kifejezés. Amint az utána következő kifejezés (/$|$) egyezést produkál nem eszik meg többet magának.
A PHP ereg függvényei nem ismerik ezt a keresési módot. Éles helyzetekben mindenképpen a gyorsabb PCRE függvénycsaládot javaslom. A példákban csak a picit barátságosabb szintaktika miatt szerepel ereg. Lássuk most a példát preg (PCRE) használatával:
$url = "http://domain.tld/dokumentumok/2006/";
preg_match ('#://([^/]+)(.*?)(/$|$)#', $url, $talalat);
var_dump ($talalat);
A legszembetűnőbb újdonság az, hogy a PCRE függvények határoló karakterek (esetünkben kettőskereszt: #) között várják a kifejezést (erről később bővebben). A határoló karaktert mi választjuk meg. Általában a per karakter használatos, esetünkben viszont nem lenne célszerű, mivel a mintánkban több helyen is szerepel. Ha a választott határoló karakter szerepel a mintában, akkor a már ismertetett módon, egy backslash karakterrel kell escape-elni azt.
A futás eredménye:
array(4) {
[0]=>
string(32) "://domain.tld/dokumentumok/2006/"
[1]=>
string(10) "domain.tld"
[2]=>
string(18) "/dokumentumok/2006"
[3]=>
string(1) "/"
}
Ahogy megszoktuk, a tömb nullás sorszámú eleme az egész találatot tartalmazza, az általunk megjelölt csoportok az első indextől kezdődnek: A protokoll utáni első "/" karaker előtti rész, azaz a hostnév; majd az URL további része; kivéve a záró "/" karaktert, ami az utolsó csoport.
A darabok ismeretében kiírhatjuk emberbarát, style-olt URL-ünket, ahol a hostnevet tetszőlegesen kiemelhetjük.
echo ('<span class="url-host">' . $talalat[1] . '</span><span class="url-path">' . $talalat[2] . '</span>');
Bankszámlaszám ellenőrzése
^[0-9]{8}([ -]?[0-9]{8}){1,2}$
A bankrendszer behatóbb simerete nélkül feltételezem, hogy egy számlaszám 16 vagy 24 számból áll, és nyolcasával szokás őt elválasztani.
Olvassuk el: nyolc számjegy (eddig semmi különös); majd jöhet (kérdőjel, azaz 0 vagy 1 darab előfordulás) egy elválasztó karakter (space vagy kötőjel); ezután ismét nyolc számjegy. Az utolsó két kifejezésből létrehozunk egy csoportot (kiemelt rész - kerek zárójel), amiből legalább egy (16 számjegyű számlaszám) vagy legfeljebb kettő (24 számjegyű számlaszám) fordulhat elő. A kifejezés a start "^" jellel kezdődik és a vége "$" jelle fejeződik be, így teljes egyezést viszgálunk, nem elég, ha a minta csak tartalmazza a számlaszámot (enélkül például a "abc12345678-12345678" is megoldás lenne).
Egy működő JavaScript példa:
És a forrás:
<script type="text/javascript">
function szamlaCheck() {
var reg = /^[0-9]{8}([ -]?[0-9]{8}){1,2}$/;
var s = document.getElementById('szamlaszam').value;
alert(reg.test(s));
}
</script>
<input type="text" id="szamlaszam"/>
<input type="button" onclick="szamlaCheck();" value="Számlaszám?"/>
További ötletek?
Kimerítettem minden eszembe jutó példát. Ha van egy (nem nagyon speciáis) felhasználásod, amire nem találod a megoldást (vagy akár igen), és jól mutatna itt példának várok minden ötletet a kapcsolat oldalon.
Külső linkek
- Wikipédia sok részletre kitejedő leírása
- PHP Pearl kompatibilis RegEx funkciók javasolt
- PHP POSIX RegEx funkciók beépített, de lassabb
- Szabványos JavaScript RegEx (azaz RegExp) objektum a Mozilla dokuemtációjából