Nacházíte se zde: Domů Ponořme se do Pythonu 3

Úroveň obtížnosti: ♦♦♦♢♢

Regulární výrazy

Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems.
(Když se někteří lidé setkají s problémem, pomyslí si: „Já vím! Použiji regulární výrazy.“ V tom okamžiku mají problémy dva.)
Jamie Zawinski

 

Ponořme se

Získávání malých kousků textu z velkých bloků textu představuje výzvu. Pythonovské řetězcové objekty poskytují metody pro vyhledávání a náhrady: index(), find(), split(), count(), replace() atd. Ale použití těchto metod je omezeno na nejjednodušší případy. Tak například metoda index() hledá jediný, pevně zadaný řetězec a vyhledávání je vždy citlivé na velikost písmen. Pokud chceme řetězec s vyhledat bez ohledu na velikost písmen, musíme zavolat s.lower() (převod na malá písmena) nebo s.upper() (převod na velká písmena) a zajistit odpovídající převod prohledávaných řetězců. Metody replace() and split() mají stejná omezení.

Pokud svého cíle můžete dosáhnout metodami řetězcového objektu, měli byste je použít. Jsou rychlé, jednoduché a snadno čitelné. O rychlém, jednoduchém a čitelném kódu bychom se mohli bavit ještě dlouho. Ale pokud se přistihnete, že používáte velké množství různých řetězcových funkcí a příkazů if, abyste zvládli speciální případy, nebo pokud musíte kombinovat volání split() a join(), abyste řetězce rozsekávali na kousky a zase je slepovali, v takových případech může být vhodné přejít k regulárním výrazům.

Regulární výrazy představují mocný a (většinou) standardizovaný způsob vyhledávání, náhrad a rozkladu textu se složitými vzorci znaků. Syntaxe regulárních výrazů je sice obtížná a nepodobná normálnímu kódu, ale výsledek může být nakonec čitelnější než řešení používající mnoho řetězcových funkcí. Existují dokonce způsoby, jak lze do regulárních výrazů vkládat komentáře. To znamená, že jejich součástí může být podrobná dokumentace.

Pokud už jste regulární výrazy používali v jiných jazycích (jako jsou Perl, JavaScript nebo PHP), bude vám pythonovská syntaxe připadat důvěrně známá. Abyste získali přehled o dostupných funkcích a jejich argumentech, přečtěte si shrnutí v dokumentaci modulu re.

Případová studie: Adresa ulice

Následující série příkladů byla inspirována problémem, který jsem před několika lety řešil v práci. Potřeboval jsem vyčistit a standardizovat adresy ulic, které byly vyexportované z původního systému, ještě před jejich importem do nového systému. (Vidíte? Já si ty věci jen tak nevymýšlím. Ony jsou ve skutečnosti užitečné.) Tento příklad ukazuje, jak jsem na to šel.

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')                
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')                
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')  
'100 NORTH BROAD RD.'
>>> import re                               
>>> re.sub('ROAD$', 'RD.', s)               
'100 NORTH BROAD RD.'
  1. Mým cílem bylo standardizovat adresu ulice tak, aby se 'ROAD' vždycky zkrátilo na 'RD.'. Na první pohled jsem si myslel, že je to dost jednoduché, takže prostě použiji řetězcovou metodu replace(). Koneckonců, všechna data už byla převedena na velká písmena, takže problém citlivosti na velikost písmen odpadl. A vyhledávaný řetězec 'ROAD' je konstantní. A v tomto klamně jednoduchém případě s.replace() samozřejmě funguje.
  2. Život je ale, naneštěstí, plný protipříkladů a na jeden takový jsem hned narazil. Problém následující adresy spočívá v dvojím výskytu 'ROAD'. Jednou jde o část jména ulice 'BROAD' a jednou o samostatné slovo. Metoda replace() tyto dva výskyty najde a slepě je oba nahradí. A já jen pozoruji, jak se mé adresy kazí.
  3. Abychom problém adres s více než jedním výskytem podřetězce 'ROAD' vyřešili, můžeme se uchýlit k něčemu takovému: hledání a náhradu 'ROAD' budeme provádět jen v posledních čtyřech znacích adresy (s[-4:]) a zbytek řetězce ponecháme beze změny (s[:-4]). Ale už sami vidíte, že to začíná být těžkopádné. Například už jen to, že řešení závisí na délce řetězce, který nahrazujeme. (Pokud bychom chtěli nahradit 'STREET' zkratkou 'ST.', museli bychom napsat s[:-6] a s[-6:].replace(...).) Líbilo by se vám, kdybyste se k tomu museli za šest měsíců vrátit a hledat chybu? Jsem si jistý, že ne.
  4. Nastal čas, abychom přešli k regulárním výrazům. Veškerá funkčnost spojená s regulárními výrazy se v Pythonu nachází v modulu re.
  5. Podívejme se na první parametr: 'ROAD$'. Jde o jednoduchý regulární výraz, ke kterému 'ROAD' pasuje jen v případě, když se vyskytne na konci řetězce. Znak $ vyjadřuje „konec řetězce“. (Existuje také odpovídající znak, stříška ^, která znamená „začátek řetězce“.) Voláním funkce re.sub() hledáme v řetězci s regulární výraz 'ROAD$' a nahradíme jej řetězcem 'RD.'. Nalezne se tím ROAD na konci řetězce s, ale nenalezne se podřetězec ROAD, který je součástí slova BROAD. To se totiž nachází uprostřed řetězce s.

