Další Předchozí Obsah

9. Regulární výrazy

Unix bez regulárních výrazů je jako sex bez partnera/partnerky. Dá se to používat, ale člověk o cosi zásadního přichází. Znalost regulárních výrazů vám dá do rukou mimořádně silný nástroj pro práci s textem.

?P:Zkrátka regulární výraz je univerzální pomocník při práci s textem. Používá jej celá řada programů v Unixu. Umožňují prohledávat soubory (grep, egrep), editovat je (sed, vi), analyzovat a vypočítávát zajímavé údaje (awk) či nabízejí plnohodnotný programovací jazyk, kde si můžete dělat, co vás napadne (Perl, Tk).

9.1 Jednoduché výrazy

Nejjednodušším regulárním výrazem je obyčejné písmeno - třeba r. Když se v textu hledá řetězec, který by tomuto regulárnímu výrazu vyhověl, hledá se jednoduše písmeno "r". Implicitně se (jak bývá v Unixu zvykem) rozlišují malá a velká písmena. Ve většině nástrojů však můžete tuto vlastnost vypnout.

Jelikož i v těch nejjednodušších případech člověk zpravidla hledá slovo a ne jediné písmeno, lze regulární výrazy řetězit. Použijete-li regulární výraz root, představuje vlastně zřetězení čtyř elementárních jednopísmenných regulárních výrazů. Výsledkem je chování, které byste očekávali - v textu se bude hledat slovo "root".

Vyhledávání jednoduchých slov je tou nejprimitivnější, ale zároveň nejčastější aplikací regulárních výrazů.


Řekněme, že hledáte nejčerstvější soubory v aktuálním adresáři. Nevím jak vy, ale já v hlavě nenosím, jak se jmenuje volba příkazu ls, která zajistí uspořádání podle času. Takže zadám man ls a následně si pomocí /time nechám vyhledat první výskyt slova "time". Nebudu-li spokojen, stisknu klávesu n a poskočím tak na další výskyt.
Popsané hledání založené na regulárních výrazech dovedou oba programy používané obvykle pro zobrazování manuálových stránek (a řady dalších textů): more i less. Tyto programy zároveň ilustrují jeden obecný princip: regulární výraz se typicky vyhledává jako podřetězec v jednotlivých řádcích textu.

9.2 Libovolný znak

Poměrně často dochází k situacím, kdy vám na určité části hledaného řetězce nezáleží. Například chcete ve zdrojovém textu HTML stránky vyhledávat začátky buněk v tabulkách - čili značky a . Až na třetí znak jsou oba řetězce shodné, takže je lze vyhledávat jediným regulárním výrazem. Pouze je třeba říci, že na jeho třetím znaku nezáleží.

Tuto činnost obstará znak tečka (.). Při hledání jí vyhoví libovolný znak kromě konce řádku. Nelze ji však ignorovat - nějaký znak jí program vždy musí přiřadit.


Výše zmíněné hledání řetězců ""<TD>" či "<TH>" obstará regulární výraz <T.>. Přesněji řečeno mu vyhoví libovolný čtyřznakový řetězec, který začíná "<T" a končí znakem ">".

9.3 Ne až tak libovolný znak

Použitím tečky zcela rezignujete na hodnotu příslušného znaku. V některých případech se to hodí, jindy byste však potřebovali výběr omezit přísněji. Pak můžete sáhnout po hranatých závorkách.

Zapíšete-li do hranatých závorek skupinu znaků, bude tomuto regulárnímu výrazu vyhovovat právě jeden z těchto znaků. Například výrazu [xyz] vyhoví buď znak "x" nebo "y" nebo "z". Jestliže povolené znaky tvoří interval, můžete si ušetřit práci a v hranatých závorkách uvést pouze jeho meze, které spojíte pomlčkou.


Pro vyhledání libovolné číslice poslouží regulární výraz [0-9]. Předchozí hledání ""<TD>" a "<TH>" bylo příliš benevolentní, protože za T povolovalo libovolný znak. Lepší je regulární výraz <T[DH]>, který se skutečně omezí jen na uvedené dvě značky.
Jednotlivých znaků a jejich intervalů můžete do hranatých závorek napsat, co hrdlo ráčí. Například výrazu [a0-9zl-nt] vyhoví libovolné z písmen a, l, m, n, t, z nebo libovolná číslice.

