Z času na čas sa stretneme s problémom, kedy je potrebné analyzovať reťazce na vyššej úrovni ako len porovnávaním. Tieto situácie sa dajú riešiť vlastným kódom, kedy reťazec pracne analyzujete. V takýchto situáciách jeale výhodnejšie a jednoduhšie použiť regulárne výrazy. Tento článok vám ukáže ako regulárne výrazy implementovať do C/C++ kódu a uvediem aj zopár asi najpoužívanejších príkladov.
Regulárne výrazy
Vráťme sa od PCRE k samotnej podstate regulárnych výrazov. Regulárne výrazy sú reťazce značiek a písmen, ktoré predstavujú masky, predpisy textového reťazca. Najlepšou ukážkou regulárneho výrazu je asi ‘*.exe’ pri hľadaní exe súborov. Na základe tohto predpisu sa prechádza zoznam súborov a vyhodnocuje sa či vyhovuje predpisu alebo nie. Toto je však len ilustračný príklad aby ste pochopili čo regulárne výrazy sú.Na testovanie správnosti regulárnych výrazov existuje skvelá utilitka Visual RegExp ktorú nájdete na laurent.riesterer.free.fr/regexp/.Tie najdôležitejšie ktoré použijem v tomto článku si vysvetlime na jednoduchých príkladoch.
Zoberme si teda že v našom programe prechádzame riadky textového súboru a testujeme ich či sú v súlade s predpisom regulárneho výrazu. Dajme tomu že regulárny výraz bude „jablko“. V tomto prípade budú tomuto výrazu vyhovovať všetky reťazce v ktorých sa bude nachádzať sled znakov jablko. Ak chceme vo výrazoch použiť znaky ako „+“ alebo „/“, je potrebné pred tieto znaky vsunúť znak “\”. Ak napríklad chceme hľadať “jablko+hruska”, tak regulárny výraz bude vyzerať “jablko\+hruska”. Toto escape-ovanie je z jednoduchého dôvodu. Tieto znaky majú bez escape špeciálny význam.
Zobeme si inú situáciu, kedy máme reťazec, a potrebujeme zistiť či sa jedná o zápis IP adresy. Regulárny výraz pre tento účel bude vyzerať následovne:
[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}
Výraz vyzerá na prvý pohľad komplikovane ale keď sa lepšie pozriete, ide o opakujúci sa vyraz ‘[0-9]{1,3}\.’. Vyraz v [ ] hovorí, o aký znak pôjde. V tomto prípade pôjde o čísla od 0 do 9. Ďalšia časť výrazu v { } hovorí koľko krát sa číslo bude opakovať. Ide o interval 1 až 3 znaky. Na konci sa nachádza escape-ovany znak bodky. Ak by sme neuviedli interval v {}, výraz by kontroloval iba jedno číslo čo by znamenalo iba IP adresy v tvare 1.2.3.4 alebo 2.5.1.5.
Reťazce môžeme pomocou regulárnych výrazov aj rozdeliť na menšie skupiny, na substringy. Opäť modelová situácia. Máme aplikáciu ktorá pracuje s titulkami. Každý asi videl nasledujúci formát.
{0}{123} Translated by ..
Tento text potrebujeme rozdeliť na začiatočný frame, konečný frame a text. To môžeme zrealizovať nasledujúcim regulárnym výrazom:
\{([0-9]+)\}\{([0-9]+)\}(.+)
Vyzerá pekne divoko však? Rozoberieme si ho na menšie časti. V texte sa používajú zátvorky {} ktoré majú v regulárnych výrazoch význam intervalov, preto sme ich museli escapeovať. Medzi zátvorkami \{ a \} sa nachádza výraz ([0-9]+). Ide o číslo frame-u. Znamienko + znamená že pôjde o interval {1, nekonečno}. Zátvorky ( a ) nám vytvárajú skupinu - substring pomocou ktorej sa potom dostaneme k frame-om a textu. V tomto výraze sú 3 skupiny a to: začiatočný frame, konečný frame a text. Na konci výrazu sa nachádza skupina (.+). Bodka má vo výrazoch význam akéhokoľvek znaku a + znamená opäť interval {1, nekonečno}, čiže pôjde o zbytok textu.
Mojim cieľom nebolo tu písať všetko o konštrukcii regulárnych výrazoch. V tejto časti som len rozobral regulárne výrazy ktoré použijem ďalej v článku. Na stránke www.regularnivyrazy.info sa o písaní regulárnych výrazov dozviete omnoho viac.
Matchovanie v C/C++
Teraz keď už máte predstavu o tom ako pracujú regulárne výrazy, si ukážeme ako teda s nimi pracovať v C/C++ pomocou knižnice PCRE. Všetky potrebné funkcie sa nachádzajú v headri pcre.h. Používanie reg. výrazov sa dá rozdeliť na 2 kroky. Najprv je potrebné regulárny vyraz skompilovať. Na to slúži funkcia pcre_compile(), ktorá výraz skompiluje, vytvorí vnútornú štruktúru a vráti pcre handle. Ďalším krokom je spustenie tohto regulárneho výrazu nad určitým reťazcom. K tomu je určená funkcia pcre_exec(), ktorej predáme handle na pcre získané pomocou pcre_compile() a reťazec, v ktorom chceme výraz match-núť. Funkcia podľa toho vráti číslo. Ak sa vyraz v reťazci vyskytuje, funkcia vráti číslo väčšie ako 0. V opačnom prípade vráti číslo menšie ako 0. Najprimitívnejšia aplikácia využívajúca regulárne výrazy teda bude vyzerať nasledovne:example1.c:
#include <stdio.h>
#include <string.h>
#include <pcre.h>
#define REG_EXP "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}"
#define VEC_SIZE 30
int main(int argc, char** argv)
{
pcre* re_handle;
const char* err;
int err_offset;
int ovector[VEC_SIZE];
int res;
if (argc < 2) {
printf("USAGE %s [text]\n", argv[0]);
return 0;
}
//kompilacia vyrazu
re_handle = pcre_compile(REG_EXP, 0, &err, &err_offset, NULL );
if (!re_handle) {
printf("ERROR: %s", err);
return 0;
}
//vyhodnotenie vyrazu
res = pcre_exec(re_handle, NULL, argv[1], strlen(argv[1]), 0, 0, ovector, VEC_SIZE);
if (res < 0) {
printf("text is not IP address\n");
} else {
printf("text is IP address\n");
}
pcre_free(re_handle);
return 0;
}
Ak si všimnete, všetky "\" v REG_EXP sú dvojmo. Je to z dôvodu že v Céčku sa znak "\" píše ako "\\". Funkcia pcre_compile() má okrem patternu REG_EXP aj iné parametre. Druhým parametrom sú špeciálne nastavenia ako použité kódovanie reťazca. V tomto prípade 0 znamená defaultné nastavenie. Ďalším ukazovateľom je pointer do ktorého sa uloží text v prípade chybnej kompilácie. Do premennej err_offset sa zase uloží číslo pozície v patterne kde sa vyskytla chyba. Tretím a štvrtým parametrom funkcie pcre_exec() je text a veľkosť textu, nad ktorým chceme spustiť regulárny vyraz. Nasledujúca "0" je začiatočná pozícia v texte odkiaľ sa zaháji match-ovanie. Ďalšia "0" opäť znamená defaultné nastavenie ako to bolo u pcre_compile().Ďalšie premenné sú zatiaľ nepodstatné a ich význam si vysvetlíme neskôr. Funkcia nakoniec vráti číselnú hodnotu. Ak je tato číselná hodnota menšia ako 0, tak sa požadovaný reg. výraz v argv[1] nenachádza. Ak je výsledok väčší ako 1, vtedy určitá časť argv[1] vyhovuje reg. výrazu a regulárny vyraz sa našiel. Nakoniec je volaná funkcia pcre_free(), ktorá ma za úlohu uvolniť handle z pamäte. Po kompilácii spustime program s argumentom ktorý bude reprezentovať IP adresu.
-bash-3.00$ ./example1 192.168.1.1
text is IP address
Program zisti podľa reg. výrazu, že text má formát IP adresy a preto vypíše "text is IP address". Skúste program spustiť s náhodným slovom. Slovo nebude vyhovovať reg. výrazu a preto program vypíše "text is not IP address".
-bash-3.00$ ./example1 slovo
text is not IP address
Počet výskytov a ako získať text vyhovujúci reg. výrazu
Zoberme si modelovú situáciu, kedy potrebujeme napríklad zistiť koľko kráť sa v texte vyskytuje reťazec vyhovujúci reg. výrazu. Ak si myslíte, že do premennej ressa v predchádzajúcom príklade ukladá číslo ktoré je počet vyhovujúcich substringov, tak sa mýlite. Funkcia pcre_exec() pracuje tak, že sa zastaví na prvom výskyte substringu. Jeho začiatočnú pozíciu (tzv. offset) zapíše do ovector[0] a konečnú pozíciu do ovector[1]. To nám stačí na to aby sme si vedeli vytvoriť logiku, ktorá vypíše jednotlivé substringy a spočíta výskyt substringov vyhovujúcich regulárnemu výrazu.example2.c:
#include <stdio.h>
#include <string.h>
#include <pcre.h>
#define REG_EXP "[0-9]{3}"
#define VEC_SIZE 30
int main(int argc, char** argv)
{
pcre* re_handle;
const char* err;
int err_offset;
int ovector[VEC_SIZE];
int res = 0;
int pos = 0;
int count = 0;
char substring[4];
if (argc < 2) {
printf("USAGE %s [text]\n", argv[0]);
return 0;
}
//kompilacia vyrazu
re_handle = pcre_compile(REG_EXP, 0, &err, &err_offset, NULL );
if (!re_handle) {
printf("ERROR: %s", err);
return 0;
}
//prechadzanie stringu a hladanie vyhovujucich substringov
while ( (res = pcre_exec(re_handle, NULL, argv[1], strlen(argv[1]), pos, 0, ovector, VEC_SIZE)) >= 0 ) {
count++;
memcpy(substring, (argv[1] + ovector[0]), (ovector[1] - ovector[0]));
pos = ovector[1];
printf("matched %s\n", substring);
}
printf("count of 3 decimal numbers:%d\n", count);
pcre_free(re_handle);
return 0;
}
V programe sa nachádza cyklus. Tento cyklus sa bude vykonávať dovtedy, pokiaľ sa v texte budú nachádzať substringy vyhovujúce reg. výrazu. Vo vnútri cyklu sa pri každom nájdení inkrementuje premenná count. Potom sa do pomocnej premennej substring nakopíruje časť argv[1], ktorá vyhovuje reg. výrazu. Obsah tejto premennej potom vypíšeme na výstup. Nakoniec program vypíše na výstup počet. Regulárny vyraz je pozmenený a celkovo program spočíta koľko krát sa nachádza napríklad v IP 3-mieste číslo.
-bash-3.00$ ./example2 192.168.1.1
matched 192
matched 168
count of 3 decimal numbers:2
Tajomstvo poľa ovector odhalené - prístup k skupinám
Na začiatku článku som spomínal reg. výraz, pomocou ktorého môžeme pekne rozložiť riadok reprezentujúci titulky k filmom. Spomínal som, že v regulárnych výrazoch znaky ( a ) vytvárajú skupiny. K týmto skupinám je totižto možné jednoducho pristupovať pomocou ovector poľa. Taktiež som už v článku spomínal čo znamenajú prvé dve hodnoty poľa ovector. Nasledujúce hodnoty zase slúžia na prístup k jednotlivým skupinám regulárneho výrazu. Treba si uvedomiť, že skupinu v ovector poli reprezentuje vždy pár hodnôt. Reg. výraz, ktorý sme si spomenuli v súvislosti s titulkami obsahuje 3 skupiny. Prvý pár ovector[0] a ovector[1] tvoria hodnoty začiatku a konca substringu vyhovujúcemu regulárnemu výrazu. Ďalší pár ovector[2] a ovector[3] tvoria hodnoty určujúce prvú skupinu. Ďalší pár zase určuje druhú skupinu a ďalší pár určuje tretiu skupinu. S toho vyplýva, že hodnoty ktoré pre nás budú zaujímavé v poli ovector sú ovector[0] až ovector[7]. Taktiež si ozrejmíme akú hodnotu vlastne pcre_exec() vráti. Funkcia vráti počet skupín. Keď už vieme čo je čo, dokážeme spracovať riadok tituliek tak aby sme naplnili štruktúru dát.example3.c:
#include <stdio.h>
#include <string.h>
#include <pcre.h>
#define REG_EXP "\\{([0-9]+)\\}\\{([0-9]+)\\}(.+)"
#define VEC_SIZE 30
#define BUF_SIZE 255
typedef struct {
int start;
int end;
char* text;
} subtitle;
int getSubstrFromRe(char* line, int* ovector, int pos, char* out, int out_size)
{
char* substr_start;
int substr_size = 0;
substr_start = line + ovector[(2*pos)];
substr_size = ovector[(2*pos)+1] - ovector[(2*pos)];
if (substr_size >= out_size) {
return 0;
}
memset(out, '\0', out_size);
memcpy(out, substr_start, substr_size);
return 1;
}
int fillSubtitle(subtitle* sub, pcre* re, char* line)
{
int res = 0;
int ovector[VEC_SIZE];
char buf[BUF_SIZE];
//vykonanie reg. vyrazu
res = pcre_exec(re, 0, line, strlen(line), 0, 0, ovector, VEC_SIZE);
if (res < 0) {
return 0;
}
//spracovanie start-u
getSubstrFromRe(line, ovector, 1, buf, BUF_SIZE);
sub->start = atoi(buf);
//spracovanie end-u
getSubstrFromRe(line, ovector, 2, buf, BUF_SIZE);
sub->end = atoi(buf);
//spracovanie textu
getSubstrFromRe(line, ovector, 3, buf, BUF_SIZE);
sub->text = malloc(strlen(buf));
strcpy(sub->text, buf);
return 1;
}
int main(int argc, char** argv)
{
FILE* subtitle_file = NULL;
pcre* re_handle = NULL;
pcre_extra* re_extra = NULL;
const char* err;
int err_offset;
char buf[BUF_SIZE];
subtitle sub;
if (argc < 2) {
printf("USAGE %s [subtitle file]\n", argv[0]);
return 0;
}
//kompilacia reg.vyrazu
re_handle = pcre_compile(REG_EXP, 0, &err, &err_offset, NULL );
if (!re_handle) {
printf("ERROR reg.exp.: %s", err);
return 0;
}
subtitle_file = fopen(argv[1], "r");
while ( fgets (buf, BUF_SIZE, subtitle_file) != NULL ) {
if (fillSubtitle(&sub, re_handle, buf)) {
printf("line info:\n");
printf(" -start:%d\n", sub.start);
printf(" -end:%d\n", sub.end);
printf(" -text:%s\n", sub.text);
free(sub.text);
}
}
fclose(subtitle_file);
pcre_free(re_handle);
return 0;
}
Program vlastne prechádza textový súbor riadok po riadku. Každý riadok je spracovaný regulárnym výrazom osobitne vo funkcii fillSubtitle(), ktorá napĺňa štruktúru subtitle. Druhou dôležitou funkciou je getSubstrFromRe(). Táto funkcia získava obsah skupín regulárneho výrazu z ovector a aktuálneho riadku. Obsahom ďalej naplní premennú buf, ktorá sa používa vo funkcii fillSubtitle(). Program je trosku rozsiahlejší ako predchádzajúce príklady. Je to praktickým a kompletným zhrnutím toho, čo sme si o PCRE v tomto článku povedali. Už vás ďalej teda nebudem trápiť, predsa len PCRE je celkom jednoduchá vec s ktorou je možné docieliť dosť veľa vecí a ktorá vie uľahčiť programovanie.