Pokračujme v mém příběhu o čištění adres. Brzy jsem zjistil, že předchozí řešení, kdy 'ROAD' lícuje s koncem adresy, není dost dobré. Ne všechny adresy totiž obsahují údaj, že se jedná o ulici. Některé adresy jednoduše končí jménem ulice. Většinou to vyšlo, ale pokud by se ulice jmenovala 'BROAD', pak by regulární výraz pasoval na 'ROAD', které se nachází na konci řetězce, ale je součástí slova 'BROAD'. A to není to, co bych potřeboval.

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)   
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)   
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)   
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s)  
'100 BROAD RD. APT 3'
  1. To, co jsem opravdu chtěl, bylo vyhledání podřetězce 'ROAD', který se nacházel na konci řetězce a navíc tvořil samostatné slovo (a ne část nějakého delšího slova). V regulárním výrazu to vyjádříme zápisem \b, který má význam „hranice slova se musí vyskytnout právě tady“ (b jako boundary). V Pythonu je to komplikované skutečností, že znak '\' musíme v řetězci vyjádřit zvláštním způsobem. (Tento znak se anglicky nazývá též „escape character“ a používá se pro zápis zvláštních posloupností. Má tedy zvláštní význam. Pokud jej chceme použít v prostém významu, musíme jej také zapsat jako „escape“ sekvenci. Prakticky to znamená, že jej musíme zdvojit.) Někdy se to označuje jako mor zpětných lomítek. Je to jeden z důvodů, proč se psaní regulárních výrazů v Perlu jeví snadnější než v jazyce Python. Negativní stránkou Perlu je míchání vlastních regulárních výrazů a odlišností při jejich zápisu. Takže pokud se někde projevuje chyba, dá se někdy obtížně odhadnout, zda je to chyba syntaxe nebo chyba ve vašem regulárním výrazu.
  2. Mor zpětných lomítek můžeme obejít tím, že uvedením písmene r před uvozovacím znakem použijeme to, čemu se říká surový řetězec (ve smyslu přírodní, nezpracovaný; anglicky raw string). Tím Pythonu říkáme, že se v tomto řetězci nepoužívají speciální posloupnosti (escape sequence). Zápis '\t' vyjadřuje tabulační znak, ale r'\t' se opravdu chápe jako znak \ následovaný písmenem t. Pokud budete pracovat s regulárními výrazy, doporučuji vám vždy používat surové řetězce. V opačném případě dospějete velmi rychle k velkým zmatkům. (Regulární výrazy jsou už i tak dost matoucí.)
  3. Ach jo. Naneštěstí jsem brzy našel případy, které odporovaly mému přístupu. V tomto případě obsahovala adresa slovo 'ROAD' jako samostatné slovo, ale to se nenacházelo na konci. Za označením ulice se totiž nacházelo číslo bytu. A protože se 'ROAD' nenacházelo na úplném konci řetězce, nepasovalo to s regulárním výrazem, takže celé volání re.sub() neprovedlo vůbec žádnou náhradu a vrátil se původní řetězec, což nebylo to, co jsem chtěl.
  4. Abych tento problém vyřešil, odstranil jsem znak $ a přidal jsem další \b. Teď už regulární výraz můžeme číst „vyhledej samostatné slovo 'ROAD' kdekoliv v řetězci“, ať už je to na konci, na začátku nebo někde uprostřed.

Případová studie: Římská čísla

Římská čísla už jste určitě viděli, i když jste je možná nerozpoznali. Mohli jste je vidět u starých filmů nebo televizních pořadů jako „Copyright MCMXLVI“ místo „Copyright 1946“, nebo na stěnách knihoven a univerzit („založeno MDCCCLXXXVIII“ místo „založeno 1888“ ). Mohli jste je vidět v různých číslováních a odkazech na literaturu. Jde o systém zápisu čísel, který se opravdu datuje do dob starého římského impéria (proto ten název).

U římských čísel se používá sedm znaků, které se opakují a kombinují různými způsoby, aby vyjádřily číselnou hodnotu.

Následují základní pravidla pro konstrukci římských čísel:

Kontrola tisícovek

Jak bychom vlastně mohli ověřit, zda je libovolný řetězec platným římským číslem? Podívejme se na to po jednotlivých číslicích. Římské číslice se vždycky píší od největších k nejmenším. Začněme tedy u nejvyšších, na místě tisícovek. U čísel 1000 a vyšších se tisícovky vyjadřují jako řada znaků M.