Kromě pomlčky se v hranatých závorkách vyskytuje ještě jeden speciální znak. Pokud hned za otevírací hranatou závorkou zapíšete stříšku (^), bude celá skupina negována. To znamená, že regulárnímu výrazu vyhoví libovolný znak odlišný od těch, které jsou uvedeny ve skupině. Například [^0-9] vyhoví cokoli kromě číslice.

Intervaly znaků vycházejí z kódování ASCII. To znamená, že například výrazu [a-z] vyhoví libovolné malé písmeno anglické abecedy. Doplnit velká písmena není žádný velký problém ([a-zA-Z]), ale s českými znaky je potíž. V některých programech najdete konstrukce, kterým vyhoví i znaky české abecedy, univerzálně platné elegantní řešení však neexistuje.

9.4 Speciální znaky

Možná už vás napadlo "ale co když potřebuji vyhledat tečku?" Tedy obecněji: jak vyřadit speciální význam některých znaků. Obecná odpověď na tuto otázku zní "zpětným lomítkem". V Unixu bývá zvykem, že pokud speciálnímu znaku předřadíte zpětné lomítko, vypnete tak jeho speciální chování (a v některých případech právě naopak, jak uvidíte později).

Celkem pohledný regulární výraz \.\.\. hledá tři tečky. Chcete-li vyhledat písmeno uzavřené v hranatých závorkách (tedy cosi jako "[x]"), použijte \[[a-z]\].

9.5 Opakování výrazu

Základní konstrukcí pro opakování regulárních výrazů je hvězdička (*). Znamená, že regulární výraz bezprostředně před ní se může zopakovat, kolikrát to jenom jde.

Výrazu A* tedy vyhoví libovolný počet písmen "A", zatímco [0-9]* ztělesňuje libovolně dlouhou posloupnost číslic (opakovaným regulárním výrazem je zde [0-9], tedy libovolná číslice). V řadě programovacích jazyků je identifikátor definován jako libovolně dlouhá posloupnost písmen a číslic začínající písmenem. Pomocí regulárního výrazu bychom jej zapsali jako [a-zA-Z][a-zA-Z0-9]*. Zdůrazňuji, že opakování se týká jen regulárního výrazu, který je uveden bezprostředně před hvězdičkou. Uvedený výraz tedy znamená "právě jeden výskyt [a-zA-Z] (písmeno), za nímž následuje libovolný počet výskytů [a-zA-Z0-9] (písmeno nebo číslice)".
Snad nejběžnějším opakovaným výrazem je tečka, která v kombinaci s hvězdičkou (.*) znamená "libovolný řetězec znaků". V souvislosti s opakováním si dobře zapamatujte tři důležité skutečnosti:

Přípustnost nulového počtu opakování znamená, že opakovanému regulárnímu výrazu vždy může vyhovět i prázdný řetězec. Praktickým důsledkem je, že jen vzácně dává smysl vyhledávat samotný opakovaný výraz. Zpravidla je třeba jej alespoň z jedné strany ohraničit něčím povinným.


Chcete-li vyhledat v textu všechna čísla, nemá smysl hledat "libovolně dlouhou posloupnost číslic" ([0-9]*), protože posloupnost číslic nulové délky obsahuje každý řádek (vyzkoušejte grep '[0-9]*' soubor na libovolný soubor - uvidíte, že "najde" všechny jeho řádky). Správně je třeba hledat "alespoň jednu číslici", tedy použít regulární výraz [0-9][0-9]*.
Skutečnost, že opakování se týká regulárního výrazu, nikoli srovnávaného řetězce, je velmi důležitá. Zapíšete-li .*, spojujete dva prvky: symbol pro libovolný znak a symbol opakování. Výslednou konstrukci lze chápat dvěma způsoby. Buď jako libovolný počet libovolných znaků (opakování regulárního výrazu) nebo že v textu může být libovolný znak a ten se pak může opakovat, kolikrát chce (opakování ve zkoumaném řetězci). Správný je první výklad, jinak bychom se z toho nejspíš zbláznili.

