Programujeme v jazyku C# III. Diel 7. – Regulárne výrazy II

Michal Čižmár  /  27. 06. 2007, 00:00

V dnešnom dieli Vám ukážem ako vytvoriť predkompilované regulárne výrazy a zároveň sa pozrieme ako je to potom s rýchlosťou takéhoto regEx. V druhom príklade je jednoducho vysvetlené, ako parsovať grupy údajov z textu pomocou regEx, čo je často veľmi užitočné.

[Kľúčové slová]
Compiled Regular Expression, Speed up bechmark, parsing data with RegExp
Veľa ľudí rozmýšľa, pred tým ako použije regulárny výraz vo svojej aplikácii, ako sa to odzrkadlí následne na výkone (rýchlosti) spracovania, vyhľadávania dát. Preto som spravil malý experiment, kde porovnávam :

1. Klasické overovanie dát pomocou funkcionality, ktorú nám poskytuje trieda String.
2. Pomocou klasických regulárnych výrazov
3. Použitím predkompilovaných regulárnych výrazov

>>Rýchlostné porovnanie použitia regulárnych výrazov

Nasledujúci príklad (vychádza z príkladu z predchádzajúceho dielu) má dva účely: ukázať vám, ako si prichystať predkompilovaný regulárny výraz a potom ho použijeme ne porovnanie s klasickým regEx atď.. veď čítajte ďalej.
Je dosť dlhý, ale v tomto prípade som uvážil, že je to vhodnejšie, ako robiť dva menšie príklady.

Príklad 3.7.1

using System;
using
System.Text.RegularExpressions;
using
System.Reflection;

namespace
PerformanceTest{
class
MainClass
{
    private static int IP2TEST = 400000 ;
    private static IpRegEx regExCom;
    private static Regex regExp;
    private static string pattern = @"^([01]?dd?|2[0-4]d|25[0-5])." +
        @"([01]?dd?|2[0-4]d|25[0-5]).([01]?dd?|2[0-4]d|25[0-5])." +
        @"([01]?dd?|2[0-4]d|25[0-5])$";
//------------------------------------------------
public static void Main(string[] args)
{
   
    //CreateCompiledRegEx(); Raz odkomentuj
    //Klasicke STRING vyhodnocovanie
    string [] addresses = GenerateIpAddres(IP2TEST);
    DateTime start = DateTime.Now;
    for(int i =0;i< IP2TEST; i++)
        TestIpAddr(addresses[i]);

    DateTime end = DateTime.Now;
    TimeSpan rozdiel = end - start;
    Console.WriteLine("Trvanie:" + rozdiel.TotalMilliseconds);

    //Klasicke RegEx vyhodnocovanie
    regExp = new Regex(pattern);
    start = DateTime.Now;
    for(int i =0;i< IP2TEST; i++)
        TestIpAddrRegEx(addresses[i]);

   
end = DateTime.Now;
   
rozdiel = end - start;
   
Console.WriteLine("Trvanie:" + rozdiel.TotalMilliseconds);

    //Kompilovane RegEx vyhodnocovanie
    regExCom = new IpRegEx();
    start = DateTime.Now;
    for(int i =0;i< IP2TEST; i++)
        TestIpAddrRegExCom(addresses[i]);

    end = DateTime.Now;
    rozdiel = end - start;
    Console.WriteLine("Trvanie:" + rozdiel.TotalMilliseconds);

    Console.ReadLine();
}
//------------------------------------------------
public static void CreateCompiledRegEx()
{

    //Tato cast, vygeneruje DLL so skompilovanym RegEx
    //lenze az po prvom spusteni, sa moze pridat ako referencia dll


    RegexCompilationInfo rci = new RegexCompilationInfo(
    pattern, RegexOptions.Compiled,
        "IpRegEx", "PerformanceTest", true);

    AssemblyName an = new AssemblyName();
    an.Name = "IpRegEx";

    //Skompilovanie   
    RegexCompilationInfo[] rciList = { rci };
    Regex.CompileToAssembly(rciList, an);
}
//------------------------------------------------
private static string [] GenerateIpAddres(int count)
{
    Random rnd = new Random(DateTime.Now.Second);
    string [] adresses = new string[count];
    int p1,p2,p3,p4;
    for(int i =0; i < count; i++)
    {
        p1 = rnd.Next(400);
        p2 = rnd.Next(400);
        p3 = rnd.Next(400);
        p4 = rnd.Next(400);

        adresses[i]= p1.ToString()+ "."+
        p2.ToString() + "." +
        p3.ToString() +"."+
        p4.ToString();
    }
return adresses;
}
//------------------------------------------------
public static bool TestIpAddr(string addr)
{
    char [] separator = new Char[]{'.'};
    string [] parts = addr.Split(separator);
    if (parts.Length!=4) return false;

    int onePart = 0;
    try{
    for(int i = 0; i < 4; i++)
    {
        onePart = int.Parse(parts[i]);
        if(onePart<0 || onePart>255) return false;
    }
    }
catch(Exception)
    {
    return false;
    }
return true;
}
//----------------------------------------------------
public static bool TestIpAddrRegExCom(string addr)
{
    return regExCom.IsMatch(addr);
}
//----------------------------------------------------
public static bool TestIpAddrRegEx(string addr)
{
    return regExp.IsMatch(addr);
}
//----------------------------------------------------
}
}

>>Ako sa robí predkompilovaný regulárny výraz?