>>> import re
>>> pattern = '^M?M?M?$'        
>>> re.search(pattern, 'M')     
<_sre.SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')    
<_sre.SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')   
<_sre.SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM')  
>>> re.search(pattern, '')      
<_sre.SRE_Match object at 0106F4A8>
  1. Tento vzorek má tři části. Znak ^ zajistí vazbu další části výrazu na začátek řetězce. Pokud bychom jej nepoužili, pak by vzorek pasoval nezávisle na tom, kde by se znaky M nacházely. A to bychom nechtěli. Chceme si být jistí ním, že pokud se nějaké znaky M najdou, musí se nacházet na začátku řetězce. Zápis M? odpovídá nepovinnému výskytu jednoho znaku M. A protože se opakuje třikrát, odpovídá výraz výskytu žádného až tří znaků M za sebou. Znak $ odpovídá konci řetězce. Když to dáme dohromady se znakem ^ na začátku, znamená to, že vzorek musí odpovídat celému řetězci. Znakům M nemůže žádný jiný znak předcházet a ani za nimi nemůže následovat.
  2. Základem modulu re je funkce search(). Ta přebírá regulární výraz (pattern) a řetězec ('M') a zkusí, jestli k sobě pasují. Pokud je shoda nalezena, vrátí funkce search() objekt, který nabízí různé metody k popisu výsledku. Pokud ke shodě nedojde, vrací funkce search() hodnotu None, což je pythonovská hodnota null (nil, nic). V tomto okamžiku nás zajímá jen to, zda vzorek pasuje. Abychom mohli odpovědět, stačí se podívat na návratovou hodnotu funkce search(). Řetězec 'M' odpovídá regulárnímu výrazu, protože první nepovinný znak M sedí a druhý a třetí nepovinný znak M se ignoruje.
  3. Řetězec 'MM' vyhovuje, protože první a druhý nepovinný znak M pasují a třetí M se ignoruje.
  4. Řetězec 'MMM' vyhovuje, protože všechny tři znaky M pasují.
  5. Řetězec 'MMMM' nevyhovuje. Všechny tři znaky M pasují, ale pak regulární výraz trvá na tom, že řetězec musí skončit (protože je to předepsáno znakem $). Jenže řetězec ještě nekončí (protože následuje čtvrté M). Takže search() vrací None.
  6. Zajímavé je, že prázdný řetězec tomuto regulárnímu výrazu vyhovuje, protože všechny znaky M jsou nepovinné.

Kontrola stovek

Kontrola stovek je obtížnější než kontrola tisícovek. Je to tím, že v závislosti na hodnotě existuje několik vzájemně se vylučujících způsobů, kterými mohou být stovky vyjádřeny.

Takže tu máme čtyři možné vzory:

Poslední dva vzory můžeme zkombinovat:

Následující příklad ukazuje, jak můžeme u římských čísel ověřit zápis stovek.

>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'  
>>> re.search(pattern, 'MCM')             
<_sre.SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')              
<_sre.SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')          
<_sre.SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')            
>>> re.search(pattern, '')                
<_sre.SRE_Match object at 01071D98>
  1. Tento vzorek začíná stejně jako u předchozího příkladu. Kontrolujeme hranici začátku řetězce (^) a potom místo pro tisícovky (M?M?M?). V závorkách je poté uvedena nová část, která definuje sadu tří vzájemně výlučných vzorků oddělených svislými čarami: CM, CD a D?C?C?C? (což vyjadřuje nepovinné D následované žádným nebo třemi znaky C). Analyzátor (parser) regulárního výrazu kontroluje každý z těchto vzorků v daném pořadí (zleva doprava), zvolí první, který situaci odpovídá, a ostatní ignoruje.
  2. Řetězec 'MCM' vyhovuje, protože pasuje první M, druhý a třetí znak M vzorku se ignorují. Následující podřetězec CM odpovídá prvnímu vzorku v závorce (takže části vzorku CD a D?C?C?C? se neuvažují). MCM je římské číslo vyjadřující hodnotu 1900.
  3. Řetězec 'MD' vyhovuje, protože pasuje první M, druhé a třetí M se ignorují. Vzorek D?C?C?C? pasuje k D (každý z následujících tří znaků C je nepovinný, takže se ignorují). MD je římské číslo vyjadřující 1500.
  4. Řetězec 'MMMCCC' testem prošel. Všechny tři znaky M pasují. Následující vzorek D?C?C?C? pasuje k podřetězci CCC (znak D je nepovinný a ignoruje se). MMMCCC je římské číslo vyjadřující hodnotu 3300.
  5. Řetězec 'MCMC' nevyhovuje. První znak M pasuje, druhé a třetí M se ignorují. Následující CM vyhovuje, ale poté vzorek předepisuje znak $, který nesedí, protože ještě nejsme na konci řetězce. (Pořád nám zbývá nezpracovaný znak C.) Poslední znak C nelze napasovat ani na část vzorku D?C?C?C?, protože ta se vzájemně vylučuje s částí vzorku CM, která se již použila.
  6. Zajímavé je, že tomuto vzorku vyhovuje prázdný řetězec, protože všechny znaky M jsou nepovinné a ignorují se. Prázdný řetězec dále vyhovuje i části vzorku D?C?C?C?, protože všechny znaky jsou nepovinné a ignorují se.