Hladovost opakování se projevuje tím, že opakovaný regulární výraz se vždy snaží roztáhnout na co největší délku - zahrnout do sebe co největší počet znaků zkoumaného řetězce. Proto když například řetězec "brambora" srovnáte s regulárním výrazem r.*a (libovolný řetězec znaků začínající "r" a končící "a"), bude vyhovujícím řetězcem "rambora" (od prvního "r" až po poslední "a").

Příliš hladové opakování

Na hladovost opakování je třeba si dávat pozor. Díky ní se snadno může stát, že opakující se výraz pohltí i ty znaky, se kterými jste nepočítali. Klasickým příkladem tohoto chování je regulární výraz pro řetězec znaků v uvozovkách.

Začátečníci mají často tendenci uvažovat následovně: řetězec v uvozovkách, to jsou otevírací uvozovky, pak cokoli a na konci druhé uvozovky. To vyjádříme regulárním výrazem ".*". Problém je, že díky hladovosti hvězdičky se tento regulární výraz roztáhne od prvních uvozovek ve zkoumaném řetězci až po poslední. Takže například když jej vypustíte na řetězec 'Volali "Ahój" a "Nazdár".', dopadne to takto:


"Ahój" a "Nazdár"

Řešením je nepřipouštět v uzavřeném řetězci libovolné znaky, ale pouze znaky jiné než koncový. V našem případě cokoli kromě uvozovek, takže tím správným regulárním výrazem bude "[^"]*":

9.6 grep a spol.

Rodina programů *grep slouží k vyhledávání v souborech. Typické použití: hledáte určitý identifikátor v haldě zdrojových kódů nebo chcete zjistit, odkud se spouští určitý program. Spuštění je prosté:

$ grep vzor seznam_souborů

Vzorem je regulární výraz. Výstup programu tvoří řádky, které vyhovují zadanému vzoru (což nejčastěji znamená, že obsahují zadané slovo). Pokud program zkoumá více než jeden soubor, vypíše zároveň před každý řádek název souboru, ze kterého pochází. Prostřednictvím voleb lze ovlivnit jeho chování. Těmi nejběžnějšími jsou:

-inerozlišovat malá písmena od velkých
-wvybírat jen řádky, na nichľ vzoru vyhovuje celé slovo
-vnegovat výsledek (vypisovat řádky, které nevyhovují vzoru)
-lvypisovat jen jména souborů
-rrekurzivně procházet adresáře (umí jen některé verze grepu)

grep je představitelem celé rodinky programů. Mají podobná jména i funkci, liší se jen v detailech. Jejími standardními členy jsou tyto tři programy:

grep klasický grep, vzorem můľe být obyčejný regulární výraz
egrep vzorem je rozšířený regulární výraz (viz příště), pouľívá rychlejší vyhledávací algoritmus
fgrep vzorem je jen obyčejný řetězec znaků; teoreticky nejrychlejší, ale praktická měření ukazují opak; zapomeňte na něj

Kromě nich existují ještě některé další pozoruhodné alternativy. Asi nejzajímavější je agrep (approximate grep) vyhledávající řetězce, které se zadanému vzoru pouze podobají. Najde například nejpodobnější nebo všechny takové, které se od vzoru liší jen v daném počtu znaků.

Vřele doporučuji používat grep a spol. pocházející z GNU projektu. Ve srovnání s klasickými implementacemi je rychlejší (používá lepší algoritmy) a navíc umí některé příjemnosti (třeba rekurzivní hledání). Zejména komerční verze Unixu však mají tendenci trvat na originálních verzích.

9.7 sed

Program sed je neinteraktivní editor. Zadáte mu sadu příkazů a on podle nich zpracuje vstupní text. De facto se jedná o nástroj pro vytváření editačních filtrů.

Regulární výrazy se v sedu vyskytují hned ve dvojí roli. Lze je použít k vyhledání řádků, na které se má vztahovat určitý příkaz. Druhým místem výskytu regulárních výrazů je příkaz pro nahrazování, který má tvar s/vzor/náhrada/. Vyhledává regulárním výrazem zadaný vzor a pokud jej najde, vloží na jeho místo náhradu. Za závěrečné lomítko můžete připojit ještě volby. Tou nejpoužívanější je g (global), která zajistí nahrazení všech výskytů vzoru na řádku. Standardně se totiž nahrazuje jen první.

Příkazy sedu mají obecně tvar


