A C# programozási nyelv

Reguláris kifejezések

Ha szöveges állományban szeretnénk keresni valamit, aminek nem tudjuk a pontos alakját, akkor használhatunk reguláris kifejezéseket. Például ha egy .html fájlban két tag közötti részben keresünk valamit („<Font …> … fontos … </Font …>”). Tipikus felhasználása: html fájlok, naplófájlok.

Az illeszkedést vizsgáló motorokat jelenleg háromféleképpen szokták megvalósítani:

determinisztikus véges automata Ezt használja az awk. Lineáris idejű, mivel sosem lépnek vissza az illesztés közben, így egy karaktert sosem vizsgálnak meg kétszer. Az algoritmusból adódóan garantálja a lehetséges leghosszabb illesztést, de nem képes utalások keresésére.
nemdeterminisztikus véges automata Ezt használja a .Net Framework, Perl és Python. Mivel reguláris kifejezés kiterjesztésével próbál illeszteni, így álkifejezéseket és visszautalásokat is képes megtalálni. Visszalépéses algoritmust használ. Ez rosszabb esetben exponenciális futási időt eredményezhet. Miután az első találatot adja vissza, korántsem garantált, hogy amit a talált, az a leghosszabb illesztés.
POSIX nemdeterminisztikus véges automata Nagyban hasonlít az előzőhöz, a különbség: addig folytatja a visszalépést, amíg nem tudja garantálni, hogy a lehető leghosszabb illesztést sikerült megtalálnia.

A reguláris kifejezések egyszerű karaktersorozatok, például ”valami\sfontos”. Ez a kifejezés az összes olyan szövegrészletnek megfelel, ahol a “valami” és a “fontos” között egy darab whitespace karakter áll. Egy reguláris kifejezést tehát egy string-gel hozunk létre. Ezt a string-et string objektumok konkatenációjaként is létrehozhatjuk,pl. ha futási időben dől el a keresési minta, vagy ha fájlból olvassuk ki, vagy a felhasználó írja be közvetlenül azt. Ha a forrásfájlba gépelnénk be a string-et, fordítási hibát kapnánk, mert a „\s” -t értelmezni próbálja az előfordító, mint speciális karaktert. Ilyen esetekre megoldás, ha dupla „\” -t használunk, vagy ha a string elé „@” –ot írunk. Következzen egy egyszerű példa: a „kék” és „kek” szavakat keressük valamilyen környezetben. A Match.Index találat esetén az illeszkedő string első karakterének az index -ét adja vissza.

Regex reg1=new Regex("k(e|é)k");
Regex reg2=new Regex(@"k(e|é)k");
//reg1==reg2
Match m=reg1.Match("kicsi kek virág");
//m.Index==7

Regex reg1=new Regex("\\(k(e|é)k\\)");
Regex reg2=new Regex(@"\(k(e|é)k\)");
//reg1==reg2Match m=reg1.Match("kicsi (kek) virág");
//m.Index==7

Regex reg1=new Regex("\\\\k(e|é)k");
Regex reg2=new Regex(@"\\k(e|é)k");
//reg1==reg2
Match m=reg1.Match("kicsi bordo\\kek virág"); //a memóriába egy „\” kerül
//m.Index==11

A továbbiakban minden karaktersorozaton azt a string -et értem ami elé ki van téve a „@”. A leggyakrabban használt metódusoknak léteznek statikus túlterhelt változatai is, de ezek csak paraméterezésükben különböznek.

Mint ahogy azt a fenti példákban láthattuk, vannak olyan karakterek, amelyeket önmagukkal próbálja illeszteni a keresőmotor, de vannak olyanok is, amelyek egyéb jelentést hordoznak. Ezek az un. metakarakterek.

Speciális karakterek

A következő táblázat tartalmazza a használható reguláris kifejezéseket, azok unicode megfelelőjét és az általa hordozott jelentést.