Uffff! Vidíte, jak se mohou regulární výrazy rychle stát nechutnými? A to jsme zatím vyřešili části římských čísel jen pro tisíce a stovky. Ale pokud jste zatím vše sledovali, budou pro vás desítky a jednotky jednoduché, protože u nich použijeme naprosto stejný přístup. Ale podívejme se ještě na další možnost vyjádření vzorku.

Využití syntaxe {n,m}

V předcházející podkapitole jsme pracovali se vzorkem, ve kterém se mohly stejné znaky opakovat až třikrát. V regulárních výrazech existuje ještě jiný způsob, jak to vyjádřit. Někteří lidé jej považují za čitelnější. Podívejme se nejdříve na způsoby, které jsme použili v předcházejícím příkladu.

>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')     
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MM')    
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMM')   
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM')  
>>> 
  1. Zde dochází ke shodě se začátkem řetězce a s prvním nepovinným M, ale ne s druhým a s třetím M (což je v pořádku, protože jsou nepovinná). Potom následuje konec řetězce.
  2. Zde dochází ke shodě se začátkem řetězce a s prvním a druhým nepovinným M, ale ne s třetím M (ale to je v pořádku, protože je nepovinné). Poté pasuje i konec řetězce.
  3. Zde dochází ke shodě se začátkem řetězce, se všemi třemi nepovinnými M a s koncem řetězce.
  4. Zde dochází ke shodě se začátkem řetězce a se všemi třemi nepovinnými M, ale poté nenásleduje předepsaný konec řetězce (protože tu máme ještě jedno nepasující M). To znamená, že vzorek nesedí a vrací se None.
>>> pattern = '^M{0,3}$'        
>>> re.search(pattern, 'M')     
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')    
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')   
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM')  
>>> 
  1. Tento vzorek říká: „Zde musí být začátek řetězce, potom následují nula až tři znaky M a pak musí být konec řetězce.“ Na místě 0 a 3 mohou být uvedena libovolná čísla. Pokud chceme předepsat „nejméně jeden, ale ne víc než tři znaky M“, můžeme napsat M{1,3}.
  2. Zde dochází ke shodě se začátkem řetězce a pak s jedním ze tří možných M a s koncem řetězce.
  3. Zde dochází ke shodě se začátkem řetězce a pak s dvěma ze tří možných M a s koncem řetězce.
  4. Zde dochází ke shodě se začátkem řetězce a pak s třemi ze tří možných M a s koncem řetězce.
  5. Zde dochází ke shodě se začátkem řetězce a pak s třemi ze tří možných M, ale poté nedochází ke shodě s předpisem pro konec řetězce. Tento regulární výraz předepisuje maximálně tři znaky M následované koncem řetězce, ale řetězec obsahuje čtyři, takže vzorek nepasuje a vrací se None.

Kontrola desítek a jednotek

Rozšiřme tedy regulární výraz pro kontrolu římských čísel o kontrolu na místě desítek a jednotek. Následující příklad ukazuje, jak můžeme kontrolovat desítky.

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')     
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')      
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')     
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')   
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX')  
>>> 
  1. Tento řetězec pasuje k předepsanému začátku řetězce, pak k prvnímu nepovinnému M, následuje shoda s CM, poté s XL a s předpisem pro konec řetězce. Připomeňme si, že syntaxe (A|B|C) vyjadřuje „odpovídá právě jednomu z A, B nebo C“. Došlo ke shodě s XL, takže se ignorují možnosti XC a L?X?X?X?. Poté byl nalezen konec řetězce. MCMXL je římské číslo vyjadřující hodnotu 1940.
  2. Tento řetězec vyhovuje předepsanému začátku řetězce, pak prvnímu nepovinnému M, následuje shoda s CM a pak s L?X?X?X?. Co se týká části L?X?X?X?, vyhovuje jí L a přeskakují se všechny tři nepovinné znaky X. Poté se dostáváme ke konci řetězce. MCML je římské číslo vyjadřující hodnotu 1950.
  3. Tento řetězec pasuje k předepsanému začátku řetězce, pak k prvnímu nepovinnému M, následuje shoda s CM, poté s nepovinným L, s prvním nepovinným X, pak se přeskočí druhé a třetí nepovinné X a následuje očekávaný konec řetězce. MCMLX je římské číslo vyjadřující hodnotu 1960.
  4. Tento řetězec vyhovuje předepsanému začátku řetězce, pak prvnímu nepovinnému M, potom CM, pak následuje nepovinné L a všechna tři nepovinná X a vyžadovaný konec řetězce. MCMLXXX je římské číslo vyjadřující hodnotu 1980.
  5. Tento případ vyhovuje předepsanému začátku řetězce, pak prvnímu nepovinnému M, potom CM, pak tu máme nepovinné L a všechna tři nepovinná X, ale poté dochází k selhání předpokladu konce řetězce, protože nám zbývá ještě jedno X, se kterým jsme nepočítali. Takže celý regulární výraz selhává (nepasuje) a vrací se None. MCMLXXXX není platné římské číslo.