řádky příkaz

Počáteční definice řádků určuje, na které řádky vstupního textu se příkaz bude vztahovat (chybí-li, znamená to všechny řádky).

Řekněme, že jste změnili doménu z kdesi.cz na jinde.cz. Navíc chcete ze svých WWW stránek odstranit pracovní texty - tedy veškeré úseky začínající <DIV CLASS="pracovni"> a končící </DIV>. Zajistí to následující dvojice příkazů:


s/kdesi\.cz/jinde\.cz/g
/<DIV CLASS="pracovni">/,/<\/DIV>

První je klasické nahrazení, které se bez určení řádků vztahuje na celý vstupní text. Druhým příkazem je d (delete), kterému podlehnou všechny skupiny řádků od řádku vyhovujícího prvnímu regulárnímu výrazu až po nejbližší následující, který vyhovuje druhému. Všimněte si, že lomítko v </DIV> je třeba chránit zpětným lomítkem, protože v příkazu s má speciální funkci oddělovače. Tuto dvojici uložíte řekněme do souboru zmena a každou stránku pak podrobíte příkazu

$ sed -f zmena

9.8 Omezený počet opakování

Základním problémem klasické opakovací hvězdičky je, že je nekontrolovatelná. Pro některé situace potřebujete přesnější vyjadřování.

Vaše touhy uspokojí konstrukce \{min,max\}. Opět se vztahuje na bezprostředně předcházející regulární výraz a říká, že se má opakovat alespoň min-krát, nanejvýš však max-krát. Jako každé opakování i tohle je hladové, takže se snaží uplatnit vždy co největší z povoleného počtu opakování.

Tvar tohoto opakovátka je velmi variabilní. Pokud chybí horní mez (\{min,\}), znamená to, že maximální počet opakování je neomezený. Jestliže v konstrukci použijete jen samotné číslo (\{počet\}), musí se regulární výraz opakovat přesně daný počet-krát.


Regulární výraz pro rodné číslo by vypadal takto:
[0-9]\{6\}/[0-9]\{3,4\}
Šest číslic, lomítko a ještě tři nebo čtyři číslice.

9.9 Nejpopulárnější opakovačky

Dva velmi populární případy opakování si vysloužily svůj vlastní speciální znak. Prvním je "alespoň jeden výskyt" - tedy cosi velmi podobného klasické opakovací hvězdičce, až na to, že opakovaný regulární výraz nelze vynechat. Stejného efektu dosáhnete konstrukcí \{1,\}, ale to je příliš složité psaní. Proto se alespoň jeden výskyt předchozího regulárního výrazu zapisuje znakem plus (+).

Druhou populární situací je nepovinný (čili nanejvýš jeden) výskyt. Opět jej lze zapsat pomocí \{0,1\}, ale kratší je otazník (?). Dialekty regulárních výrazů se u této dvojice znaků opět silně rozcházejí. Programy používající klasické regulární výrazy (grep, sed, vi) jim předřazují zpětné lomítko (\+ a \?). Generace, která implementuje rozšířené regulární výrazy, (egrep, awk, Perl) je píše bez něj (+ a ?).

9.10 Pozice

Všechny naše dosavadní regulární výrazy byly poněkud neukotvené. Řetězec, který jim vyhovuje, se mohl vyskytovat kdekoli ve zkoumaném textu. Občas však člověk musí být přísnější.

Proto regulární výrazy nabízejí několik speciálních pozičních znaků. Těmi nejznámějšími jsou stříška (^), která ztělesňuje začátek řádku (resp. zkoumaného řetězce znaků), a dolar ($) označující jeho konec.


grep '^#' vám tedy najde řádky začínající znakem '#', grep '[0-9]$' řádky končící číslicí a konečně grep '^-\+$' řádky složené pouze z pomlček (nikoli však prázdné).
Dalším významným místem je hranice slova. Ve většině regulárních dialektů máte k dispozici konstrukci \<, která označuje začátek slova, a \>, které vyhoví pouze jeho konec. Zajímají-li vás všechny řádky, na nichž se písmeno 'a' vyskytuje v roli jednopísmenné spojky, nasaďte

$ grep '\' soubor

9.11 Zapamatuj a vzpomeň si

