13.4 Funkce pro práci s regulárními výrazy
Nyní se podíváme na to, jak využít regulární výrazy pro práci s řetězci v R.
Většina funkcí pro práci s regulárními výrazy definovaná v balíku stringr má podobnou syntaxi: str_XXX(string, pattern, ...)
, kde XXX
je část jména funkce, string
je vektor zpracovávaných řetězců, pattern
je použitý regulární výraz a ...
případné další parametry. Pokud některá funkce vrací jen první výskyt hledaného vzoru v řetězci, pak většinou existuje i podobná funkce s koncovkou _all
, která vrací všechny výskytu zadaného vzoru.
13.4.1 Detekce vzoru
Nejjednodušším případem použití regulárních výrazů je nalezení řetězců, které odpovídají danému regulárnímu výrazu.
K detekci, zda řetězec odpovídá zvolenému regulárnímu výrazu, slouží funkce str_detect(string, pattern, negate = FALSE)
, kde string
je prohledávaný vektor řetězců a pattern
je hledaný vzor. Funkce vrací logický vektor stejné délky jako string
s hodnotou TRUE
, pokud byl vzor nalezen, a hodnotou FALSE
v opačném případě. Nastavení parametru negate
na TRUE
výsledek obrací: nyní funkce vrací TRUE
, pokud řetězec daný vzor neobsahuje. Řekněme, že chceme zjistit, které řetězce ve vektoru s1
obsahují slova o pěti písmenech, kde první dvě jsou “ma” a poslední dvě “ka” (další příklady najdete výše):
<- c("matka", "mačka", "mačká", "maska", "Matka", "marka!", "ma ka")
s1 str_detect(s1, "ma.ka") # TRUE, pokud obsahuje vzor
## [1] TRUE TRUE FALSE TRUE FALSE TRUE TRUE
str_detect(s1, "ma.ka", negate = TRUE) # TRUE, pokud neobsahuje vzor
## [1] FALSE FALSE TRUE FALSE TRUE FALSE FALSE
Funkce str_which(string, pattern, negate = FALSE)
je podobná a má i stejné parametry. Místo logických hodnot však vrací celočíselný vektor pozic prvků vektoru string
, které splňují zadaný regulární výraz:
str_which(s1, "ma.ka")
## [1] 1 2 4 6 7
Často potřebujeme řetězce, které odpovídají regulárnímu výrazu vybrat. To by bylo možné provést pomocí subsetování:
str_detect(s1, "ma.ka")] s1[
## [1] "matka" "mačka" "maska" "marka!" "ma ka"
Balík stringr však k tomuto účelu nabízí komfortnější funkci str_subset(string, pattern, negate = FALSE)
, která vrací ty prvky vektoru s
, které obsahují vzor p
(parametry funkce mají stejný význam jako u str_detect()
):
str_subset(s1, "ma.ka")
## [1] "matka" "mačka" "maska" "marka!" "ma ka"
Podívejme se na praktičtější příklad. Řekněme, že chceme vybrat ty řetězce z vektoru cisla
, které obsahují telefonní čísla:
<- "(\\+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_subset(cisla, r)
## [1] "Leoš: 777 666 555 444"
## [2] "Lída: domů +420 734 123 456, práce 777-666-555"
Mohl by nás zajímat i počet výskytů regulárního výrazu v řetězci. K tomu slouží funkce str_count(string, pattern)
, která spočítá počet výskytů regulárního výrazu pattern
v řetězci string
. Pracuje vektorově přes zadaný vektor řetězců string
i přes regulární výraz pattern
. Implicitní hodnota pattern
je prázdný řetězec ""
. V tomto případě vrací funkce str_count()
počet znaků v řetězci (počítaných podle aktuálního locale). Podívejme se na několik příkladů:
<- c("jablko", "ananas", "hruška", "rybíz")
ovoce str_count(ovoce, "a") # počet výskytů "a" v každém slově
## [1] 1 3 1 0
# počet "a" v jablku, "a" v ananasu, "b" v hrušce a "r" v rybízu
str_count(ovoce, c("a", "a", "b", "r"))
## [1] 1 3 0 1
str_count(c("Ahoj", "lidičky!")) # počet znaků v každém řetězci
## [1] 4 8
13.4.2 Získání částí řetězců, které splnují vzor
Většinou nám nestačí najít výskyt řetězce, který odpovídá regulárnímu výrazu, ale chceme tento řetězec získat. K tomu slouží funkce str_extract(string, pattern)
, která získá z každého řetězce ve vektoru string
tu jeho část, která odpovídá prvnímu výskytu vzoru pattern
. Funkce vrací vektor stejné délky jako string
; pokud není vzor nalezen, vrací NA
.
Řekněme, že chceme např. vybrat z tweetů jednotlivé hashtagy (zjednodušíme si život a budeme předpokládat, že hashtag začíná křížkem a tvoří ho písmena a čísla a končí s prvním výskytem ne-alfanumerického znaku jako je mezera, tečka, čárka apod.):
<- "#[[:alpha:]]+" # hashtag následovaný jedním nebo více písmeny
r <- c("#Brno je prostě nejlepší a #MU je nejlepší v Brně.",
tweet "Někdy je možné najít zajímavé podcasty na #LSE.",
"Stupnování 'divnosti': divný, divnější, #ParisHilton.",
"Docela prázdný tweet.")
str_extract(tweet, r)
## [1] "#Brno" "#LSE" "#ParisHilton" NA
Funkce str_extract()
vrací pouze první výskyt výrazu v každém řetězci. Pokud chceme získat všechny výskyty, musíme použít funkci str_extract_all(string, pattern, simplify = FALSE)
. Implicitně funkce vrací seznam, jehož prvky odpovídají prvkům vektoru string
; nenalezené výskyty pak indikuje prázdný vektor řetězců (character(0)
). Funkce však umí zjednodušit výsledek na matici. K tomu slouží parametr simplify
nastavený na hodnotu TRUE
. V tomto případě odpovídají řádky výsledné matice prvkům vektoru string
; nenalezené výskyty jsou pak označené jako prázdné řetězce ""
.
str_extract_all(tweet, r)
## [[1]]
## [1] "#Brno" "#MU"
##
## [[2]]
## [1] "#LSE"
##
## [[3]]
## [1] "#ParisHilton"
##
## [[4]]
## character(0)
str_extract_all(tweet, r, simplify = TRUE)
## [,1] [,2]
## [1,] "#Brno" "#MU"
## [2,] "#LSE" ""
## [3,] "#ParisHilton" ""
## [4,] "" ""
Někdy nechceme získat celý vzor, nýbrž pouze jeho části. K tomu slouží funkce str_match(string, pattern)
a str_match_all(string, pattern)
, kde string
je vektor prohledávaných řetězců a pattern
regulární výraz. K rozdělení regulárního výrazu do částí se používají skupiny.
Funkce str_match()
hledá první výskyt regulárního výrazu pattern
v řetězci a vrací matici, jejíž řádky odpovídají prvkům vektoru string
. První sloupec je celý regulární výraz, druhý sloupec první skupina v regulárním výrazu, třetí sloupec druhá skupina atd. (Pokud jsou skupiny zanořené jedna do druhé, pak se jejich pořadí počítá podle pořadí levé závorky.) Nenalezené prvky mají hodnotu NA
.
Pokud např. chceme získat jméno hashtagu bez křížku, můžeme vzít druhý sloupec matice, kterou v našem případě vrátí funkce str_match()
:
<- "#([[:alpha:]]+)" # totéž, co výše, ale všechna písmena tvoří skupinu
r str_match(tweet, r)
## [,1] [,2]
## [1,] "#Brno" "Brno"
## [2,] "#LSE" "LSE"
## [3,] "#ParisHilton" "ParisHilton"
## [4,] NA NA
Poznámka: Vždy stojí za zvážení, zda provést nějakou operaci naráz pomocí složitého regulárního výrazu, nebo ji rozdělit do několika kroků. Extrakci hashtagů z minulého příkladu můžeme snadno provést ve dvou jednodušších krocích: nejdříve extrahovat celý hashtag, a pak z něj odstranit křížek:
str_extract(tweet, r) %>% str_remove("#")
## [1] "Brno" "LSE" "ParisHilton" NA
Funkce str_match_all()
vrací všechny výskyty regulárního výrazu. Jejím výsledkem je seznam matic. Řádky těchto matic odpovídají jednotlivým nálezům. Sloupce mají význam jako výše. Pokud není regulární výraz v daném řádku nalezen, je výsledkem prázdná matice.
str_match_all(tweet, r)
## [[1]]
## [,1] [,2]
## [1,] "#Brno" "Brno"
## [2,] "#MU" "MU"
##
## [[2]]
## [,1] [,2]
## [1,] "#LSE" "LSE"
##
## [[3]]
## [,1] [,2]
## [1,] "#ParisHilton" "ParisHilton"
##
## [[4]]
## [,1] [,2]
Podívejme se opět na poněkud komplexnější příklad. Vektor cisla
obsahuje řetězce, které obsahují telefonní čísla. Pro každého člověka chceme získat jeho první telefonní číslo a převést je do standardního tvaru (řekněme, že standardní tvar vynechává předčíslí země a trojice čísel odděluje pomlčkou). Postup může být následující: nejdříve získáme jednotlivé výskyty regulárního výrazu pomocí funkce str_match()
. Z výsledku si vezmeme pouze skupiny, které odpovídají trojicím čísel (v našem případě třetí, čtvrtý a pátý sloupec výsledku). Nakonec spojíme jednotlivá trojčíslí do jednoho řetězce pomocí funkce str_c()
; abychom jí zabránili spojit všechna trojčíslí pro všechny lidi dohromady, aplikujeme funkci str_c()
na jednotlivé řádky matice pomocí funkce map_chr()
z balíku purrr:
<- "(\\+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_match(cisla, r)[, 3:5]
cisla2 ::map_chr(1:nrow(cisla2), ~ str_c(cisla2[., ], collapse = "-")) purrr
## [1] "777-666-555" "734-123-456" NA
13.4.3 Indexy řetězců splnujících vzor
Někdy se hodí najít indexy, kde začíná a končí vzor v daném řetězci. K tomu slouží funkce str_locate()
a str_locate_all
. Funkce str_locate(string, pattern)
najde indexy prvního výskytu regulárního výrazu pattern
v řetězci string
. Výsledkem je matice, jejíž řádky odpovídají prvkům vektoru string
. První sloupec je index začátku prvního výskytu vzoru, druhý sloupec je index konce prvního výskytu vzoru. Pokud není vzor v řetězci nalezen, vrátí NA
. Nalezené indexy výskytu řetězce je možné následně použít k extrakci daného řetězce pomocí funkce str_sub()
. To je však ekvivalentní k použití funkce str_extract()
.
<- "#[[:alpha:]]+" # hashtag následovaný jedním nebo více písmeny
r <- c("#Brno je prostě nejlepší a #MU je nejlepší v Brně.",
tweet "Někdy je možné najít zajímavé podcasty na #LSE.",
"Stupnování 'divnosti': divný, divnější, #ParisHilton.",
"Docela prázdný tweet.")
str_locate(tweet, r) # vrací matici indexů prvních výskytů klíčových slov v tweetu
## start end
## [1,] 1 5
## [2,] 43 46
## [3,] 41 52
## [4,] NA NA
str_sub(tweet, str_locate(tweet, r)) # vyřízne tato klíčová slova
## [1] "#Brno" "#LSE" "#ParisHilton" NA
str_extract(tweet, r) # totéž
## [1] "#Brno" "#LSE" "#ParisHilton" NA
K nalezení všech výskytů vzoru ve vektoru řetězců string
slouží funkce str_locate_all(string, pattern)
, která vrací seznam matic indexů. Prvky seznamu odpovídají prvkům vektoru string
. Řádky každé matice odpovídají jednotlivým výskytům vzoru v jednom prvku vektoru string
. První sloupec matice je index začátku výskytu, druhý sloupec je index konce výskytu. Pokud není vzor nalezen, vrací prázdnou matici:
str_locate_all(tweet, r)
## [[1]]
## start end
## [1,] 1 5
## [2,] 28 30
##
## [[2]]
## start end
## [1,] 43 46
##
## [[3]]
## start end
## [1,] 41 52
##
## [[4]]
## start end
Funkce invert_match(loc)
invertuje matici indexů vrácenou funkcí str_locate_all()
, takže obsahuje indexy částí řetězce, které neodpovídají vzoru. Detaily najdete v dokumentaci.
13.4.4 Nahrazení vzoru
Regulární výrazy umožňují i nahrazení částí řetězců, které odpovídají danému regulárnímu výrazu. K tomu slouží funkce str_replace()
a str_replace_all()
. Funkce str_replace(string, pattern, replacement)
nahradí ve vektoru řetězců string
první výskyt vzoru pattern
řetězcem replacement
. Funkce str_replace_all()
má stejnou syntaxi, ale nahradí všechny výskyty vzoru ve vektoru string
.
Začněme nejjednodušším případem. Řekněme, že chceme v každém tweetu skrýt hashtagy: nahradit je řetězcem `“XXX” nebo je úplně vymazat. To můžeme udělat např. takto:
<- "#[[:alpha:]]+"
r str_replace_all(tweet, r, "#XXX") # náhrada hashtagu pomocí #XXX
## [1] "#XXX je prostě nejlepší a #XXX je nejlepší v Brně."
## [2] "Někdy je možné najít zajímavé podcasty na #XXX."
## [3] "Stupnování 'divnosti': divný, divnější, #XXX."
## [4] "Docela prázdný tweet."
str_replace_all(tweet, r, "") # vymazání hashtagu
## [1] " je prostě nejlepší a je nejlepší v Brně."
## [2] "Někdy je možné najít zajímavé podcasty na ."
## [3] "Stupnování 'divnosti': divný, divnější, ."
## [4] "Docela prázdný tweet."
Pokud chceme nějaký text odstranit, můžeme jej nahradit prázdným řetězcem (jak ukazuje příklad výše), nebo použít užitečnou zkratku, kterou nabízí funkce str_remove(string, pattern)
a str_remove_all(string, pattern)
. Ta první odstraní první výskyt regulárního výrazu pattern
z řetězce string
, druhá všechny výskyty. Vymazat hashtag můžeme tedy i takto:
str_remove_all(tweet, r)
## [1] " je prostě nejlepší a je nejlepší v Brně."
## [2] "Někdy je možné najít zajímavé podcasty na ."
## [3] "Stupnování 'divnosti': divný, divnější, ."
## [4] "Docela prázdný tweet."
Funkce str_replace_all()
dokáže nahradit více vzorů naráz. K tomu stačí nahradit vzor pattern
a nahrazující řetězec replacement
pojmenovaným vektorem řetězců: jména prvků vektorů určují, co se nahrazuje, hodnoty určují, čím se nahrazuje. Řekněme, že chceme nahradit slova “jeden,” “dva” a “tři” odpovídajícími číslicemi:
<- c("jeden banán", "dva pomeranče", "tři mandarinky")
ovoce str_replace_all(ovoce,
c("jeden" = "1", "dva" = "2", "tři" = "3"))
## [1] "1 banán" "2 pomeranče" "3 mandarinky"
Regulární výrazy však dokáží víc než jen nahradit kus řetězce jiným fixním řetězcem (např. “XXX”): dokáží náhradu zkonstruovat přímo z nahrazovaného textu. K tomu opět slouží skupiny.
Funkce str_replace()
i str_replace_all()
si zapamatují obsah skupin obsažených v regulárním výrazu a mohou jej použít v nahrazujícím řetězci r
. Obsah první skupiny je \1
, druhé skupiny \2
atd. (v R je ovšem třeba zpětné lomítko zdvojit). Řekněme, že chceme hashtagy v tweetech upravit tak, že hashtag bude nejdříve uveden bez křížku, a pak v závorce s křížkem:
<- "#([[:alpha:]]+)"
r str_replace_all(tweet, r, "\\1 (#\\1)")
## [1] "Brno (#Brno) je prostě nejlepší a MU (#MU) je nejlepší v Brně."
## [2] "Někdy je možné najít zajímavé podcasty na LSE (#LSE)."
## [3] "Stupnování 'divnosti': divný, divnější, ParisHilton (#ParisHilton)."
## [4] "Docela prázdný tweet."
Paměť v regulárních výrazech se dá použít na celou řadu praktických úprav řetězců. Řekněme například, že máme vektor datumů v anglosaském formátu “MM-DD-YYYY,” a chceme je převést do českého formátu “DD. MM. YYYY.” Bez regulárních výrazů to může být pracná záležitost. S použitím regulárních výrazů je to hračka:
<- c("born: 06-01-1921", "died: 02-01-2017", "no information at all")
datumy str_replace_all(datumy, "(\\d{2})-(\\d{2})-(\\d{4})", "\\2. \\1. \\3")
## [1] "born: 01. 06. 1921" "died: 01. 02. 2017" "no information at all"
13.4.5 Rozdělení řetězců podle vzoru
Často je potřeba rozdělit řetězec do několika částí oddělených nějakým vzorem. K tomu slouží funkce str_split(string, pattern, n = Inf, simplify = FALSE)
a str_split_fixed(string, pattern, n)
, které rozdělí řetězec string
v bodech, kde je nalezen vzor pattern
. Celé číslo n
určuje, na kolik částí je řetězec rozdělen. Funkce str_split()
nepotřebuje mít n
zadané (implicitně rozdělí řetězec do tolika částí, do kolika je potřeba). Funkce vrací seznam, jehož každý prvek odpovídá jednomu prvku vektoru string
. Funkce str_split_fixed()
musí mít zadaný počet n
a vrací matici, jejíž řádky odpovídají prvkům vektoru string
a sloupce jednotlivým nálezům (přebytečné sloupce jsou naplněné prázdným řetězcem ""
). Pokud je počet n
nedostatečný, je nerozdělený zbytek řetězce vložen do posledního zadaného sloupce. Pokud však funkci str_split()
nastavíme parameter simplify
na TRUE
, pak také zjednoduší svůj výsledek do matice.
<- c("jablka a hrušky a hrozny", "pomeranče a fíky a datle a pomela")
ovoce str_split(ovoce, " a ")
## [[1]]
## [1] "jablka" "hrušky" "hrozny"
##
## [[2]]
## [1] "pomeranče" "fíky" "datle" "pomela"
str_split(ovoce, " a ", 3)
## [[1]]
## [1] "jablka" "hrušky" "hrozny"
##
## [[2]]
## [1] "pomeranče" "fíky" "datle a pomela"
str_split_fixed(ovoce, " a ", 4)
## [,1] [,2] [,3] [,4]
## [1,] "jablka" "hrušky" "hrozny" ""
## [2,] "pomeranče" "fíky" "datle" "pomela"
str_split_fixed(ovoce, " a ", 3)
## [,1] [,2] [,3]
## [1,] "jablka" "hrušky" "hrozny"
## [2,] "pomeranče" "fíky" "datle a pomela"
str_split(ovoce, " a ", simplify = TRUE)
## [,1] [,2] [,3] [,4]
## [1,] "jablka" "hrušky" "hrozny" ""
## [2,] "pomeranče" "fíky" "datle" "pomela"
Řekněme, že potřebujeme rozdělit řetězce, které obsahují pouze dobře formátované datum v české konvenci na den, měsíc a rok. To můžeme udělat např. takto:
<- c("1.6.2001", "1. 2. 2003", "blábol")
datum str_split_fixed(datum, "\\.\\s*", 3)
## [,1] [,2] [,3]
## [1,] "1" "6" "2001"
## [2,] "1" "2" "2003"
## [3,] "blábol" "" ""
Pokud řetězce s daty nejsou formátované dobře, ale mohou být v jednom z formátů “DD.MM.YYYY,” “DD. MM. YYYY” nebo “DD-MM-YYYY,” pak stačí jen zobecnit regulární výraz, který jednotlivé složky data odděluje:
<- c("10-5-1999", "1.6.2001", "1. 12. 2003", "blábol")
datum str_split_fixed(datum, "(\\.\\s*|-)", 3)
## [,1] [,2] [,3]
## [1,] "10" "5" "1999"
## [2,] "1" "6" "2001"
## [3,] "1" "12" "2003"
## [4,] "blábol" "" ""
(Pro praci s volne formatovanymi daty ma mnoho uzitecnych funkci balik lubridate, viz oddil 6.2.)
Někdy chceme s řetězci pracovat po slovech. K tomu slouží funkce word()
, která rozdělí řetězec na slova a vrátí slova se zadanými indexy. Použití je
word(string, start = 1L, end = start, sep = fixed(" "))
kde string
je vektor rozdělovaných řetězců, start
je index prvního vraceného slova, end
je index posledního vraceného slova a sep
je regulární výraz, který odděluje slova (implicitně je to jedna mezera). Indexy mohou být i záporné – pak se počítají odzadu, tj. -1
je poslední slovo. Všechny parametry se recyklují.
Na příklady se podívejte do dokumentace.