Výraz pro test jednotek vytvoříme stejným způsobem. Ušetřím vás detailů a ukážu vám jen konečný výsledek.

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

So what does that look like using this alternate {n,m} syntax? This example shows the new syntax.

>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')              
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')          
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII')   
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')                 
<_sre.SRE_Match object at 0x008EEB48>
  1. Zde dochází ke shodě se začátkem řetězce, pak s jedním ze tří možných znaků M a následně s předpisem D?C{0,3}. U posledního podvýrazu dochází ke shodě s nepovinným D a s nulou ze tří možných znaků C. Posuňme se dál. Zde pasuje podvýraz L?X{0,3}, protože vyhoví nepovinné L a nula ze tří možných znaků X. Další kousek řetězce vyhovuje podvýrazu V?I{0,3}, protože je nalezeno nepovinné V a nula ze tří možných znaků I. A na závěr nastává očekávaný konec řetězce. MDLV je římské číslo vyjadřující hodnotu 1555.
  2. Zde dochází ke shodě se začátkem řetězce a pak s dvěma ze tří možných znaků M, pak s D?C{0,3} s jedním D a s jedním ze tří možných znaků C. Pokračujeme L?X{0,3} s jedním L a jedním ze tří možných znaků X. A dále tu máme V?I{0,3} s jedním V a jedním ze tří možných znaků I. Pasuje i očekávaný konec řetězce. MMDCLXVI je římské číslo vyjadřující hodnotu 2666.
  3. Zde dochází ke shodě se začátkem řetězce a pak s třemi ze tří možných znaků M, pak je tu D?C{0,3} s jedním D a s třemi ze tří možných znaků C. Pokračujeme L?X{0,3} s jedním L a s třemi ze tří možných znaků X. A dále se uplatní V?I{0,3} s jedním V a s třemi ze tří možných znaků I. A očekávaný konec řetězce. MMMDCCCLXXXVIII je římské číslo vyjadřující hodnotu 3888. Současně je to největší římské číslo, které můžete napsat bez použití rozšířené syntaxe.
  4. A teď se pozorně dívejte. (Připadám si jako kouzelník. „Děti, pozorně se dívejte. Teď ze svého klobouku vytáhnu králíka.“) Tady nám pasuje začátek řetězce, pak následuje nula ze tří možných znaků M, pak pasuje D?C{0,3} — přeskočení nepovinného D a absence znaku C (nula až tři možné výskyty). Pokračujeme shodou s podvýrazem L?X{0,3} přeskočením nepovinného L a přípustnou absencí znaku X (nula až tři možné výskyty). A dále se uplatní V?I{0,3} přeskočením nepovinného V a shodou jednoho ze tří možných znaků I. A pak je tu konec řetězce. No páni.

Pokud jste to všechno stihli sledovat a rozuměli jste tomu napoprvé, jde vám to líp, než to šlo mně. Teď si představte, že se snažíte porozumět regulárnímu výrazu, který napsal někdo jiný a který se nachází uprostřed kritické funkce rozsáhlého programu. Nebo si představte, že se po několika měsících vracíte ke svému vlastnímu regulárnímu výrazu. Už se mi to stalo a není to pěkný pohled.

Podívejme se na alternativní syntaxi, která nám pomůže zapsat regulární výraz tak, aby se dal udržovat.

Víceslovné regulární výrazy

Zatím jsme se zabývali tím, čemu budu říkat „kompaktní“ regulární výrazy. Jak jste sami viděli, obtížně se čtou. Dokonce i když přijdete na to, co nějaký z nich dělá, není tu žádná záruka, že mu budete rozumět o šest měsíců později. To, co opravdu potřebujeme, je dokumentace připisovaná k danému místu.

V Pythonu toho lze dosáhnout u takzvaných víceslovných regulárních výrazů (verbose regular expressions). Víceslovný regulární výraz se od kompaktního regulárního výrazu liší ve dvou směrech:

Z dalšího příkladu to bude jasnější. Revidujme kompaktní regulární výraz, s kterým jsme pracovali před chvílí, a převeďme jej na víceslovný regulární výraz. Příklad nám ukáže, jak na to.

>>> pattern = '''
    ^                   # začátek řetězce
    M{0,3}              # tisíce - 0 až 3 M
    (CM|CD|D?C{0,3})    # stovky - 900 (CM), 400 (CD), 0-300 (0 až 3 C),
                        #        nebo 500-800 (D následované 0 až 3 C)
    (XC|XL|L?X{0,3})    # desítky - 90 (XC), 40 (XL), 0-30 (0 až 3 X),
                        #        nebo 50-80 (L následované 0 až 3 X)
    (IX|IV|V?I{0,3})    # jednotky - 9 (IX), 4 (IV), 0-3 (0 až 3 I),
                        #        nebo 5-8 (V následované 0 až 3 I)
    $                   # konec řetězce
    '''