Prostředky pro zapamatování jsou směšně jednoduché. Část, kterou si má regulární výraz podržet v paměti, prostě ohraničíte konstrukcemi \( a \).

Když později chcete použít zapamatovaný řetězec, napište \číslo, kde číslo je pořadové číslo zapamatovaného úseku. Pořadová čísla začínají jedničkou a rozhoduje o nich pořadí levé (otevírací) závorky zapamatovávané sekvence.


Podrobíte-li řádek ze souboru /etc/passwd regulárnímu výrazu ^\([^:]*\):[^:]*:\([^:]*\), bude \1 obsahovat přihlašovací jméno a \2 jemu odpovídající identifikátor (UID).
Zkusím něco, co by alespoň vzdáleně připomínalo reálný život. Řekněme, že máte výstupy z jakéhosi algoritmu - na každém řádku sadu čísel oddělených mezerami. Každý řádek zároveň končí správným výsledkem. Pokud algoritmus pracuje správně, poslední dvě čísla na řádku jsou totožná. Hledáme tedy řádky, ve kterých se poslední číslo liší od předposledního. K řešení poslouží grep s negovanou podmínkou (volba -v): Vzor začíná mezerou před předposledním číslem. Za ní následuje neprázdná posloupnost nemezerových znaků ([^ ]\+), která se zapamatuje. Po ní následuje alespoň jedna mezera ( \+) a znovu stejná posloupnost, za kterou už je jen konec řádku.

Použití při nahrazování

Daleko častěji se zapamatované řetězce vyskytují v příkazech pro nahrazování. Díky nim si můžete ze vstupních dat vytáhnout informace, které vás zajímají, a poskládat si je do tvaru, který potřebujete.

Běžný problém všedního dne: potřebujete u skupiny souborů změnit příponu z .htm na .html. Pro podobné účely sice existují různá udělátka, ale je třeba si je doinstalovávat a práce s nimi nebývá úplně snadná. takže se podívejme, jak poslouží standardní nástroje, které najdete v každém Unixu.

Postup je jednoduchý: obstaráte si seznam jmen souborů, každé jméno pak změníte na příkaz mv staré nové a tyto příkazy provedete. Popsaný postup lze realizovat třeba takto:


$ ls *.htm > seznam
$ sed 's/\(.*\)/mv \1 \1l/' seznam > akce
$ chmod a+x akce
$ ./akce
$ rm seznam akce 

A teď něco drsnějšího. Chtěl bych ze souboru /etc/passwd vyrobit seznam domácích stránek uživatelů. Takže potřebuji řádky transformovat z původní podoby

uživatel:heslo:UID:GID:vlastní jméno:... 

na

<A HREF="/~uživatel">vlastní jméno</A> 

Kýženým substitučním příkazem, který to zařídí, je

s/\([^:]*\):\([^:]*:\)\{3\}\([^:]*\).*/<A HREF="/~\1">\3<\/A>/ 

9.12 Skupiny

Jazyky nabízejí řadu konstrukcí, které sice nepřinášejí nějakou zásadní inovaci stran schopností regulárních výrazů, ale v běžném životě silně potěší. Asi nejčastěji používané jsou kategorie znaků. Z těch nejběžnějších lze jmenovat:

Zápis Význam Odpovídá
\d číslice [0-9]
\D nečíslice [^\d]
\w alfanumerický znak [a-zA-Z_0-9]
\W nealfanumerický znak [^\w]
\s prázdný znak [\ \t\n\r]
\S neprázdný znak [^\s]

Tyto speciální znaky celkem výrazně přispívají ke srozumitelnosti regulárních výrazů. Kromě nich je leckde podporován i zápis kategorií podle POSIXu, kde se například číslice zapisuje jako [:digit:], alfanumerický znak jako [:alnum:] a prázdné místo [:space:]. Tyto kategorie se však zapisují mezi [], takže například řádek začínající číslicí se vyjádří pomocí ^[[:digit:]], což už má k eleganci poměrně daleko.

Jako závěrečnou přílohu vám nabízím stručný přehled regulárních výrazů v nejběžnějších nástrojích. Konkrétně se jedná o GNU grep a egrep verze 2.3, GNU awk verze 3.0.4, vim verze 5.5 a Perl verze 5.005_03.


Další Předchozí Obsah