\a \u0007 Bell
\b \u0008 Szövegben, csere esetén és karakterlistában ([...]) backspace. Keresés esetén szóhatár.
\B   Nem lehet szóhatár a keresésben.
\t \u0009 Tabulátor
\r \u000D Kocsivissza
\v \u000B Vertikális tabulátor
\f \u000A Lapdobás
\n \u000B Új sor
\e \u001B Escape
\0nnn   ASCII-karakter oktális kód alapján.
\xnn   ASCII-karakter hexadecimális kód alapján.
\cn   ASCII-vezérlő karakter (A Control+C gombok lenyomásakor a \cC –nek megfelelő vezérlő karakter továbbítódik ).
\unnnn   Unicode karakter hexadecimális kód alapján.
.   Tetszőleges, pontosan 1 db megjelenítendő karakter.
[abcd…]   Pontosan egy karakter a felsorolás elemei közül.
[^abcd…]   Pontosan egy karakter, ami nem szerepel a felsorolásban.
[0-9a-fA-F]   Pontosan egy karakter, ami szerepel a karaktertartományban.
\w   Szó karakter (betűk és számok) ~ [a-zA-Z0-9]
\W   Nem szó karakter ~ [^a-zA-Z0-9]
\s   White-space karakter ~ [\f\n\r\t\v]
\S   Nem white-space karakter ~ [^\f\n\r\t\v]
\d   Szám karakter ~ [0-9]
\D   Nem szám karakter ~ [^0-9]
^kif   A „kif” reguláris kifejezés a sor elején.
kif$   A „kif” reguláris kifejezés a sor végén.
\Akif   A „kif” reguláris kifejezés a szöveg elején.
Kif\Z   A „kif” reguláris kifejezés a szöveg végén, de lehet utána \n.
Kif\z   A „kif” reguláris kifejezés a szöveg végén.

A speciális jelentésű karakterek átadására a "\" karaktert eléírva van lehetőség.

A fentieken kívül reguláris kifejezésben használhatunk még un. mennyiségjelzőket is:

* A megelőző kifejezésből tetszőleges mennyiség.
+ A megelőző kifejezésből legalább egy.
? A megelőző kifejezésből legfeljebb egy.
{n} A megelőző kifejezésből pontosan n.
{n,} A megelőző kifejezésből legalább n.
{n,m} A megelőző kifejezésből legalább n, de legfeljebb m.

Csoportosítás

Amennyiben a kifejezésünkben csoportokat (a Group osztály valósítja meg) hozunk létre, minden egyes csoport egy külön találatnak (Match osztálynak) számít (a Match osztály a Group –ból származik). A teljes reguláris kifejezés Match metódusa által visszaadott Match osztály is egy csoport. Ennek az osztálynak a Groups tulajdonságával egy GroupsCollection típusú tömböt kapunk vissza, aminek az első eleme maga a Match típusú objektum, vagyis a teljes találatot reprezentáló objektum. A Group osztálynak van egy Captures tulajdonsága mely egy CaptureCollection típusú tömböt ad vissza (a Group pedig a Capture –ből származik), mellyel lekérdezhetjük, hogy az adott csoportot pontosan hányszor és hol találta meg, de csak a teljes találaton belül.

Csoportosításokat a ( ) (gömbölyű zárójelek) segítségével hozhatunk létre. Egy egyszerű csoportosítás például a (\s)(kek). Ez, megtalálja a szövegben a „kek” részletet, ha előtte whitespace állt. A nullás csoport lesz az egész reguláris kifejezés, az egyes a white-space a kettes pedig a „kek” kifejezés.

A rendszer a nem nevesített csoportokat automatikusan sorszámozza, így lehet rájuk hivatkozni.

Nevesített csoportok

Bonyolultabb minták esetén a rendszer automatikus csoportszámozása rontja az áttekinthetőséget, átrendezhetőséget. Lehetőségünk van a csoportjainkat elnevezni.

A következő példa bemutatja, hogy a „kif” reguláris kifejezést hogyan tudjuk „csoportnev” névvel elnevezni:

(?<csoportnev>kif)

Tartomány jelölése csoportokkal

Speciális szintaktikája van, nem keverendő össze az előzővel. A nevesített csoportokat használhatjuk könyvjelzőként is, amennyiben tól-ig szövegrészletet szeretnénk illeszteni. Ehhez két csoportot kell definiálni: az elsőt, amelyik az illesztés kezdetét-, illetve a másodikat, amelyik a végét jelöli. Az első csoport szintaktikájában semmi szokatlan nincs:

(?<csoportnev1>kif)

A második csoport is csak kissé különbözik:

(?<csoportnev2-csoportnev1>kif)

A csoportnev2 név alatt fogjuk tárolni a két csoport közötti szövegrészletet. A csoportnev1 definiálja a könyvjelzőt, azaz azt a pontot, ahonnan az illesztést elkezdjük. Mivel ez ténylegesen csak könyvjelző szerepet tölt be, kevésbé lényeges és a Groups tömbben sem lesz benne.

Példa:

(?<bookm>\.).*(?<important-bookm>!!)

Ha ezt illesztjük a “Valami. De ezt el ne felejtsd!!” karakterláncra, akkor a találatban szereplő Groups tömb két elemet fog tartalmazni: a teljes találatot vagyis „. De ezt el ne felejtsd!!” és a két csoport közti szövegrészletet, vagyis „ De ezt el ne felejtsd”.