>>> re.search(pattern, 'M', re.VERBOSE)                 
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)         
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)   
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'M')                             
  1. Nejdůležitější věcí při práci s víceslovnými regulárními výrazy je to, abychom nezapomněli předat jeden argument navíc: v modulu re je definována konstanta re.VERBOSE, kterou dáváme najevo, že vzorek se má brát jako víceslovný regulární výraz. Jak vidíte, v tomto vzorku se nachází docela hodně bílých znaků (všechny se ignorují) a několik komentářů (opět se všechny ignorují). Pokud budete ignorovat bílé znaky a komentáře, dostanete naprosto stejný regulární výraz, jaký jsme si ukázali v minulé podkapitole. Ale je mnohem čitelnější.
  2. Zde dochází ke shodě se začátkem řetězce a pak s třemi M, pak s CM, následuje L a tři ze tří možných X, pak IX a konec řetězce.
  3. Tady pasuje začátek řetězce, pak tři z možných tří M, následuje D a tři ze tří možných C, pak L a tři ze tří možných X, pak V a tři ze tří možných I a konec řetězce.
  4. Shoda nebyla nalezena. Proč? Protože jsme neuvedli příznak re.VERBOSE. Takže funkce re.search považuje vzorek za kompaktní regulární výraz, ve kterém hrají roli všechny bílé znaky i znaky #. Python nemůže rozpoznávat automaticky, zda je regulární výraz víceslovný nebo ne. Python považuje každý regulární výraz za kompaktní — pokud explicitně neřekneme, že je víceslovný.

Případová studie: Analýza telefonních čísel

Prozatím jsme se soustředili na shodu celých vzorků. Vzorek buď pasuje, nebo ne. Ale regulární výrazy jsou ještě mnohem mocnější. Pokud regulární výraz pasuje, můžeme z řetězce vybrat specifické úseky. Můžeme zjistit, jaká část a kde pasovala.

Následující příklad přinesl opět reálný život. Setkal jsem se s ním o jeden pracovní den dříve než s tím předchozím. Problém: rozklad amerického telefonního čísla. Klient požadoval, aby se číslo dalo zadávat ve volném tvaru (v jednom poli formuláře), ale pak je chtěl mít ve firemní databázi rozdělené na kód oblasti, hlavní linku, číslo a případně klapku. Proštrachal jsem web a našel jsem spoustu příkladů regulárních výrazů, které byly pro tento účel vytvořeny. Ale žádný z nich nebyl dost benevolentní.

Tady máme pár telefonních čísel, která měla být přijata:

Docela široký záběr, že? V každém z těchto případů jsem potřeboval zjistit, že číslo oblasti bylo 800, číslo hlavní linky bylo 555 a zbytek telefonního čísla byl 1212. U čísel s klapkou (extension, ext.) jsem potřeboval zjistit, že klapka byla 1234.

Takže si projděme vývoj řešení pro analýzu telefonního čísla. Následující příklad ukazuje první krok.

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')  
>>> phonePattern.search('800-555-1212').groups()             
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')                 
>>> phonePattern.search('800-555-1212-1234').groups()        
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'groups'
  1. Regulární výraz čteme vždy zleva doprava. Tento odpovídá začátku řetězce a pak následuje (\d{3}). Co to je \d{3}? No, \d vyjadřuje „libovolnou číslici (09). Společně s {3} znamená „přesně tři číslice“. Jde o variaci na syntaxi {n,m}, kterou jsme si ukazovali dříve. Když to vše obalíme do závorek, znamená to „napasuj se přesně na tři číslice a potom si je zapamatuj jako skupinu, kterou si můžeme vyžádat později“. Pak musí následovat pomlčka. Pak má následovat skupina zase přesně tří číslic. A pak další pomlčka. A další skupina tentokrát čtyř číslic. A poté se očekává konec řetězce.
  2. Ke skupinám, které se zapamatovaly během analýzy předepsané regulárním výrazem, můžeme přistupovat metodou groups() objektu, který vrátila metoda search(). Vrací tolikačlennou n-tici, kolik skupin bylo v regulárním výrazu definováno. V našem případě jsme definovali tři skupiny: jednu s třemi číslicemi, další s třemi číslicemi a poslední se čtyřmi číslicemi.
  3. Tento regulární výraz ale není hotový, protože nezvládne telefonní čísla s klapkou na konci. Pro tento účel musíme regulární výraz rozšířit.
  4. Tento případ ilustruje, proč bychom ve skutečně používaném kódu neměli nikdy „řetězit“ použití metod search() a groups(). Pokud metoda search() nevrátí žádnou shodu, vrací None a nikoliv objekt vyjadřující shodu s regulárním výrazem (MatchObject). Volání None.groups() vyvolá naprosto zřejmou výjimku. None totiž žádnou metodu groups() nemá. (Je to samozřejmě méně zjevné v situaci, kdy se taková výjimka vynoří někde z hloubky našeho kódu. Ano, tady mluvím z vlastní zkušenosti.)
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')  
>>> phonePattern.search('800-555-1212-1234').groups()              
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')                       
>>> 
>>> phonePattern.search('800-555-1212')                            
>>> 
  1. Tento regulární výraz se s předchozím téměř shoduje. Také nejdříve předepisuje začátek řetězce, pak se pamatuje skupina tří číslic, pomlčka, pak se pamatuje skupina tří číslic, pomlčka a nakonec se pamatuje skupina čtyř číslic. Nové je tady to, že se očekává další pomlčka, pak se pamatuje skupina jedné nebo více číslic a teprve potom má nastat konec řetězce.
  2. Metoda groups() teď vrací n-tici se čtyřmi prvky, protože regulární výraz nyní definuje čtyři pamatované skupiny.
  3. Tento regulární výraz ale, bohužel, také není konečnou odpovědí, protože předpokládá, že jednotlivé části telefonního čísla jsou odděleny pomlčkou. Co kdyby je někdo oddělil mezerami, čárkami nebo tečkami? Potřebujeme obecnější řešení, které by akceptovalo více typů oddělovačů.
  4. Ouha! Tenhle regulární výraz nejen že nedělá vše, co si přejeme. Je to ve skutečnosti krok zpět, protože teď nejsme schopni analyzovat číslo bez klapky. To vůbec není to, co jsme chtěli. Pokud tam klapka je, pak chceme vědět jaká. Pokud tam klapka není, pak chceme znát, jaké byly části hlavního čísla.