Najprv si musíme uvedomiť, ako funguje vnútorný mechanizmus klasických regulárnych výrazov. Tým, že napr. pri vzniku inštancie triedy RegExp jej zadáte samotný regulárny výraz, prebehne vnútorný proces, ktorý tento vstupný string preloží do špeciálneho medzi-jazyka, ktorý je optimálny pri práci s RegEx – teda sa jednoduchšie vyhodnocuje, ale pozor nie je to MSIL.
Potom pri každom vyhodnocovaní regulárneho výrazu sa ešte prekladá tento medzi jazyk a už asi tušíte, kde sa strácajú CPU cykly.


Takže si treba dobre rozmyslieť, v akom type aplikácie sa chystáte použiť regulárny výraz. Či skôr vyžadujete prehľadnosť a jednoduchosť alebo rýchlosť.

.NET (ale napr. aj Java) nám ponúkajú ale kompromis medzi týmito dvoma voľbami vo forme predkompilovaných regEx. Všimnite si metódu CreateCompiledRegEx().
Myslím, že príklad je jasný. Podstatní je to, že takto vygeneruje samostatné assambly vo forme DLL. Takže tentoraz sa ozaj regulárny výraz rozloží do jazyka MSIL a obsahuje algoritmus len pre vyhodnocovanie jedného regulárneho výrazu.

Vhodné je na takéto regExps založiť samostatný projekt a pripojiť ho k solution. Vpríklade to vôbec nie je spravené elegantne, pretože najpr je potrebné spustit program tak, že sa zavolá len spomínaná metóda
a z toho vznikle DLL. Potom možeme jej volanie zakomentovať a vygenerovné DLL pripojiť cez references->Add->Browse a vyhldať súbor dll.

Použitie
je veľmi jednoduché, pretože ste si nachystali vlastnú triedu, ktorá ma všetky vlastnosti ako trieda Regex až na to, že vyhodnocovaný regExp je v nej na pevno zadaný. Trieda má také meno, aký ste zadali parameter pri vytváraní
RegexCompilationInfo, v našom prípade IpRegEx

>>Ako prebehlo testovanie?
Všimnite si, že v príklade je metóda [] GenerateIpAddres(int ), ktorá podľa zadanej hodnoty vygeneruje daný počet náhodných IP adries (v našom prípade 40 000).
Klasické overovanie som spravil podľa svojho uváženia a nehovorím, že by to nešlo spraviť ešte rýchlejšie.


>>Výsledky .
Graf hovorí za všetko.
Graf

Údaje sú v milisekundách.
- MODRÁ –
klasické vyhodnocovanie podľa vlastného algoritmu
- ČERVENÁ –
pomocou regulárneho výrazu
- ŽLTÁ -
použitý ten istý, ale tentoraz predkompilovaný regEx.



>>Parsovanie grúp údajov pomocnou RegExp.

Na koniec si pozrite tento príklad. Dosť to používam a možno sa to hodí aj Vám.
Pomocou syntaxe (?<group_name> pattern ) môžeme určité časti vyťažovaných dát pomenovať (dá sa k nim pristupovať aj pomocou indexov a nie mien, ale takto je to elegantnejšie).
V príklade potrebuje rozdeliť vstupný údaj na prehľadný dátum a čas. Tu by nám nepomohla ani trieda DateTime. Ak by Vám niečo nebolo v príklade jasné, napíšte prosím do diskusie, myslím si, že je dosť jedno-jednoznačný ;-) príklad.

Príklad 3.7.2

using System;
using
System.Text.RegularExpressions;
class
MainClass
{

    public
static void Main(string[] args)
    {
        //11.08.2007 8:10
        string input = "230820070825";
        string pattern =
        @"^(?<den>d{2})(?<mesiac>d{2})"+
        @"(?<rok>d{4})(?<hodina>d{2})"+
        @"(?<minuta>d{2})$";
        Regex regExp = new Regex(pattern);
        if(regExp.IsMatch(input))
        {
            MatchCollection groups = regExp.Matches(input);
           
            Console
.WriteLine("Rok:" + groups[0].Groups["rok"].Value);
            Console.WriteLine("Mesiac:" + groups[0].Groups["mesiac"].Value);
            Console.WriteLine("Den:" + groups[0].Groups["den"].Value);
            Console.WriteLine("Hodina:" + groups[0].Groups["hodina"].Value);
            Console.WriteLine("Minuta:" + groups[0].Groups["minuta"].Value);
        }
    Console.ReadLine();
    }

}




>>Čo bude nabudúce?

Nabudúce sa konečne pozrieme na utility, ktoré sú FREE a umožňujú nám tvoriť regulárne výrazy a jeden príklad zas dáme na ďalšiu možnosť použitia RegEx.
Ak máte nejaké fajn príklady na regExp, stále Vás prosím, napíšte ich do diskusie.

[Príklady z článku na stiahnutie]

porovnanie rýchlosti – cely projekt
porovnanie rýchlosti – len EXE + DLL, pre Vaše vlastné testovanie
parsovanie dátumu a času pomocou RegEx.

[Iné zdroje]

amk.ca/python/howto/regex/

ondotnet.com/pub/a/dotnet/2002/03/11/regex2.html

O rôznych typoch regEx:
en.wikibooks.org/wiki/Regular_Expressions/Regular_Expression_Syntaxes


[Predchádzajúce diely]

Programujeme v jazyku C# III. Diel 6. – Regulárne výrazy I
Programujeme v jazyku C# III. Diel 5. –Všetko o DataSet II.
Programujeme v jazyku C# III. Diel 4. – Všetko o DataSet I.



SEE YOU!


Michal Čižmár