13.3 Regulární výrazy
Jednoduché úpravy řetězců popsané výše a hledání, jaké je obvyklé v programech typu Word nebo Excel nestačí, když řešíme nějakou složitější situaci. Řekněme, že potřebujeme v textu najít každé telefonní číslo a převést je na nějaký standardní tvar. Každé telefonní číslo se však skládá z jiných číslic a navíc mohou být tyto číslice i různě oddělené: telefonní číslo 123456789
je totéž, co 123 456 789
, +420 123 456 789
a +420-123-456-789
. Podobné problémy nastávají i při zjišťování data: stejné datum může být např. zadané jako 1.6.2006
, 1. 6. 2006
i 1. června 2006
. K hledání a úpravám takto volně definovaných skupin znaků slouží regulární výrazy.
Regulární výraz je řetězec, který obsahuje vzor (pattern) toho, jak vypadá hledaný kus textu. Je to tedy jakási abstraktní maska, která ukazuje, jaké vlastnosti musí kus textu splňovat, aby byl vybrán. Zkoumaný řetězec se pak prochází a hledají se ty jeho části, které splňují daný vzor.
Bohužel se regulární výrazy implementované v různých programech od sebe mírně liší. Z historických důvodů se dnes v R používají tři různé typy regulárních výrazů: základní funkce v R používají regulární výrazy standardu POSIX 1003.2 nebo (na požádání) regulární výrazy podle standardu jazyka Perl. Balík stringr používá regulární výrazy založené na ICU. Naštěstí se od sebe různé standardy liší jen v různých nadstavbách. Zde se podíváme na základ, který je společný všem třem výše zmíněným formám regulárních výrazů. (Plné definice těchto standardů najdete v dokumentaci funkcí: help("regex", package = "base")
pro POSIX a help("stringi-search-regex", package = "stringi")
pro ICU.)
Jak už bylo řečeno, regulární výraz je řetězec. Některé znaky v něm se berou doslovně, jiné mají speciální význam, který může záviset na kontextu, ve kterém jsou použité. Prakticky používají regulární výrazy čtyři operace:
- spojování – znaky zapsané za sebou se spojí do jednoho výrazu; např.
abcd
je spojení čtyř znaků, které fungují jako celek, tj. jako řetězec “abcd” - logické “nebo” označené symbolem
|
vybírá jednu z možností; např.ab|cd
znamená řetězec “ab” nebo řetězec “cd,” protože spojování má vyšší prioritu než “nebo” - opakování – umožňuje říct, kolikrát se má nějaký řetězec v textu vyskytovat; k tomu poskytují regulární výrazy kvantifikátory, viz dále; např.
abc+
znamená řetězceabc
,abcc
,abccc
atd.; opakuje se jen poslední znak, protože operace spojování i “nebo” mají nižší prioritu než kvantifikátory - seskupování – znaky zapsané do obyčejných závorek
( )
tvoří skupinu, tj. jeden celek, který se může např. opakovat, měnit priority vyhodnocování výrazů apod., protože seskupení má nejvyšší prioritu ze všech operací; skupiny mají i speciální význam při vybírání částí regulárních výrazů, viz dále
Všechny znaky kromě [\^$.|?*+(){}
se berou doslovně, tj. např. a
znamená písmeno “a,” 1
znamená číslici “1” atd. Regulární výraz "jak"
tedy najde v textu všechny výskyty slova “jak” všechně výskytů v jiných slovech, např. “jakkoli”; nenajde však “Jak,” protože regulární výraz chápe malá a velká písmena jako různé znaky.
Znaky [\^$.|?*+(){}
mají speciální význam. To znamená, že výraz cože?
nenajde doslovně řetězec “cože?” (ve skutečnosti najde řetězec “což” nebo řetězec “cože”). Stejně tak Ano.
neznamená doslovně “Ano.” nýbrž jakýkoli řetězec, ve kterém za “Ano” následuje jeden znak (např. “Ano!”).
Konkrétně tečka .
označuje libovolný znak včetně mezer, tabelátorů apod. Výraz ma.ka
najde slova jako “matka,” “maska” nebo “marka,” ale ne “maka.”
Pokud chcete zadat přímo speciální znak, je třeba jej zabezpečit pomocí zpětného lomítka. Skutečná tečka je tedy \.
, hranatá závorka je \[
atd.
Bohužel je v R zpětné lomítko samo aktivním znakem. To znamená, že je třeba jej zabezpečit dalším zpětným lomítkem. Místo \.
je tak třeba zadat "\\."
apod. Nepříjemné je to v případě, kdy potřebujete hledat doslovně zpětné lomítko. V regulárním výrazu je musíte zabezpečit jiným zpětným lomítkem, takže dostanete \\
. V R však dále musíte každé zpětné lomítko zabezpečit dalším zpětným lomítkem, takže regulární výraz, který hledá zpětné lomítko musí být v R zadaný jako čtyři zpětná lomítka: \\\\
.
13.3.1 Rozsahy
Někdy chceme najít řetězec, který obsahuje jakýkoli znak z určité množiny. K tomu slouží rozsahy (character classes). Nejjednodušší způsob, jak v regulárním výrazu zadat rozsah, je použít operátor hranatá závorka.
Hranaté závorky určují výčet platných znaků. Tak např. [Aa]
najde všechny výskyty malého “a” a velkého “A.” Takže výrazu [Aa]dam
vyhoví jak “Adam,” tak “adam.”
V případě číslic a ASCII znaků mohou hranaté závorky obsahovat i rozsah zadaný pomocí pomlčky. Pro nalezení číslic 0 až 4 tedy není třeba psát [01234]
, nýbrž stačí [0-4]
. Pokud se má pomlčka chápat v rozsahu doslovně, je třeba ji napsat jako první znak.
Hranaté závorky mohou obsahovat i negaci, ke které slouží znak ^
uvedený hned za otevírající hranatou závorkou. Výraz [^Aa]
splňují všechny znaky mimo malého “a” a velkého “A.”
Pro nektere casto uzivane vycty existuji specialni symboly. Tabulka 13.3 uvadi nejdulezitejsi z nich. Krome techto rozsahu mohou regularni vyrazy v R obsahovat i POSIXove rozsahy, ktere maji zvlastni tvar [-jmeno-]
a mohou se vyskytovat jen ve vnejsim rozsahu, tj. jako [[-jmeno-]]
nebo napr. [a[-jmeno-]b]
apod. Jejich seznam uvadi tabulka 13.4. POSIXové rozsahy se od základních rozsahů liší v tom, že respektují locale počítače, takže mezi písmena počítají i znaky národní abecedy.
Rozdíl mezi \w
a [:alnum:]
tedy spočívá v tom, že [:alnum:]
obsahuje v českém locale i písmena s háčky a čárkami, zatímco \w
obsahuje jen písmena anglické abecedy (zahrnutá v ASCII). Pokud tedy uvažujete písmena, doporučuji používat vždy POSIXové rozsahy.
symbol | význam | výčtem |
---|---|---|
\d |
číslice | [0-9] |
\D |
není číslice | [^0-9] |
\w |
písmeno, číslice nebo podtržítko | [a-zA-z0-9_] |
\W |
cokoli, co není \w |
[^a-zA-z0-9_] |
\s |
prázdný znak | [ \t\n\r\f] |
\S |
není prázdný znak | [^ \t\n\r\f] |
\b |
hranice slova | |
\B |
není hranice slova | |
\h |
horizontální mezera | |
\H |
není horizontální mezera | |
\v |
vertikální mezera | |
\V |
není vertikální mezera |
rozsah | význam |
---|---|
[:lower:] |
malá písmena v locale počítače |
[:upper:] |
velká písmena v locale počítače |
[:alpha:] |
malá i velká písmena v locale počítače |
[:digit:] |
číslice, tj. 0, 1, …, 9 |
[:alnum:] |
alfanumerické znaky, tj. [:alpha:] a [:digit:] |
[:blank:] |
mezera a tabelátor |
[:cntrl:] |
řídící znaky |
[:punct:] |
! , " , # , % , & , ' , ( , ) , * , + , , , - , . , / , : , ; , < , > , ? , @ , [ , \ , ] , ^ , _ , `, {, |
13.3.2 Kvantifikátory
Znaky *+?{
jsou kvantifikatory, ktere ridi pocet opakovani predchoziho znaku, rozsahu nebo skupiny. Jejich vyznam shrnuje tabulka 13.5.
znak | význam |
---|---|
* |
libovolný počet opakování (tj. vůbec, jednou nebo víckrát) |
+ |
aspoň jeden výskyt (tj. jednou nebo víckrát) |
? |
maximálně jeden výskyt (tj. vůbec nebo jednou) |
{5} |
právě pětkrát |
{3,5} |
nejméně třikrát a nejvýše pětkrát |
{3,} |
nejméně třikrát |
{,5} |
maximálně pětkrát |
Podívejme se na příklady:
.*
odpovídá jakémukoli znaku, který je opakovaný libovolně krát, tj. jakýkoli řetězec včetně""
\w+
odpovídá aspoň jednomu písmenu anglické abecedy[+-]?\d+
odpovídá celému číslu (najde např. “+12,” “1,” “-3” apod.; z “3.14” najde “3” a “14”)
Kvantifikátory jsou “hladové.” To znamená, že najdou řetězec s maximální délkou, která vyhovuje vzoru. To se nemusí vždy hodit. Např. pokud chceme najít všechny přímé řeči, regulární výraz ".*"
nebude fungovat, protože najde celý výraz mezi první a poslední uvozovkou:
<- '"Už je to dobré," řekl Josef. "Pojdme si zaplavat."'
s1 str_extract_all(s1, '".*"') # jeden řetězec od první po poslední uvozovku
## [[1]]
## [1] "\"Už je to dobré,\" řekl Josef. \"Pojdme si zaplavat.\""
Problém vyřeší “líný” kvantifikátor, který najde řetězec s minimální délkou, který splňuje zadaný vzor. Z kvantifikátoru uděláte líný tak, že za něj připojíte otazník. Např. kvantifikátor pro nula až nekonečný počet výskytů *
je hladový; jeho líná verze je *?
:
str_extract_all(s1, '".*?"') # vektor dvou řetězců, v každém je jedna přímá řeč
## [[1]]
## [1] "\"Už je to dobré,\"" "\"Pojdme si zaplavat.\""
13.3.3 Začátek a konec řetězce
Normálně regulární výrazy najdou jakoukoli část řetězce. Někdy je však užitečné hledat jen za začátku nebo na konci řetězce. K tomu slouží “kotvy” (anchors).
Znak ^
uvedený na začátku regulárního výrazu znamená začátek řetězce (nebo ve speciálním případě řádku, viz dále).
Znak $
uvedený na konci regulárního výrazu znamená konec řetězce (nebo ve speciálním případě řádku, viz dále).
Pokud tedy chceme najít jen řádky, které začínají písmenem “A,” můžeme použít regulární výraz ^A
. Pokud chceme najít řádky, které končí tečkou, můžeme použít regulární výraz \.$
(v R bude potřeba zpětné lomítko zdvojit a zadat "\\.$"
). Výraz ^-+$
splní pouze řádky, které obsahují pouze pomlčky.
13.3.4 Skupiny
Někdy je potřeba některé znaky seskupit. K tomu slouží kulaté závorky.
Skupiny ovlivňují priority vyhodnocování regulárního výrazu. To je užitečné např. při alternaci. Např. regulárnímu výrazu abc|def
vyhoví řetězce “abc” a “def.” Naproti tomu výrazu ab(c|d)ef
vyhoví řetězce “abcef” a “abdef.” (Normálně má spojení prioritu před alternací.)
Skupiny ovlivňují priority i při použití kvantifikátorů. Např. výrazu abc*
vyhoví řetězce “ab,” “abc,” “abcc” atd. Naproti tomu výrazu (abc)*
vyhoví prázdný řetězec ""
, “abc,” “abcabc” atd. (Normálně má kvantifikátor přednost před spojením.)
Skupiny navíc dávají regulárním výrazům “paměť.” První skupině odpovídá \1
, druhé skupině \2
atd. (Pokud jsou do sebe skupiny vložené, pak se jejich čísla počítají podle pořadí levé závorky.) Tuto paměť je možné využít jak ve vyhledávacím řetězci, tak při náhradě regulárního výrazu, viz dále. Např. k nalezení pětiznakového palindromu (tj. slova, které je stejné, ať ho čteme zleva nebo zprava, např. kajak nebo madam) můžeme použít regulární výraz (.)(.).\2\1
. V něm musí být znak \1
stejný jako první znak palindromu (odpovídající první skupině s tečkou) a \2
musí být znak stejný jako druhý znak palinromu (odpovídající druhé skupině s tečkou). (Náš regulární výraz najde ovšem i palindromy složené z čísel apod. Pro hledání skutečných palindromů by bylo bezpečnější nahradit v regulárním výrazu tečky např. rozsahem [[:alpha:]]
.)
13.3.5 Příklady
Podívejme se nyní na regulární výrazy pro dva případy zmíněné výše: pro nalezení telefonního čísla a datumu.
Regulární výraz, který pozná všechny výše uvedené formáty telefonního čísla, vypadá takto: (\+420)?[\s-]*\d{3}[\s-]*\d{3}[\s-]*\d{3}
. V R však musejí být všechna zpětná lomítka zdvojena, takže regulární výraz musí být zapsán takto:
<- "(\\+420)?[-\\s]*\\d{3}[-\\s]*\\d{3}[-\\s]*\\d{3}"
r <- c("Leoš: 777 666 555 444",
cisla "Lída: domů +420 734 123 456, práce 777-666-555",
"Leona: nevím")
str_extract_all(cisla, r)
## [[1]]
## [1] " 777 666 555"
##
## [[2]]
## [1] "+420 734 123 456" " 777-666-555"
##
## [[3]]
## character(0)
Regulární výraz, který pozná všechny výše uvedené formáty data:
<- str_c("\\d{1,2}\\.\\s*(\\d{1,2}\\.|leden|únor|březen|duben|červen|červenec|srpen|",
r "září|říjen|listopad|prosinec)\\s*\\d{4}")
<- c("1. 6. 2001", "1.1.2012", "1. červen 2016", "ahoj")
dat str_detect(dat, r)
## [1] TRUE TRUE TRUE FALSE
13.3.6 Praktická práce s regulárními výrazy
Regulární výrazy jsou velmi mocné, ale poněkud nepřehledné. Vždy byste si proto měli vyzkoušet, zda váš regulární výraz 1) nachází to, co chcete, a 2) nenachází to, co nechcete. Je proto velmi užitečné, abyste si připravili několik příkladů řetězců, které by výraz měl a které by neměl najít.
K otestování regulárního výrazu pak můžete použít buď funkce popsané v dalším oddílu, nebo uživatelsky přívětivé funkce str_view(string, pattern, match = NA)
a str_view_all(string, pattern, match = NA)
, kde string
je prohledávaný vektor řetězců, pattern
je zkoušený regulární výraz a match
je logická hodnota. Pokud je TRUE
, zobrazí jen řetězce, které splňují daný regulární výraz, pokud FALSE
, pak zobrazí jen řetězce, které daný regulární výraz nesplňují. Pokud necháte implicitiní hodnotu NA
, pak zobrazí všechny zadané řetězce. Funkce zobrazí v RStudiu v záložce Viewer
zadané řetězce a barevně vyznačí části, které odpovídají zadanému regulárnímu výrazu. První funkce zobrazí pouze první výskyt, zatímco druhá funkce všechny výskyty.
Pohodlné testování regulárních výrazů umožňuje i doplněk RStudia RegExplain. Protože není zatím součástí CRANu, nainstalujete jej takto:
::install_github("gadenbuie/regexplain") devtools
I když váš regulární výraz funguje, měli byste být opatrní a pamatovat na to, že funguje jen za předem určených předpokladů: náš výraz pro datum např. najde jen datum zadané tak, jak je zvykem v České republice, a to ještě jen v případě, že je zadán i rok. Pokud byste chtěli obecnější řešení, začal by váš regulární výraz být složitější a složitější. Pak se často vyplatí výraz zjednodušit a hledaný řetězec hledat na dvakrát nebo na třikrát a nebo využít i jiné nástroje než jen regulární výrazy. Některé texty byste neměli pomocí regulárních výrazů procházet téměř nikdy. Sem patří např. webové stránky, které je možné procházet mnohem elegantněji s použitím syntaxe XPath nebo CSS.
Pamatujte take, ze regularni vyrazy obsahuji mnohem vice nez to, co jste videli v tomto oddilu. Plny vycet detailu je na techto (podle meho mineni velmi neprehlednych) strankach http://www.regular-expressions.info/reference.html. Ve většině případů vám však bude obsah tohoto oddílu bohatě stačit.