Nem tárolt csoportok

Sokszor van szükség arra, hogy pozicionálás, vagy megfelelő előfeltételek kialakítása végett speciális csoportot hozzunk létre. Természetesen ennek kimenetére nincsen szükségünk, hiszen csak az illesztendő szöveg megkeresésében játszik szerepet. Ha azt szeretnénk, hogy a csoport kimenete ne kerüljön tárolásra, jelöljük a csoportot a következőképpen:

(?: … )

Például a (?:ha)(rom) kifejezést illesztve a “harom” –ra mindössze két csoport lesz a találatban. A teljes illesztés (“harom”) és a tárolt csoport (“rom”). Ez a tulajdonság nem öröklődik tovább az egymásba ágyazott reguláris kifejezéseken, ezért ha meg szeretnénk keresni a „kek ibolya” és „kék ibolya” részletet de nem szeretnénk eltárolni a színt, a következőképpen tehetjük meg:

(?:k(?:e|é)k\s)(ibolya)

Feltételcsoportok

Azokban az esetekben, amikor egy szöveg egy adott kontextusában keresünk valamilyen részletet, de arra nem vagyunk kíváncsiak a továbbiakban, jól alkalmazhatóak a feltételcsoportok.

kif(?= … ) Nulla hosszúságú, pozitív, előretekintő feltétel. Csak azokat a mintákat keresi, amelyek az adott csoportban folytatódnak. Valami(?=\sAmerika) a Valami szót találja meg, ha utána az Amerika szó áll.
(?<= … )kif Nulla hosszúságú, pozitív, visszatekintő feltétel. Csak azokat a mintákat keresi, melyek az adott csoporttal kezdődnek. (?<=Valami\s)Amerika az Amerika szót találja meg, ha előtte a Valami szó áll.
kif(?! … ) Nulla hosszúságú, negatív, előretekintő feltétel. Csak azokat a mintákat keresi, amelyek nem az adott csoportban folytatódnak. Valami(?!\sAmerika) a Valami szót találja meg, ha utána nem az Amerika szó áll.
(?<! … )kif Nulla hosszúságú, negatív, visszatekintő feltétel. Csak azokat a mintákat keresi, amelyek nem az adott csoporttal kezdődnek. (?<!Valami\s)Amerika az Amerika szót találja meg, ha előtte nem a Valami szó áll.

Visszautalás

A visszautalás (Backreferences) segítségével egy előzőleg megtalált kifejezést használhatunk fel újra, például a (k(e|é)k)\1 megtalálja azokat a részeket ahol a „kék” szó kétszer le van írva ugyanolyan alakban. „kekkék” –et nem fogja megtalálni. A „\1” visszautalás az elsőként megtalált csoportra (zárójelek közti kifejezés)(a keretrendszer a névtelen csoportokat automatikusan 1-től indexeli).

A visszautalásokat kétféleképpen végezhetjük: az egyik módszer, mint ahogy azt a példánkban is láthattuk, „\szám” forma használata. 0 és 9 között ezt a rendszer mindenképen visszautalásnak tekinti, míg 10 -től csak akkor, ha található a számnak megfelelő csoport, egyébként oktális értékként dolgozza fel. Ha a csoportunkat elnevezzük (?<csoportnev>kif), a visszautalásnak az elnevezés alapján kell történnie. Ha számmal neveztük el (azaz a csoportnev egy szám), akkor a visszautalás ugyanúgy „\szám” alakban történik.

Amennyiben a csoportnev nem szám, hanem szöveg, a visszautalásnak \k<csoportnev> formában kell történnie. Az előző példa alapján: (?<csop>k(e|é)k)\<csop>.

A \k<csoportnev> számokkal is használható, ha a csoportnev helyett számot írunk.

Behelyettesítés

A | (pipe) jelet alkalmazva definiálhatunk alternatív ágakat is, mint ahogy azt az előző példákban is láthattuk.

Feltételes behelyettesítés

A szintaktikája a következő:

(_(csoportnev)igen|nem)

Amennyiben a csoportnev nevű kifejezést sikerült illeszteni, akkor az „igen” kifejezéssel folytatja tovább, egyébként a „nem” -mel. Ha nem írunk „nem” ágat, de szükség lenne rá, akkor a keresés leáll.

Csere

Lehetőség van a szöveg változtatására is a reguláris kifejezés találatai alapján. Kétféle módszer van: az egyikben megadjuk a reguláris kifejezést, és azt, hogy mire cseréljük. Lehetőség van a következő metakarakterek használatára:

