Regulární výrazy (5)

9. 5. 2000
Doba čtení: 4 minuty

Sdílet

Na pátou část seriálu jsem si pošetřil snad nejsilnější prvek regulárních výrazů - jejich paměť. Regulární výraz si totiž dokáže zapamatovat řetězec, který vyhověl jeho části, a později jej použít. Největší služby tento mechanismus odvede při nahrazování.

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 ). Jistě si vzpomínáte, že v Perlu nemá nealfanumerický znak předcházený závorkou nikdy speciální význam, takže tam se ke stejnému účelu používají obyčejné závorky. Takových úseků můžete v regulárním výrazu mít hned několik. Dokonce je lze i vzájemně vnořovat.

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.

Příklad:

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 (U­ID).

O dobrodiní paměti jste ochuzeni u programů egrep a awk. Jejich algoritmus pro srovnávání regulárních výrazů nepodporuje zapamatování (a z principiálních důvodů ani podporovat nemůže). V příští části se k této otázce vrátím podrobněji.

ict ve školství 24

Použití při hledání

Použití zapamatovaných částí při vyhledávání je poměrně vzácné. Řečeno mluvou politiků: společenská potřeba takové služby je celkem malá. Příklady sice existují, nicméně bývají takové školometské. Tak si nějaké zkusíme.

Příklad:

Hezkou a zcela neužitečnou ilustrací zapamatování je hledání palindromů (slov, která se nezmění, když je čtete pozadu). Tak například výrazu

\<\(.\)\(.\).\2\1\>

vyhoví právě a pouze pětiznakový palindrom (např. radar či rotor). První dva znaky slova si zapamatuje, za nimi následuje třetí libovolný znak, čtvrtý musí být stejný jako zapamatovaný druhý a pátý znak se musí shodovat s prvním.

Příklad:

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):

grep -v ' \([^ ]\+\) \+\1$'

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.

Příklad:

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

Uznávám, že prosté připojení „l“ na konec jména souboru je dosti snadnou modifikací. Složitější věci však znamenají jen úpravu příkazu s. Například změnu přípony z .doc na .txt by zajistilo s/\(.*)\.doc/mv \1­.doc \1.txt/ – zapamatuje si jen vlastní jméno souboru a přípony jsou explicitně vyjmenovány.

Příklad:

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>/

Jak vidíte, prostřednictvím závorek lze předepsat počet opakování i rozsáhlejšímu úseku regulárního výrazu - zde se třikrát opakuje skupina [^:]*:. V takovýchto situacích závorky většinou neslouží k zapamatování (i když si pochopitelně něco zapamatují), ale čistě k vymezení opakované části. Ta má - bez ohledu na skutečný počet opakování - jen jediné pořadové číslo. Proto se závěrečné zapamatované jméno uloží jako \3.

Problémem regulárních výrazů je, že jsou velmi kompaktní. Vyznat se ve výše citovaném substitučním příkazu zabere chvíli času (zpravidla více, než jej vymyslet). V Perlu si můžete vytvoření seznamu rozložit: nejprve řádek rozkrájíte v místě výskytu dvojteček a z výsledného pole pak použijete první a pátý prvek (indexy 0 a 4):

while ( $radek = <> ) {
    @uzivatel = split(/:/, $radek);
    print "<A HREF=\"/~$uzivatel[0]\">";
    print "$uzivatel[4]</A>\n";
}

Program uložte třeba do souboru htmlseznam a spusťte

perl htmlseznam /etc/passwd

Shrnutí

výraz význam
\(výraz) zapamatuje si text vyhovující výrazu
\1 první zapamatovaný řetězec

Autor článku

Pavel Satrapa působí na Ústavu nových technologií a aplikované informatiky na Technické univerzitě v Liberci, píše knihy a motá se kolem tuzemské akademické sítě CESNET.