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

s1 <- c("matka", "mačka", "mačká", "maska", "Matka", "marka!", "ma ka")
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í:

s1[str_detect(s1, "ma.ka")]
## [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:

r <- "(\\+420)?[-\\s]*\\d{3}[-\\s]*\\d{3}[-\\s]*\\d{3}"
cisla <- c("Leoš: 777 666 555 444",
           "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ů:

ovoce <- c("jablko", "ananas", "hruška", "rybíz")
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.):

r <- "#[[:alpha:]]+"  # hashtag následovaný jedním nebo více písmeny
tweet <- c("#Brno je prostě nejlepší a #MU je nejlepší v Brně.",
           "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():

r <- "#([[:alpha:]]+)"  # totéž, co výše, ale všechna písmena tvoří skupinu
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:

r <- "(\\+420)?[-\\s]*(\\d{3})[-\\s]*(\\d{3})[-\\s]*(\\d{3})"
cisla <- c("Leoš: 777 666 555 444",
           "Lída: domů +420 734 123 456, práce 777-666-555",
           "Leona: nevím")
cisla2 <- str_match(cisla, r)[, 3:5]
purrr::map_chr(1:nrow(cisla2), ~ str_c(cisla2[., ], collapse = "-"))
## [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().

r <- "#[[:alpha:]]+"  # hashtag následovaný jedním nebo více písmeny
tweet <- c("#Brno je prostě nejlepší a #MU je nejlepší v Brně.",
           "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:

r <- "#[[:alpha:]]+"
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:

ovoce <- c("jeden banán", "dva pomeranče", "tři mandarinky")
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:

r <- "#([[:alpha:]]+)"
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:

datumy <- c("born: 06-01-1921", "died: 02-01-2017", "no information at all")
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.

ovoce <- c("jablka a hrušky a hrozny", "pomeranče a fíky a datle a pomela")
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:

datum <- c("1.6.2001", "1. 2. 2003", "blábol")
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:

datum <- c("10-5-1999", "1.6.2001", "1. 12. 2003", "blábol")
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.