Regulární výrazy v praxi – subvýrazy

16. listopadu 2004

Regulární výrazy jsou mocným nástrojem, který může výrazně zjednodušit a zpřehlednit kód programu. Důležitou součástí práce s regulárními výrazy jsou takzvané subvýrazy. Subvýrazy jsou části regulárního výrazu, které umožňují zjednodušení zápisu běžného regulárního výrazu i tvorbu komplikovanějších výrazů, schopných inteligentně postihnout a manipulovat například i s řetězci, jejichž přesnou podobu nelze předem určit.

Pokud vám pojem regulární výrazy mnoho neříká, doporučuji vám přečíst si nejdříve předchozí články této série. Regulární výrazy je možno používat v mnoha (nejen) programovacích jazycích. My se v tomto článku omezíme na použití v PHP a na regulární výrazy dle normy POSIX 1003.2 (extended). Pro práci s takovými regulárními výrazy disponuje PHP třemi funkcemi (respektive šesti funkcemi, pokud počítáme i „case insensitive“ verze těchto funkcí). Tyto funkce byly také stručně popsány v jednom z předchozích článků.

Regulární výraz může být velmi účinný, i když nemá žádné subvýrazy. Použitím subvýrazů se však možnosti použití regulárních výrazů značně zvětší. Nejdříve si ukažme, jak vypadá regulární výraz obsahující subvýrazy. Výraz ^a(b(c))d(e)f$ obsahuje tři subvýrazy, které jsou dány uzávorkováním kulatými závorkami. Subvýrazy se označují (číslují) v pořadí podle pozice otevírací (levé) závorky a tak prvnímu subvýrazu bude odpovídat bc, druhému subvýrazu c a třetímu e.

Proč jsou subvýrazy tak užitečné a jak je můžeme využít? Napadají mě rovnou tři situace, kdy se budou subvýrazy hodit:

  1. Chceme definovat, že se určitý textový řetězec (sekvence znaků) má v rámci regulárního výrazu opakovat.
  2. Chceme zaměnit řetězec za jiný řetězec, který vznikne „přeskládáním“ původního řetězce.
  3. Chceme rozdělit (parsovat) řetězec na určité části, se kterými můžeme následně pracovat samostatně.

Na jednotlivé situace se podíváme blíže, a to vždy s použitím dvou příkladů. První (modelový) příklad je vždy jednodušší a má především demonstrovat princip. Druhý příklad je z běžného života (tedy spíše běžného života programátora).

První situace

Chceme definovat, že se určitý textový řetězec (sekvence znaků) má v rámci regulárního výrazu opakovat.

Modelový příklad

Chceme, aby regulárnímu výrazu odpovídaly řetězce, které začínají písmenem „a“, končí písmenem „d“ a mezi těmito písmeny se musí minimálně jedenkrát vyskytovat sekvence znaků „bc“. Takový regulární pak bude vypadat ^a(bc)+d$ a budou mu odpovídat řetězce jako abcd, abcbcd, abcbcbcd a podobně – abcbd však už vyhovovat nebude.

Příklad z praxe

Chceme vytvořit regulární, kterému by odpovídal dvouciferný i čtyřciferný zápis letopočtu 20. a 21. století. Použijeme regulární výraz ^(19|20)?[0-9]{2}$. První (a jediný) subvýraz (19|20) odpovídá prvním dvěma cifrám letopočtu 20. a 21. století. Kvantifikátor ?, který následuje bezprostředně za tímto subvýrazem, signalizuje, že řetězec odpovídající subvýrazu (tedy 19 či 20) nemusí být vůbec přítomen.

Pro úplnost doplním, že to, zda určitý řetězec odpovídá regulárnímu výrazu, zajistíme pomocí funkce ereg($pattern, $string), kde $pattern je regulární výraz a $string je testovaný textový řetězec. Funkce vrací TRUE, pokud $string odpovídá regulárnímu výrazu $pattern, jinak vrací FALSE.

Druhá situace

Chceme zaměnit řetězec za jiný řetězec, který vznikne „přeskládáním“ původního řetězce.

Modelový příklad

Máme k dispozici jméno osoby ve tvaru Jméno Příjmení a chceme ho zobrazit jako Příjmení Jméno. K tomuto převodu použijeme funkci ereg_replace($pattern, $replacement, $string), která zjistí, zda $string odpovídá regulárnímu výrazu $pattern a pokud ano, vrátí tato funkce řetězec $replacement. V opačném případě vrátí funkce $string (tedy původní řetězec).

Síla této funkce spočívá v tom, že $replacement může obsahovat řetězce odpovídající subvýrazům regulárního výrazu. Pomocí speciální konstrukce \\číslo je možno se odkazovat na subvýrazy regulárního výrazu $pattern, kde číslo odpovídá číslu subvýrazu, jak jsem jej popsal výše. Pokud použijeme funkci s parametry $pattern="^([^ ]+) ([^ ]+)$", $replacement="\\2 \\1" a jako $string „předhodíme“ funkci Jan Novák, potom nám funkce vrátí Novák Jan. Regulární výraz $pattern je tvořen dvěma subvýrazy oddělenými mezerou. Každému z těchto subvýrazů odpovídá sekvence znaků (o délce minimálně jeden znak) neobsahující mezeru (protože mezera nám slouží pro oddělení prvního a druhého subvýrazu – tedy jména a příjmení).