Následující příklad ukazuje regulární výraz, který si poradí s různými oddělovači mezi částmi telefonního čísla.

>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')  
>>> phonePattern.search('800 555 1212 1234').groups()  
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()  
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')              
>>> 
>>> phonePattern.search('800-555-1212')                
>>> 
  1. Držte si klobouky, jedeme z kopce! Očekáváme začátek řetězce, potom skupinu tří číslic, pak \D+. A co je zase tohle? Zápis \D vyjadřuje libovolný znak s výjimkou číslice a + znamená „1 nebo víckrát“. Takže \D+ pasuje na jeden nebo více znaků, které nejsou číslicemi. A to je právě to, co použijeme místo přímo zapsané pomlčky a co nám bude pasovat s různými oddělovači.
  2. Protože používáme \D+ místo -, bude nám regulární výraz pasovat i na telefonní čísla, kde jsou jednotlivé části odděleny mezerami.
  3. Ale čísla oddělená pomlčkami budou fungovat také.
  4. Stále to ale ještě, bohužel, není konečná odpověď, protože tam nějaký oddělovač je. Co když někdo zadá telefonní číslo úplně bez mezer nebo jiných oddělovačů?
  5. Jejda! Pořád ještě není vyřešeno to, že se požaduje zadání klapky. Takže teď máme dva problémy, ale můžeme je oba vyřešit stejnou technikou.

Následující příklad ukazuje regulární výraz pro telefonní čísla bez oddělovačů.

>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  
>>> phonePattern.search('80055512121234').groups()      
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()  
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()        
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')           
>>> 
  1. Jediná věc, kterou jsme od minulého kroku udělali, byla záměna + za *. Mezi částmi telefonního čísla nyní místo \D+ předepisujeme \D*. Pamatujete si ještě, že + znamená „jednou nebo víckrát“? Fajn. Takže * znamená „nula nebo více výskytů“. Takže teď bychom měli být schopni zpracovat čísla, která neobsahují vůbec žádný oddělovací znak.
  2. No podívejme, ono to opravdu funguje! Jak to? Napasovali jsme se na začátek řetězce, pak jsme si zapamatovali skupinu tří číslic (800), potom nula nenumerických znaků, pak následuje zapamatovaná skupina tří číslic (555), pak nula nenumerických znaků, pak zapamatovaná skupina čtyř číslic (1212), pak nula nenumerických znaků, pak zapamatovaná skupina libovolného počtu číslic (1234) a konec řetězce.
  3. Ostatní obměny teď fungují také: tečky místo pomlček i kombinace mezer a x před klapkou.
  4. Nakonec se nám podařilo vyřešit i dlouho odolávající problém: klapka už je opět nepovinná. Metoda groups() vrací n-tici se čtyřmi prvky i tehdy, když nebyla nalezena klapka. V takovém případě se ale na místě čtvrtého prvku vrací prázdný řetězec.
  5. Nechci být poslem špatných zpráv, ale pořád ještě nejsme hotovi. Co je tady špatně? Před kódem oblasti máme znak navíc, ale regulární výraz předpokládá, že na začátku řetězce se má jako první nacházet kód oblasti. Žádný problém. Úvodní znaky před kódem oblasti můžeme přeskočit již dříve představenou technikou „nula nebo více nečíselných znaků“.

Další příklad ukazuje, jak bychom si měli počínat.