$n Behelyettesíti az adott számmal jelölt csoportot, amelyet a keresés közben talált.
${nev} Behelyettesíti a „nev”-vel jelölt csoportot, amelyet a keresés közben talált.
$$ Maga a „$” karakter.
$& Magát a találatot helyettsíti be.
$` A találat előtt található egész szöveget helyettesíti be.
$’ A találat után található egész szöveget helyettesíti be.
$+ A legutolsó megtalált csoportot helyettesíti be.
$_ A teljes bemeneti szöveget helyettesíti be.

A másik lehetőség, hogy megadunk egy visszahívható (callback) függvényt, amely illeszkedés esetén meghívódik és a visszatérési értékét behelyettesíti.

Példa:

Regex reg=new Regex(@"k(e|é)k");
reg.Replace("Kék ég, kek Föld",new MatchEvaluator(evaluator) );

static string evaluator(Match m){
 if( m.Value=="kek" ){
  return "kék";
 }else{
  return "elírt kék";
 }
}

Split

Akkor lehet hasznos, ha nagyobb szöveget kisebb egységekre szeretnénk bontani. Például egy olyan napló fájl amelynek a bejegyzései határainál dátumok állnak. Mindenhol más dátum áll, csak a formáját ismerjük. Az ilyen esetekre ad tökéletes megoldást a reguláris kifejezés szerinti szegmentálás (Regex.Split()). Ez string tömböt ad vissza, aminek elemei a blokkok. Ha a reguláris kifejezést csoportként definiáljuk (zárójelbe rakjuk) a kimenetben az elválasztó szöveg is benne lesz.

Példa:

Regex reg=new Regex(@"kek");
reg.Replace("A kek ég, és a kek Föld");
[0]"A "
[1]" ég, és a "
[2]"Föld"

Reguláriskifejezés-opciók

Amennyiben valamilyen opció szerint szeretnénk futtatni a mintaillesztést, háromféle lehetőségünk van:

A lehetséges opciók RegexOption neve, majd a reguláris kifejezésben használható kapcsolója és leírása:

IgnorCase I A kis és nagy betűk között nincs különbség.
Multline M A „^” és a „$” jelek a sorok elejét/végét is megtalálják, nemcsak a szöveg elejét/végét.
ExplicitCapture N Minden csoport nem tárolt csoport, kivéve a nevesítetteket.
Compiled - A reguláris kifejezésből MSIL-kódot fordít.
Singleline s A „.” minden karakterre passzol, beleértve a “\n”-et is.
IgnorePatternWhitespace x A reguláris kifejezésben figyelmen kívül hagyja a nem metakarakterekkel definiált szóközöket.
RightToLeft - A szöveg feldolgozása jobbról balra történik.
ECMAScript - ECMAScript kompatibilis feldolgozást eredményez. Csak a Multiline és IgnorCase opciókkal lehet együtt használni.

Teljesítmény

Reguláris kifejezést illeszteni sokkal lassabb, mint hagyományos karakterláncot keresni, ezért ha csak a „kék” szó ékezetes és anélküli változatát keressük reguláris kifejezéssel, az 5-10 –szer lassabb, mintha egy sima string keresést hajtanánk végre kétszer. Azonban lehetőség van egy igen hatásos gyorsításra, ha szerelvénnyé fordítjuk. Ezt úgy kell elképzelni, hogy nem az az algoritmus fut le, ami eddig, hanem először kitalál egy algoritmust a konkrét reguláris kifejezéshez, és azt lefordítja. A fordítás sokáig tart, de nagy input-nál jelentős gyorsulás várható. Ezt kétféleképpen tehetjük meg:

Példa:

// Először elkészítjük az assembly-t amelyben a MyNamespace névtérben lesz egy regkif osztály. Ezt elmentjük forditottReg.dll néven:
string reg="k(e|é)k";
RegexCompilationInfo[] compinfo= new RegexCompilationInfo[]
 {new RegexCompilationInfo(reg, RegexOptions.None, "regkif", "MyNamespace", true) };
AssemblyName asname=new AssemblyName();
asname.Name="ForditottReg";
Regex.CompileToAssembly(compinfo,asname);

// Majd az alábbi módon hivatkozhatunk rá, ha a kész szerelvényre adtunk egy referenciát (Visual Studio-ban a solution manager references –re kattintva jobb gombbal).
string s="Az a kek eg";
MyNamespace.regkif regk=new MyNamespace.regkif();
Match m=regk.Match(s);