Regulární výrazy
V této kapitole se budu zabývat používáním reg. výrazů v jazyku C. Nebudu se zabývat popisem regulárních výrazů jako takových, to si prosím nastudujte z jiných stránek. Můžete se podívat například na regulární výrazy, nebo na wikipedii.
Regulární výrazy
Že nevíte, co jsou to regulární výrazy?
Regulární výrazy patří do výbavy každého dobrého programátora a každého dobrého programovacího jazyka. Regulární výrazy se používají k vyhledávání částí textu podle určitého vzoru, zvaného regulární výraz, nebo též pattern. Například reg. výraz pro e-mail může vypadat tatko:
[0-9a-zA-Z]+@[0-9a-zA-Z]+\.[a-zA-Z]+
Toto je pattern, který popisuje, jak má vypadat nějaký text. Určité znaky nebo sekvence znaků mají v patternu speciální význam.
Například
+
znamená v reg. výrazu „minimálně jeden“. Tedy znak předcházející +
se musí v textu objevit minimálně jednou (ale i vícekrát). V hranatých závorkách
jsou intervaly písmen a čísel. Tj. [3-5]
představuje jedno z čísel 3,4 a 5.
Tečka obvykle znamená libovolný znak, pokud je ale před ní
zpětné lomítko, znamená jen tečku. (Zpětné lomítko by se zapsalo jako dvě
zpětná lomítka.) A zavináč je jen zavináč :-).
Tento reg. výraz tedy můžete číst jako text, který začíná nejméně jedním číslem nebo velkým
či malým znakem A
až Z
, pak je zavináč @, pak zase minimálně
jedno číslo nebo znak, pak tečka a pak minimálně jeden velký či malý znak (znaky bez diakritiky).
Reg. výraz můžete použít k tomu, abyste zjistili, zda nějaký text tento pattern obsahuje (zda je v něm e-mail) a případně také k tomu, abyste zjistili, kde v textu se pattern nachází (kde e-mail začíná).
Tohle byl maximálně stručný úvod k reg. výrazům pro ty, o o reg. výrazech nikdy neslyšeli. Jak už jsem psal, syntaxi reg. výrazů se naučte odjinud. V této kapitole vytvořím 3 programy, které budete moci použít pro testování reg. výrazů, takže mi hned neutíkejte! :-).
Existuje mnoho „dialektů“ regulárních výrazů. V Linuxu najdete funkce, které pracují s reg. výrazy dle normy POSIX, a to tzv. basic posix a extended posix (Na obojí se používají ty samé funkce. Mezi těmito dialekty se jen přepíná jedním příznakem, viz dále).
Kromě toho existuje knihovna PCRE (Perl-compatible regular expressions), která vám umožní používat reg. výrazy kompatibilní s jazykem Perl. Jeden z důvodů, proč se programovací jazyk Perl stal slavný, jsou právě jeho regulární výrazy.
Knihovna PCRE se dá použít i ve Windows.
Existují určitě i další knihovny, které byste mohli použít pro reg. výrazy, ale tyto dvě jsou nejpoužívanější. O rozdílech mezi těmito dialekty se dočtete na Wikipedii (některé tu i zmíním v příkladech).
Posix
Posix funkce najdete v knihovně <regex.h>. Základem jsou tyto dvě funkce:
První z nich, regcomp()
vezme text s reg. výrazem (regex) a zkompiluje ho do svého
vnitřního binárního formátu regex_t
, na který pak ukazuje preg. Argumentem cflags
můžete ovlivnit některé vlastnosti reg. výrazu, například, zda chcete použít
basic nebo extend verzi posixových reg. výrazů.
S tímto reg. výrazem pak pracuje funkce regexec()
, která zjišťuje, zda se v string pattern
popsaný v preg
nachází. Pokud ano, vrátí regex 0 a do pole pmatch délky nmatch
uloží objekty typu regmatch_t
, které popisují začátek a konec nalezených substringů. Parametr
eflags může ovlivňovat vyhledávání reg. výrazů, podobně jako
cflag u regcomp()
.
Ale není tak důležitý, takže ho pro stručnost nebudu popisovat.
Struktura regmatch_t
vypadá takto:
Substringem je jednak celý nalezený pattern a potom také každá jeho část, která je uzavřená v závorkách. Například následující reg. výraz by měl vrátit 4 substringy (celý pattern + 3 závorky).
\([0-9a-zA-Z]+\)@\([0-9a-zA-Z]+\)\.\([a-zA-Z]+\)
Pozor! Basic a extend verze se liší v tom, jak závorky interpretují. V basic verzi je závorka jen závorka a substringy se musí uvádět do závorek, před kterými je zpětné lomítko (jak je to v příkladu). Extended verze to má přesně naopak!
Pokud už nebudete regex_t
používat, měli byste ho smazat funkcí regfree()
.
Nejlépe všechno pochopíte z příkladu. Nejdříve jsem si definoval strukturu pro uložení voleb z příkazové řádky. Nejdůležitejší částí struktury jsou asi pattern pro uložení reg. výrazu a string pro uložení prohledávaného textu.
- /*------------------------------------------------*/
- /* 26regexp/parse-options.h */
- #ifndef _PARSE_OPTIONS
- #define _PARSE_OPTIONS
- #include <stdbool.h>
- #include <stdio.h>
- bool err;
- bool help;
- bool show;
- bool icase;
- bool newline;
- bool extended;
- } task;
- #endif
- /*------------------------------------------------*/
Funkce parseOptions()
nastaví strukturu task
dle parametrů z příkazové řádky.
Používá k tomu funkci getopt_long(). Funkce printOptions()
vypíše strukturu task
a na to co dělá funkce printOptionsHelp()
už přijdete sami :-).
- /*------------------------------------------------*/
- /* 26regexp/parse-options.c */
- #include "parse-options.h"
- #include <getopt.h>
- int c;
- {"help", no_argument, NULL, 'h'},
- {"show-options", no_argument, NULL, 's'},
- {"pattern", required_argument, NULL, 'p'},
- {"icase", no_argument, NULL, 'i'},
- {"newline", no_argument, NULL, 'n'},
- {"no-extended", no_argument, NULL, 'b'},
- {NULL, 0, NULL, 0}
- };
- do {
- c = getopt_long(argc, argv, optstring, longopts, NULL);
- }
- t.string = argv[optind];
- }
- return t;
- }
- "ERR:\t\t %s\n"
- "Help:\t\t %s\n"
- "Show options:\t %s\n"
- "Pattern:\t '%s'\n"
- "Search in:\t '%s'\n"
- "REG_ICASE:\t %s\n"
- "REG_NEWLINE:\t %s\n"
- "REG_EXTENDED:\t %s\n",
- t.err ? "ANO" : "NE",
- t.help ? "ANO" : "NE",
- t.show ? "ANO" : "NE",
- t.pattern,
- t.string,
- t.icase ? "ANO" : "NE",
- t.newline ? "ANO" : "NE",
- t.extended ? "ANO" : "NE"
- );
- }
- printf("./regex1 -h | [-p|--pattern \"regular expression\"] [-i|--icase] [-n|--newline] [-b|--no-extended] [\"prohledávaný řetězec\"]\n\n");
- }
- /*------------------------------------------------*/
Teď přichází ta zajímavá část, funkce main()
.
Na začátku jsem si deklaroval funkce regerr()
, která vypíše chybové hlášení a
printPatterns()
, která vypíše nalezené substringy. Všiměte si volání
setlocale()
. To vám jednak přeloží chybové
hlášky do češtiny, ale hlavně zajistí správné fungování s UTF-8.
Bez setlocale()
by například pattern [[:alpha:]]
nepovažoval
č
za písmeno.
- /*------------------------------------------------*/
- /* 26regexp/posix.c */
- #include "parse-options.h"
- #include <stdlib.h>
- #include <regex.h>
- #include <locale.h>
- #define N 50
- regex_t regex;
- task t;
- regmatch_t pmatch[N];
Následuje nastavení příznaků cflags
Příznak | Význam |
---|---|
REG_EXTENDED | Použije se extended verze posixových reg. výrazů. (Jinak se použije basic.) |
REG_ICASE | Ignoruje velikosti písmen |
REG_NEWLINE | Ovlivňuje, jak se zpracovává nová řádka (\n). S tímto příznakem například tečka,
která reprezenuje libovolný znak, \n za znak nepovažuje. Znaky
začátku řetězce ^ a konce řetězce $ považují \n za začátek/konec
řetězce (jinak považují za konec jen \0 a za začátek začátek celého textu).
|
Tyto příznaky si můžete v programu z příkladu přepínat volbami z příkazové řádky, abyste si mohli vyzkoušet, jak se chovají. Existují i další příznaky, viz manuálové stránky.
Zbytek funkce main()
zavolá regcomp()
a regexec()
pro nalezení výrazu.
- /* Kompilace reg. výrazu */
- ret = regcomp(®ex, t.pattern, cflags);
- regerr(®ex, ret);
- }
- /* Vyhledávání reg. výrazů */
- /* vypsání nalezených částí reg. výrazu */
- printPatterns(N,pmatch, t.string);
- }
- }
- else {
- regerr(®ex, ret);
- }
- regfree(®ex);
- }
Funkce regerror()
(též z <regex.h>) zapíše chybové hlášení do bufferu.
Nelezené substringy vypíši jednoduše pomocí sekvence %.*s
. Funkci printf()
stačí předat jako parametry délku substringu (pmatch[i].rm_eo - pmatch[i].rm_so;
) a
ukazatel na začátek substringu (text+pmatch[i].rm_so
).
- /**
- * Vypíše chybu
- */
- }
- /**
- * Vypíše nalezené části textu
- */
- int length;
- /* hledam posledni nalezeny text */
- }
- /* vypisuji od zacatku az k poslednimu nalezenemu textu */
- length = pmatch[i].rm_eo - pmatch[i].rm_so;
- length, text+pmatch[i].rm_so);
- }
- }
- /*------------------------------------------------*/
A teď už si můžete zkoušet posix reg. výrazy:
$ ./posix -p "([0-9a-zA-Z]+)@([0-9a-zA-Z]+).([a-zA-Z]+)" spam@example.org
PATTERN BYL NALEZEN.
(0, 16) 16 'spam@example.org'
(0, 4) 4 'spam'
(5, 12) 7 'example'
(13, 16) 3 'org'
$ ./posix --no-extend -p "([0-9a-zA-Z]+)@([0-9a-zA-Z]+).([a-zA-Z]+)" spam@example.org
PATTERN nebyl NALEZEN.
$ ./posix -p "[[:alpha:]]" "Čau Světe"
PATTERN BYL NALEZEN.
(0, 2) 2 'Č'
$ ./posix -p "^.+$" "Čau
Světe"
PATTERN BYL NALEZEN.
(0, 11) 11 'Čau
Světe'
$ ./posix -n -p "^.+$" "Čau
Světe"
PATTERN BYL NALEZEN.
(0, 4) 4 'Čau'
$ ./posix -p "([^ ]*) (.*" "Čau Světe"
Nepodařilo se zkompilovat reg. exp. '([^ ]*) (.*'
Regex match failed: Nepárová ( or \(
Č
je dlouhé dva bajty, protože v UTF-8 kódování se zapisuje pomocí
dvou bajtů. Což, mimochodem, znamená,
že podle délky nepoznáte, kolik znaků reg. výrazu odpovídá. Viz znakové sady - UTF-8.
PCREPosix
PCRE - Perl-compatible regular expressions, je knihovna, která vám umožní používat jeden z nejpopulárnějších dialektů reg. výrazů, který pochází z jazyka Perl.
Knihovna je běžně součástí linuxových distribucí, ale asi si budete muset nainstalovat její vývojový balíček. V OpenSuSE třeba takto:
PCRE má vlastní sadu funkcí (API, neboli aplikační interface/rozhraní). Má ale také knihovnu <pcreposix.h>, která obsahuje funkce stejného jména, jako <regex.h>. Takto knihovna je tu kvůli snadnému přechodu z posix k PCRE.
Ve zdrojových kódech najdete soubor pcre.c
, který ukazuje použití <pcreposix.h>.
Je na chlup stejný, jako posix.c
, jen místo <regex.h> má <pcreposix.h> a
do cflags nastavuje příznaky REG_UTF8 a REG_UCP.
2c2
< /* 26regexp/posix.c */
---
> /* 26regexp/pcreposix.c */
6c6
< #include <regex.h>
---
> #include <pcreposix.h>
33c33
< cflags = 0;
---
> cflags = REG_UTF8 | REG_UCP;
Posix REG_UTF8 nemá, protože pracuje s UTF-8 (a jen s UTF-8) automaticky (dle locale), kdežto PCRE umožňuje pracovat
s 8 bitovým kódováním, UTF-8, UTF-16 i UTF-32. Nastavení UTF-8 ovšem ještě nestačí na to, aby
reg. výrazy jako [[:alpha:]]
považovali č
za znak. To se nastavuje právě
příznakem REG_UCP.
Při překladu musíte přikompilovat knihovnu pcreposix
, ale také pcre
.
Pokud na to zapomenete, možná se vám program taky přeloží, ale bude se chovat divně,
padat s chybou „Neoprávněný přístup do paměti (SIGSEGV)“ atp.
Program pcreposix
pracuje skutečne s PCRE, takže například volba --no-extend
nemá význam. PCREposix defunuje REG_EXTENDED jako 0, takže jeho nastavení nic nedělá. Definuje ho jen
kvůli kompatibilitě s <regex.h>.
PATTERN BYL NALEZEN.
(0, 16) 16 'spam@example.org'
(0, 4) 4 'spam'
(5, 12) 7 'example'
(13, 16) 3 'org'
$ ./posix -p "<p>(.*?)</p>" "<p>První odstavec.</p> <p>Druhý odstavec</p>"
PATTERN BYL NALEZEN.
(0, 46) 46 '<p>První odstavec.</p> <p>Druhý odstavec</p>'
(3, 42) 39 'První odstavec.</p> <p>Druhý odstavec'
$ ./pcreposix -p "<p>(.*?)</p>" "<p>První odstavec.</p> <p>Druhý odstavec</p>"
PATTERN BYL NALEZEN.
(0, 23) 23 '<p>První odstavec.</p>'
(3, 19) 16 'První odstavec.'
Otazník za .*
zamezí „hladovosti“ tohoto výrazu, takže se najde nejkratší možný
řetězec, který reg. výrazu odpovídá, místo nejdelšího možného. Funguje to ale jenom v Perl-compatilbe reg. výrazech,
v posixových ne.
PCRE
Výklad by mohl skončit u předchozí části, ale pcreposix má 2 nevýhody, kvůli kterým je dobré umět pracovat s nativními funkcemi PCRE. Za prvé, pcreposix volá interně nativní funkce PCRE, takže je o drobet pomalejší. Za druhé, pcreposix neumožňuje využít všechny možnosti PCRE.
Princip použití PCRE je podobný jako knihovny <regex.h>. Nejdříve se zkompiluje reg. výraz a pak se vyhledá. Liší se v tom, jaké můžete používat flagy, liší se v tom, jakým způsobem vrací výsledky (ale ne o moc) a taky má pár užitečných funkcí navíc.
Vše uvedu na příkladu. První část se zabývá parsováním voleb z příkazové řádky jako v předcházejícím příkladu. Volby se mírně liší, jinak tato část není ničím zajímavá.
- /*------------------------------------------------*/
- /* 26regexp/parse-options-pcre.h */
- #ifndef _PARSE_OPTIONS_PCRE
- #define _PARSE_OPTIONS_PCRE
- #include <stdbool.h>
- #include <stdio.h>
- bool err;
- bool help;
- bool show;
- bool icase;
- bool newline;
- bool ucp;
- } task;
- #endif
- /*------------------------------------------------*/
- /*------------------------------------------------*/
- /* 26regexp/parse-options-pcre.c */
- #include "parse-options-pcre.h"
- #include <getopt.h>
- int c;
- {"help", no_argument, NULL, 'h'},
- {"show-options", no_argument, NULL, 's'},
- {"pattern", required_argument, NULL, 'p'},
- {"icase", no_argument, NULL, 'i'},
- {"newline", no_argument, NULL, 'n'},
- {"no-ucp", no_argument, NULL, 'u'},
- {NULL, 0, NULL, 0}
- };
- do {
- c = getopt_long(argc, argv, optstring, longopts, NULL);
- }
- t.string = argv[optind];
- }
- return t;
- }
- "ERR:\t\t %s\n"
- "Help:\t\t %s\n"
- "Show options:\t %s\n"
- "Pattern:\t '%s'\n"
- "Search in:\t '%s'\n"
- "PCRE_CASELESS:\t %s\n"
- "PCRE_MULTILINE:\t %s\n"
- "PCRE_UCP:\t %s\n",
- t.err ? "ANO" : "NE",
- t.help ? "ANO" : "NE",
- t.show ? "ANO" : "NE",
- t.pattern,
- t.string,
- t.icase ? "ANO" : "NE",
- t.newline ? "ANO" : "NE",
- t.ucp ? "ANO" : "NE"
- );
- }
- printf("./regex1 -h | [-p|--pattern \"regular expression\"] [-i|--icase] [-n|--newline] [-u|--no-ucp] [\"prohledávaný řetězec\"]\n\n");
- }
- /*------------------------------------------------*/
Tady je ta zajímavá část:
- /*------------------------------------------------*/
- /* 26regexp/pcre.c */
- #include "parse-options-pcre.h"
- #include <pcre.h>
- #include <string.h>
- #include <locale.h>
- #define N 50
- pcre *regex;
- pcre_extra *study;
- task t;
- int erroffset;
- t = parseOptions(argc, argv);
- printOptionsHelp();
- }
- printOptions(t);
- }
- options = PCRE_NEWLINE_ANYCRLF | PCRE_UTF8;
- options |= PCRE_CASELESS;
- options |= PCRE_MULTILINE;
- options |= PCRE_UCP;
Volby příkazové řádky jsou naparsovány, options nastaveno, tak se může zkompilovat reg. výraz.
PCRE nabízí ještě jeden, nepovinný, krok, optimalizaci pomocí pcre_study()
.
Tato funkce si prohlédne zkompilovaný reg. výraz a vytvoří objekt, který pomůže funkci
reg_exec()
urychlit vyhledávání. Pokud není jak urychlovat, vrátí pcre_study()
NULL.
Zbytek postupu je obdobný jako u posix funkcí.
const char **errptr, int *erroffset,
const unsigned char *tableptr);
pcre_extra *pcre_study(const pcre *code, int options,
const char **errptr);
int pcre_exec(const pcre *code, const pcre_extra *extra,
const char *subject, int length, int startoffset,
int options, int *ovector, int ovecsize);
Parametru tableptr si nevšímejte a nechte ho NULL. Používá se pro nastavení
znaků, které má interpretovat [[:alpha:]]
atp. Vytváří se pomocí funkce
pcre_maketables()
, ale neměl by se nikdy používat spolu s PCRE_UTF8. Tak na něj
zapomeňte :-)
- /* Kompilace reg. výrazu */
- regex = pcre_compile(t.pattern, options, &errptr, &erroffset, NULL);
- t.pattern, errptr);
- }
- /* Optimalizace */
- study = pcre_study(regex, 0, &errptr);
- /* pcre_study() vrací NULL při chybě, ale i když jen nemůže
- * optimalizovat regex. Rozdíl se pozná podle pcreErrorStr */
- }
- /* Vyhledávání reg. výrazů */
- ret = pcre_exec(regex, study,
- t.string, /* prohledávaný text */
- 0, /* Začni prohledávat od tohoto indexu */
- 0, /* Další options, viz manuálové stránky */
- pmatch,
- );
- /* Vypiš chybu */
- /* vypsání nalezených částí reg. výrazu */
- printPatterns(ret, pmatch, t.string);
- }
- printRegerr(ret);
- }
- pcre_free(regex);
- pcre_free_study(study);
- }
- }
Typ chyby poznáte podle návratové hodnoty funkce pcre_exec()
.
Vybral jsem jen některé možnosti, ostatní jsem označil jako „Neznámá chyba“.
Seznam všech chyb najdete v manuálové stránce pcreapi
.
Zajímavý je ještě výpis nalezených substringů. Začátek a konec je uložen v poli typu int pmatch. První substring má tedy začátek a konec v pmatch[0] a pmatch[1], druhý substring v pmatch[2] a pmatch[3] atd.
Funkce pcre_exec()
vrací počet nalezených substringů, ale pokud je jich více, než
se vejde do pmatch vrací 0!
Výpis by mohl být udělán obdobně jako v příkladu u posixu pomocí %.*s
, ale PCRE nabízí
šikovnou funkci pcre_get_substring()
, která za vás zkopíruje substring do dynamicky alokovaného
řetězce (ten se pak musí dealokovast funkcí cre_free_substring()
).
- /**
- * Vypíše nalezené části textu
- */
- int j;
- ret = N / 2;
- }
- pcre_get_substring(text, pmatch, ret, j/2, &stringptr);
- pcre_free_substring(stringptr);
- }
- }
- /*------------------------------------------------*/
Při překladu tentokrát stačí uvést jen knihovnu pcre
.
$ ./pcre -p "<p>(.*?)</p>" "<p>První odstavec.</p> <p>Druhý odstavec</p>"
PATTERN BYL NALEZEN.
Match: ( 0,23): 23 '<p>První odstavec.</p>'
Match: ( 3,19): 16 'První odstavec.'
$ ./pcre -p "[[:alpha:]]" "Čau Světe"
PATTERN BYL NALEZEN.
Match: ( 0, 2): 2 'Č'
$ ./pcre --no-ucp -p "[[:alpha:]]" "Čau Světe"
PATTERN BYL NALEZEN.
Match: ( 2, 3): 1 'a'