>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  
>>> phonePattern.search('(800)5551212 ext. 1234').groups()                  
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                            
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')                      
>>> 
  1. Tady je to stejné jako v předchozím příkladu — s tou výjimkou, že před první pamatovanou skupinou znaků (před číslem oblasti) předepisuje \D* nula nebo více nenumerických znaků. Všimněte si, že si tyto nenumerické znaky nepamatujeme (předpis není uzavřen v závorkách). Pokud jsou nějaké nalezeny, jednoduše je přeskočíme a teprve pak si zapamatujeme nalezené číslo oblasti.
  2. Telefonní číslo se nám podaří úspěšně rozložit i v případě, kdy je před číslem oblasti uvedena levá závorka. (Pravá závorka za číslem oblasti se už zpracovává. Bere se jako nenumerický oddělovač a napasuje se na předpis \D* nacházející se za první pamatovanou skupinou.)
  3. Proveďme ještě test funkčnosti (sanity check), abychom se ujistili, že se nepokazilo nic, co dříve fungovalo. Úvodní znaky jsou zcela nepovinné, takže po začátku řetězce se našlo nula nenumerických znaků, pak pamatovaná skupina tří číslic (800), pak jeden nenumerický znak (pomlčka), zapamatovaná skupina tří číslic (555), pak jeden nenumerický znak (pomlčka), poté zapamatovaná skupina čtyř číslic (1212), pak nula nenumerických znaků, pak zapamatovaná skupina nula číslic a na závěr konec řetězce.
  4. Tak toto je případ, kdy mám v souvislosti s regulárními výrazy chuť vydloubnout si oči tupým předmětem. Proč tohle telefonní číslo nepasuje? Protože se před kódem oblasti vyskytuje 1, ale my jsme předpokládali, že všechny znaky před kódem oblasti budou nenumerické (\D*). Grrrrr.

Podívejme se na to znovu. Zatím se všechny regulární výrazy chytaly na začátek řetězce. Ale teď vidíme, že se na začátku řetězce může vyskytnout obsah neurčité délky, který bychom chtěli ignorovat. Mohli bychom se sice pokusit o vytvoření předpisu, kterým bychom ten začátek přeskočili, ale zkusme k tomu přistoupit jinak. Nebudeme se vůbec snažit o to, abychom se napasovali na začátek řetězce. Zmíněný přístup je použit v následujícím příkladu.

>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()         
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                        
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234').groups()                      
('800', '555', '1212', '1234')
  1. Všimněte si, že v regulárním výrazu chybí ^. Už se nesnažíme ukotvit na začátek řetězce. Nikde není řečeno, že by se náš regulární výraz měl napasovat na celý vstupní řetězec. Mechanismus, který regulární výraz vyhodnocuje, už si dá tu práci, aby zjistil, od jakého místa vstupního řetězce dochází ke shodě s předpisem, a bude pokračovat odtud.
  2. Teď už jsme úspěšně rozložili telefonní číslo, které obsahuje úvodní znaky i s nechtěnými čísly a které odděluje skupiny chtěných čísel libovolným počtem libovolných oddělovačů.
  3. Test funkčnosti (sanity check). Funguje to správně.
  4. A tohle taky funguje.

Vidíte, jak se může regulární výraz rychle vymknout kontrole? Letmo mrkněte na libovolný z předchozích pokusů. Poznáte snadno rozdíl mezi ním a po něm následujícím?

Takže dokud ještě rozumíme konečnému řešení (a tohle opravdu je konečné řešení; pokud jste objevili případ, který by to nezvládlo, nechci o něm vědět), zapišme ho jako víceslovný regulární výraz. Mohli bychom brzy zapomenout, proč jsme něco zapsali právě takto.

>>> phonePattern = re.compile(r'''
                # nevázat se na začátek řetězce, číslo může začít kdekoliv
    (\d{3})     # číslo oblasti má 3 číslice (např. '800')
    \D*         # nepovinný oddělovač - libovolný počet nenumerických znaků
    (\d{3})     # číslo hlavní linky má 3 číslice (např. '555')
    \D*         # nepovinný oddělovač
    (\d{4})     # zbytek čísla má 4 číslice (např. '1212')
    \D*         # nepovinný oddělovač
    (\d*)       # nepovinná klapka - libovolný počet číslic
    $           # konec řetězce
    ''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()  
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                          
('800', '555', '1212', '')
  1. Jediným rozdílem proti regulárnímu výrazu z minulého kroku je to, že je vše rozepsáno na více řádcích. Proto není žádným překvapením, že zpracovává vstupy stejným způsobem.
  2. Konečný test funkčnosti (sanity check). Ano, tohle pořád funguje. Jsme hotovi.

Shrnutí

Zatím jsme viděli pouhou špičku ledovce z toho, co regulární výrazy zvládnou. Jinými slovy, ačkoliv jimi můžete být momentálně zcela ohromeni, zatím jste neviděli nic. To mi věřte.

Následující věci už by vám neměly být cizí:

Regulární výrazy jsou velmi mocné, ale jejich použití není správným řešením pro každý problém. Měli byste se o nich naučit tolik, abyste věděli, kdy je jejich použití vhodné, kdy vám pomohou problém vyřešit a kdy naopak způsobí víc problémů, než vyřeší.

© 2001–11 Mark Pilgrim