Regulárním výrazů se někteří výojáři vyhýbají jako čert kříži. Přitom se jedná se jen pokročilejší způsob práce s řetězci pomocí textových šablon nazývaných právě regulární výrazy (dále už jen RV). Druhů RV existuje hned několik, já budu hovořit o regulárních výrazech odvozených z jazyka Perl, konkrétně o jejich implementaci v .NET Frameworku. Bude nás zajímat jmenný prostor System.Text.RegularExpressions.
Teoretický postup využití
Pro programátora jejich využití znamená v první řadě výraz navrhnout – to si popíšeme dále. V druhém kroku vezmeme vstupní text, který chce zpracovat a RV na něj aplikuje. Výsledkem je sada shod. Pokud v sadě není shoda žádná, výraz nesouhlasí se vstupním textem. Naopak při jedné shodě RV souhlasí na jednom místě vstupního textu a při několikanásobné shodě i na více místech.
V .NET Frameworku nalezneme i řadu dalších funkcí (Raplace, IsMatch atp.) – ty všechny ale jen usnadňují práci s procesem, který jsem popsat v předchozím odstavci.
Nejlépe poslouží jednoduchý příklad. Mějme text, v němž hledáme slovo “ale”. Navrhneme RV pro hledání slova “ale” a aplikujeme ho na vstupní text. Výsledek zpracování bude obsahovat přesný počet shod jako slov “ale” ve vstupním textu. Zároveň získáme přesné pozice, na kterých se hledaná slova nachází.
Psaní regulárních výrazů
Regulární výrazy vyhlížejí děsivě, ale při znalosti několika elementárních principů a speciálních znaků se stávají jednoduchým a přehledným nástrojem. Navíc jestli jste odpůrci nepřirozeného školního cpaní informací do hlavy, používejte praktický přehled z této kapitoly tak dlouho, než se vše naučíte nazpaměť běžným používáním. Nebude to trvat dlouho, slibuji.
K prvním pár elementům zkusim navíc napsat jejich ekvivalenty mezi funkcemi .NETu pro práci s řetězci, aby bylo patrné k čemu jdou používat. Proti funkcím mají však RV výhodu možnosti libovolné kombinace jednotlivých elementů, to ale ukážu až v pokročilejších funkcích, teď je na čase pochopit elementy základní.
Element – speciální znaky a přímý text
Při psaní RV budeme chtít často zapsat obyčejný text, jehož výskyt budeme ve vstupním řetězci hledat. To jde udělat přímo, jen si musíme dát pozor na tyto speciální znaky:
. $ ^ { [ ( | ) ] } * + ? \
Například při hledání textu obsahující frázi “Ahoj (text v závorce).” musíme v RV zapsat: “Ahoj \(text v závorce\)\.” – každý speciální znak je potřeba nahradit za únikovou sekvenci začínající \.
Pokud RV obsahuje jen přímý text, nalezneme shodu v každém řetězci, co ho obsahuje. Třeba výraz “ahoj” najde přesně tolik shod, kolik je slov “ahoj” ve zpracovávaném řetězci. Tím můžeme nahradit funkci Contains – zjišťuje přítomnost kusu textu v řetězci.
Elementy začátku a konce textu (znaky ^ a $)
V minulém odstavci bylo řečeno, že přímý text může být umísťen kdekoliv v hledaném řetězci. Pomocí znaku ^ můžeme určit ve výrazu začátek a pomocí $ zase konec.
Například výraz “^Ahoj” nalezne 1 shodu, pokud zpracovávaný řetězec začíná slovem Ahoj. Jakýkoliv výskyt slova Ahoj jinde než na začátku neodpovídá výrazu.
Stejným způsobem můžeme používat i znak $ reprezentující konec textu. Třeba “\.$” najde 1 shodu s textem končícím znakem tečky (všimněte si zpětného lomítka, tečka je speciální znak vyžadující únikovou sekvenci).
Znak konce i začátku můžeme zapsat do jednoho RV. Ukázkou může být “^Ahoj$”. Takový RV najde shodu jen s řetězcem “Ahoj”, nic nesmí být před ním, ani za ním, protože jsme upřesnili začátek i konec.
Těmito speciálními znaky můžeme nahradit textové funkce StartsWith, EndsWith – když umístíme na začátek přímého textu ^, kontroluje, zda řetězec přímým textem začíná (StartsWith) a při umístění $ na konec, zase, zda přímým textem končí (EndsWith).
Element alternativy (znak |)
Zatím umíme hledat začátek a konec a výskyty přímého textu. To moc možností zatím nedává a určitě by to nikoho nepřesvědčilo RV používat. Teď se ale podíváme na první zajímavější element v podobě znaku |. Určuje možnosti, něco jako v programování operátor OR (nebo).
Výrazu “Katka|Marek” bude nalezeny shody u všech textů obsahující slova Katka nebo slova Marek. Pokud jich bude v textu více, najde se i více shod. Navíc možností můžeme uvést klidně i víc, třeba “Katka|Marek|Andrea”.
Už zde můžeme elementy různě míchat, například RV “^Nazdar|Ahoj$” bude platný v případě, že řetězec buď končí slovem Ahoj nebo začíná Nazdar.
Elementy skupiny (znak jednoduchých závorek)
V předchozí kapitolce jsme probírali znak |. Nejen pro jeho komplexnější využití je potřeba seskupování. Nejlépe půjde účel skupin ukázat na příkladu:
Chceme rozpoznávat dvě slova - Lucka a Lucko. To jde sice udělat výrazem “Lucka|Lucko”, ale s použitím skupiny máme možnost alterovat jen jeden znak: “Luck(a|o)”. Určitě vidíte podobnost spoužíváním závorek u logických výrazů v programování, je to v zásadě hodně podobné:
If Pismeno1 = “L” And Pismeno2 = “u” And Pismeno3 = “c” And Pismeno4=”k” And (Pismeno5 = “a” Or Pismeno5 = “o”)
Element libovolného znaku (znak . )
Zatím jsme pracovali jen s přímým textem. Naštěstí v RV můžeme používat i obecné zástupné znaky. Nejzákladnější je znak tečky, reprezentující jeden libovolný znak. Nemyslím, že je na tom moc co vysvětlovat, výraz “Tom.š” odpovídá všem řetězcům obsahujícím slovo “Tom(libovolný znak)š”. Tedy namátkou slova Tomáš, Tom3š, Tom-š, Tomzš atp.
Elementy počtu (znaky složených závorek)
Za každý znak, skupinu, či zástupný symbol můžeme zapsat kolikrát se může opakovat minimálně a kolikrát maximálně. Implicitně je to vždy jeden znak, proto nemusíme počty uvádět za každým písmenem, které se má objevit jen jednou.
Formát zápisu je následující “{minum,maximum}”, popřípadě, pokud je počet stálý, tak “{počet}”. Ve výrazu se pak může výskytovat třeba takto: “Aho{5}j” odpovídá řetězci s textem “Ahoooooj”, protože jsme určili, že počet písmene “o” bude muset být 5. Rozsah dává možností více, třeba: “Aho{1,3}j” odpovídají slova “Ahoj”, “Ahooj”, “Ahoooj”.
V případě, že neuvedeme horní mez stejně jako zde: “{minimum,}”, bude maximální počet nekonečný. Nazpomeňte ale na čárku, jinak se bude brát počet jako pevný.
Pokud aplikujeme určení počtu písmen na skupinu, musí se opakovat celá skupina. Třeba “(Ahoj){2}” odpovídá “AhojAhoj”. Je samozřejmé, že můžeme kombinovat i další elementy. Například “(Ahoj|Nazdar){5}$” najde shodu v textu, který končí 5ti slovy buď Ahoj nebo Nazdar za sebou.
Další elementy počtů (znaky * + ? )
Pro usnadnění zápisu jsou tři nejpoužívanější elementy počtů zkráceny do nasledujících znaků. Jejich použití je však identické jako u verze v předchozí kapitole, zapisují se za znak, skupinu, či zástupný symbol:
- Znak * – znamená “{0,}” – 0 nebo více výskytů
- Znak + – znamená “{1,}” – 1 a více výskytů
- Znak ? – znamená “{0,1}” – žádný nebo jeden výskyt
Element výčtu znaků (znaky hranatých závorek)
Jako jeden znak dokážeme teď uvést konkrétní písmeno nebo libovolný znak (zástupný symbol tečky). Bylo by fajn umět vytvořit výčet možných znaků.
Dá se to zařídit pomocí konstrukce z hranatých závorek ve formátu “[výčet znaků]”. Do nich uvedeme výčet (popřípadě rozsah) povolených znaků. Například výraz ”[abc]{2}” vyhledá výskyty kombinací písmen ve výčtu o délce 2 znaků, tedy: aa, ab, ac, ba, bb, bc, ca, cb, cc.
Do hranatých závorek je možné umístit i rozsahy a to buď číselné nebo písmenové. Například “[0-4a-cz]” – reprezentuje znaky 0, 1, 2, 3, 4, a, b, c, z.
Opakem je výčet s prvním znakem ^ ve formátu. “[^výčet znaků]”. V tomto případě znak stříšky neoznačuje začátek řetězce, ale negaci výčtu – tedy zastupuje libovolný znak, krom těch ve výčtu (například “[^0-9]” zastupuje všechny znaky, krom čísel).
Element pojmenovaných skupin
S tím, co již známe by jsme měli být schopni sestavit i velmi pokročilé regulární výrazy. Poslední co probereme ohledně zápisu RV budou pojmenované skupiny.
Pokud najdeme v textu shodu, dokážeme jí lokalizovat (podle indexu začátku shody). Ne vždy ale shoda je výsledek, který chceme získat. Jednoduchý příklad může být třeba ini soubor s řádky ve formátu “Klíč=Hodnota”. K nalezení stačí výraz “[^=]+=.+”. Jeho rozložením zjistíme, že hledá hodnotu začínající textem bez znaku “=”, pokračující právě znakem “=” a končí obyčejným textem. Výraz si můžeme rozložit na skupiny: “([^=]+)=(.+)”, funcionalita zůstává stejnou. Nyní zapíšeme definice pojmenované skupiny za otevírací závorky skupiny ve formátu “?<jméno skupiny>” – tím ji pojmenujeme. Můžeme to udělat třeba takto: “(?<klíč>[^=]+)=(?<hodnota>.+)”.
To znamená, že pokud prozkoumáme text “Resolution=1280x1024”, získáme shodu ve které najdeme 2 pojmenované skupiny s hodnotou “klíč”=”Resolution” a “hodnota”=”1280x1024”. Díky tomu máme již i shodu rozdělenou na části, které nás budou zajímat a které můžeme rovnou přečíst.
Jak skupiny číst si ukážeme v kapitole implementace.
Přehled regulárních výrazů s příklady
Nakonec slibovaný stručný přehled všech důležitých elementů regulárních výrazů:
Výraz |
Popis |
Příklad výrazu |
Odpovídající řetězce |
^ |
Začátek textu |
^abc |
abc, abcd, abc123… |
$ |
Konec textu |
x$ |
abcx, x, aaax… |
(…) |
Logická skupina |
(123)+ |
123, 123123, 123123123… |
[…] |
Výčet možných znaků |
[0-9] |
1, 2, 4, 5, 6… |
[^…] |
Obrácený výčet znaků |
[^0-9] |
a, b, z, x… |
{…} |
Pevný počet výskytů |
ab{3}c |
abbbc |
{…,…} |
Rozsah počtu výskytů |
ab{1,2}c |
abc, abbc |
{…,} |
Minimální počet výskytů |
ab{2,}c |
abbc, abbbc, abbbbc… |
* |
Žádný nebo více znaků |
A*hoj |
hoj, Ahoj, AAhoj, AAAhoj… |
+ |
Jeden nebo více znaků |
A+hoj |
Ahoj, AAhoj, AAAhoj… |
? |
Žádný nebo jeden znak |
A?hoj |
Ahoj, hoj |
(?<…>…) |
Pojmenovaná skupina |
(?<skupina>[0-9]+) |
1, 4, 53, 634… |
…|… |
Alterace - více možností |
A(hoj|uto|aa) |
Ahoj, Auto, Aaa |
. |
Libovolný znak (krom \n) |
A.oj |
Aaoj, Aboj, Acoj, Adoj… |
\t |
Zastupuje znak tabulátoru |
|
|
\r |
Zastupuje návratu hlavy |
|
|
\n |
Zastupuje nový řádek |
\r\n |
{konec řádku} |
\w |
Ekvivalent [a-zA-Z_0-9] |
\w+ |
sba34, 45, A1, fgBc… |
\W |
Ekvivalent [^a-zA-Z_0-9] |
\W+ |
\, –, +, *… |
\d |
Ekvivalent [0-9] |
\d+ |
753, 4, 678, 3… |
\D |
Ekvivalent [^0-9] |
\D+ |
ahoj, abc, df… |
\s |
Zahrnuje neviditelné znaky |
|
|
\S |
Zahrnuje viditelné znaky |
\S+ |
Ahoj123, asd-dgg, fb… |
Přehled jsem sestavil na základě http://regexlib.com/CheatSheet.aspx.
Implementace v .NET
Vždy budeme vycházet ze třídy System.Text.RegularExpressions.Regex. Její Instance reprezentuje vykompilovaný výraz. Nabízí několik statických metod pro přímý dotaz na shody bez zbytečného kódu okolo:
' vykompilovat a spustit RV
Dim shody = System.Text.RegularExpressions.Regex.Matches("řetězec", "výraz")
Pokud ale voláme stejný RV vícekrát, raději vytvoříme jeho instanci jen jednou. Ta má pak prakticky identické metody jako jsou ty statické. Jen ušetříme čas parsování výrazu a jeho kompilace:
' vykompilování výrazu
Dim regex As New System.Text.RegularExpressions.Regex("výraz")
Dim shody1 = regex.Matches("řetězec 1") ' spustit vykompilovaný RV proti textu
Dim shody2 = regex.Matches("řetězec 2") ' spustit vykompilovaný RV proti textu
Dim shody3 = regex.Matches("řetězec 3") ' spustit vykompilovaný RV proti textu
V případě, kdy chceme vrátit jen první shodu, použijeme místo Matches jen Match.
A když nepotřebujeme vědět podrobnosti o shodách, ale jen jestli odpovídá, použijeme IsMatch:
Dim shodujeSe = System.Text.RegularExpressions.Regex.IsMatch("Ahoj! Jak je?", "^Ahoj")
Další funkce, kterou jsem zatím nezmiňovat je nahrazování pomocí Replace. Ta nahradí všechny shody za zadaný text, například:
Dim nahrazenyText = System.Text.RegularExpressions.Regex.Replace("Ahoj! Jak je?", "^Ahoj", "Nazdar")
' vrátí z původního "Ahoj! Jak je?" zaměněný řetězec "Nazdar! Jak je?"
Vraťme se ale ke kolekci shod. Uchovávají se v MatchCollection a jsou datového typu Match. Když neobsahují žádnou položku (Count = 0), nenastala žádná shoda. Pokud ale nastala a shody procházíme, budou nás zajímat tyto vlastnosti:
- Index – počáteční index znaku ve vstupním textu, kde byla shoda nalezená
- Length – textová délka shody
- Value – řetězcová hodnota, která reprezentuje text shody
- Groups – hodnoty pojmenovaných i běžných skupiny
Pro ukázku uvádím kód, co najde všechny slova v textu. Použil jsem regulárního výrazu “\w+”, tedy 1 a víe písmen bez mezer a jiných oddělovačů, tím způsobem lze snadno najít oddělené skupiny písmenek – slov:
' vstupní text
Dim text As String = "Ahoj, tohle je ukázkový text. Jsou to dvě věty!"
Console.WriteLine("Vstupní text: {0}", text)
' najít shody s RV hledající slova
Dim shody As MatchCollection = Regex.Matches(text, "\w+")
' projít a vypsat shody
For Each shoda As Match In shody
Console.WriteLine("Nalezeno slovo na pozici {0}, text: {1}", shoda.Index, shoda.Value)
Next
Console.ReadLine()
A nakonec kód, který přečte pojmenované skupiny (použiji trochu upravený, již zmiňovaný výraz pro parsování INI souborů):
' text INI souboru se 2ma řádkama
Dim text As String = "Klíč1=Hodnota1" + Environment.NewLine + "Klíč2=Hodnota2"
Console.WriteLine("Vstupní text:")
Console.WriteLine(text)
' vyhledat řádky
Dim shody As MatchCollection = Regex.Matches(text, "^(?<klíč>[^=]+)=(?<hodnota>[^\r\n]+)?", RegexOptions.Multiline)
' projít a vypsat shody podle pojmenovaných skupin
For Each shoda As Match In shody
' načíst hodnoty pojmenovaných skupin
Dim klic As String = shoda.Groups("klíč").Value
Dim hodnota As String = shoda.Groups("hodnota").Value
' vypsat výsledek
Console.WriteLine("Nalezen řádek s klíčem '{0}' a hodnotou '{1}'.", klic, hodnota)
Next
Console.ReadLine()
Ještě se pozastavím na parsovacím výrazem. Začínáme znakem “^”, tedy začátkem řádku. Pak vyhledáváme text klíče bez znaku “=”, až na “=” opravdu narazíme. Dále pokračujeme textem hodnoty, než narazíme na konec řádku (“\r\n”).
Způsoby spuštění regulárního výrazu
Každé vyhodnocení shod můžeme spustit s jinými příznaky. Je to nepovinný parametr funkcí Matches, Match, IsMatch, Replace. Příkad předání můžeme vidět v předchozím příkladu při volání funkce Matches. Nejdůležitější se mi jeví tyto tři:
- RegexOptions.Multiline – text s více řádky, začátek řádku je při jeho použití možné identifkovat znakem počátku “^”
- RegexOptions.IgnoreCase – ignoruje se velikost písmen
- RegexOptions.RightToLeft – text se prochází obráceně, z prava doleva
Závěr a soutěž
Jestli jste se dočetli až sem, je to obdivuhodné, děkuji za pozornost. Jakékoliv dotazy klaďte do diskuze, bral jsem to všechno dost rychle.
Soutěž o dvě knihy Visual Basic 2008! Build a program now je již uzavřena! Za úkol byly tyto 3 úlohy:
Úloha 1 – Napiště regulární výraz, který najde kdekoliv v textu tuto doslovnou větu: “Umístění je C:\Soubor.exe?”
Řešení - “Umístění je C:\\Soubor\.exe\?” – znaky zpětného lomítka, tečky a otazníku se museli ošetřit únikovou sekvencí.
Úloha 2 – K čemu by se dal použít výraz: “^http://(?<path>.+)/(?<file>[^\?/]*)(?<query>\?.+)?$”
Řešení – Tento výraz rozparsuje http URL adresu. Jméno serveru a adresář, do kterého se odkazujeme se vloží do pojmenované skupiny path, název souboru do file a případný query string do query.
Například “http://server.cz/slozka/podslozka/soubor.aspx?id=34” bude rozdělen na:
- path = ”server.cz/slozka/podslozka”
- file = ”soubor.aspx”
- query = ”?id=34”
Úloha 3 – Napište RV, co vyhledá čas v textu ve formátu “06:48:23” a jednotlivé části přiřadí do pojmenovaných skupin nazvaných hodiny, minuty, vteriny.
Řešení – Dobře vyřešená úloha nekontrolovala jen výskyt dvojic čísel oddělených dvojtečkou, ale i jejich rozsahy. Víme, že hodiny budou v rozsahu 0-23, minuty 0-59 a vteřiny také 0-59. Uvádím identické úlohy od obou soutěžících – rozdíl je jen v zápisu:
- “(?<hodiny>[01]\d|2[0-3]):(?<minuty>[0-5]\d):(?<sekundy>[0-5]\d)”
- “(?<hodiny>([01][0-9])|(2[0-3])):(?<minuty>[0-5][0-9]):(?<vteriny>[0-5][0-9])”
Úspěšnými řešiteli byli Pavel Veselý a Viktor Langer. Gratuluji!