Příklad z praxe

Máme k dispozici (například v databázi) datum ve formátu MM/DD/RRRR a chceme ho převést na české datum ve formátu DD. MM. RRRR. Pokud použijeme funkci ereg_replace() s parametry $pattern="^([0-9]{2})/([0-9]{2})/((19|20)[0-9]{2})$", $replacement="\\2. \\1. \\3" a jako datum ($string) „předhodíme“ funkci 12/05/2002, potom nám funkce vrátí 05. 12. 2002. Výraz $pattern má čtyři subvýrazy. Podstatné první tři jsou subvýrazy, které postihují měsíc, den a rok. V řetězci $replacement pak už jen stačí prohodit první a druhý subvýraz a doplnit za čísly tečky místo původních lomítek.

Jak vás asi napadlo, použití $pattern="^([0-9]{2})/([0-9]{2})/((19|20)[0-9]{2})$" není příliš kvalitní řešení, protože považuje za validní datum například i řetězec 76/54/2002. Proto upravíme subvýraz pro měsíc na (0[1-9]|1[0-2]) a subvýraz pro den v měsíci na (0[1-9]|[12][0-9]|3[01]) a dostaneme tak regulární výraz ^(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])/((19|20)[0-9]{2})$. Pokud bychom chtěli akceptovat i datum, kde jednociferným číslům dnů či měsíců může (ale nemusí) předcházet nula, pak stačí za 0 v regulárním výrazu doplnit kvantifikátor ?. Celý regulární výraz pak bude ^(0?[1-9]|1[0-2])/(0?[1-9]|[12][0-9]|3[01])/((19|20)[0-9]{2})$.

Třetí situace

Chceme rozdělit (parsovat) řetězec na určité části, se kterými můžeme následně pracovat samostatně.

Modelový příklad

Máme k dispozici jméno osoby ve tvaru Jméno Příjmení. My však potřebujeme pracovat zvlášť se jménem a zvlášť s příjmením. K tomu opět použijeme funkci ereg($pattern, $string, $regs). Nepovinný parametr $regs udává pole, do nějž budou uloženy části řetězce $string odpovídající jednotlivým subvýrazům regulárního výrazu $pattern. Pokud použijeme funkci s parametry $pattern="^([^ ]+) ([^ ]+)$" a $string="Jan Novák", jednotlivé části jména pak budeme mít dostupné v poli $regs.

Pokud funkci „předhodíme“ jako $string například Jan Novák, výpis obsahu pole $regs pomocí funkce var_dump() bude vypadat následovně:

array(3) {
  [0]=>
  string(9) „Jan Novák“
  [1]=>
  string(3) „Jan“
  [2]=>
  string(5) „Novák“
}

Příklad z praxe

Máme k dispozici poštovní adresu, respektive část adresy udávající název ulice, číslo orientační, případně číslo popisné. A naším cílem je získat tyto jednotlivé části. Takový požadavek může typicky nastat při změně struktury databáze s adresami. Budeme předpokládat, že číslo orientační je uvedeno vždy a číslo popisné být uvedeno může, ale nemusí. Pokud však je číslo popisné uvedeno, pak jsou čísla uvedena ve tvaru číslo_popisné/číslo_orientační (jako na občanském průkazu). Regulární výraz se tak bude skládat ze tří hlavních částí – názvu ulice, čísla popisného (nepovinně) a čísla orientačního.

Popsaný problém řeší regulární výraz ^(.*[^0-9]+) (([1-9][0-9]*)/)?([1-9][0-9]*[a-cA-C]?)$. První subvýraz popisuje název ulice – tedy textový řetězec o minimální délce jeden znak, který nesmí končit číslicí (zajistíme tak, že se do názvu ulice nezahrne i číslo popisné, respektive orientační). Druhý subvýraz popisuje číslo popisné následované lomítkem (přičemž třetí subvýraz popisuje číslo popisné samotné). Celý druhý subvýraz je následován kvantifikátorem ? a tak může být odpovídající řetězec přítomný maximálně jednou (to znamená, že nemusí být přítomný vůbec). Čtvrtý subvýraz popisuje číslo orientační, které může být vyjádřeno číslem, případně číslem následovaným písmeny a-c (respektive A-C).

Pokud funkci „předhodíme“ jako $string například Nám. 5. května 568/28, výpis obsahu pole $regs pomocí funkce var_dump() bude vypadat následovně:

array(5) {
  [0]=>
  string(21) „Nám. 5. května 568/28“
  [1]=>
  string(14) „Nám. 5. května“
  [2]=>
  string(4) „568/“
  [3]=>
  string(3) „568“
  [4]=>
  string(2) „28“
}

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *