8.4 Funkce pro práci s regulárními výrazy

Všechny funkce pro práci s regulárními výrazy v balíku stringr mají stejný interface:

str_jméno(řetězec, vzor)

kde řetězec je vektor zpracovávaných řetězců a vzor je použitý regulární výraz (některé funkce mají i další parametry). Jména všech těchto funkcí začínají na str_. Pokud některá funkce vrací je první výskyt hledaného vzoru, pak odpovídají funkce končící na _all vrací všechny výskytu tohoto vzoru.

8.4.1 Detekce vzoru

K detekci, zda řetězec obsahuje zvolený regulární výraz, slouží funkce str_detect(s, p), kde s je prohledávaný vektor řetězců a p je hledaný vzor. Funkce vrací logický vektor stejné délky jako s s hodnotou TRUE, pokud byl vzor nalezen, a hodnotou FALSE v opačném případě. Příklady viz výše.

8.4.2 Výběr řetězců, které odpovídají vzoru

Funkce str_subset(s, p) vrací ty prvky vektoru s, které obsahují vzor p. Je vlastně ekvivalentní výrazu

x[str_detect(x, p)]

Řekněme, že chceme vybrat ty řádky, 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"

8.4.3 Počet výskytů vzoru v řetězci

Funkce str_count(s, p) spočítá počet výskytů regulárního výrazu p v řetězci s. Pracuje vektorově přes zadaný vektor řetězců s i přes regulární výraz p. Implicitní hodnota p 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).

ovoce <- c("jablo", "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ů
## [1] 4 8

8.4.4 Získání částí řetězců, které splnují vzor

Funkce str_extract(s, p) získá z každého řetězce ve vektoru s tu jeho část, která odpovídá prvnímu výskytu vzoru p. Funkce vrací vektor stejné délky jako s; pokud není vzor nalezen, vrací NA.

Výběr klíčových slov z tweetů:

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_all(s, p, simplify) vrátí všechny výskytu tohoto vzoru. Pokud není parametr simplify zadán nebo je FALSE, vrací funkce seznam, jehož prvky odpovídají prvkům vektoru s; nenalezené výskyty jsou pak prázdný vektor řetězců (character(0)). Pokud je simplify = TRUE, pak vrací matici, jejíž řádky odpovídají prvkům vektoru s; 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(s, p) a str_match_all(s, p), kde s je vektor prohledávaných řetězců a p 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 p v řetězci a vrací matici, jejíž řádky odpovídají prvkům vektoru s. První sloupec je celý regulární výraz, druhý sloupec první skupina v regulárním výrazu, třetí sloupec druhá skupina atd. (Skupiny mohou být zanořené. Jejich pořadí se počítá podle pořadí levé závorky.) Nenalezené prvky mají hodnotu NA.

Pokud např. chceme získat jméno odkazovaného předmětu bez hashtagu, můžeme vzít druhý sloupec následující matice:

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

Funkce str_match_all() vrací všechny výskyty regulárního výrazu. Jejím výsledkem je seznam matic. Rádky těchto matic odpovídají jednotlivým nálezům. Sloupce mají význam jako výše. Nenalezené prvky mají v tomto případě hodnotu character(0).

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]

Příklad: převod českého telefonního čísla do standardního tvaru (budeme brát jen první číslo pro každého člověka):

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]
apply(cisla2, 1, function(x) str_c(x, collapse = "-"))
## [1] "777-666-555" "734-123-456" NA

8.4.5 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(s, p) najde indexy prvního výskytu regulárního výrazu p v řetězci s. Výsledek je matice, jejíž řádky odpovídají prvkům vektoru s. První sloupce 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:

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ů s slouží funkce str_locate_all(s, p), která vrací seznam matic indexů. Prvky seznamu odpovídají prvkům vektoru s. Rádky každé matice odpovídají jednotlivým výskytům vzoru v jednom prvku vektoru s. První sloupec matice je index začátku výskytu, druhý sloupec je index konce výskytu. Pokud není vzor nalezen, vrací matici s nula řádky:

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 viz dokumentace.

8.4.6 Nahrazení vzoru

K nahrazení vzoru ve vektoru řetězců slouží funkce str_replace() a str_replace_all(). Funkce str_replace(s, p, r) nahradí ve vektoru řetězců s první výskyt vzoru p řetězcem r. Funkce str_replace_all() má stejnou syntaxi, ale nahradí všechny výskyty vzoru ve vektoru s.

Řekněme, že chceme v každém tweetu nahradit všechna klíčová slova řetězcem `“XXX”:

r <- "#[[:alpha:]]+"
str_replace_all(tweet, r, "#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."

Klíčové slovo vymažeme tak, že je nahradíme prázdným řetězcem "":

str_replace_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 p a nahrazující řetězec r vektorem řetězců: jména prvků vektorů určují, co se nahrazuje, hodnoty určují, čím se nahrazuje:

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"

Obě funkce 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 zdvojit klíčová slova v tweetech upravit tak, že slovo bude nejdříve uvedeno bez hashtagu, a pak v závorce s hashtagem:

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."

Příklad: máte vektor datumů v anglosaském formátu, tj. “MM-DD-YYYY”, a chcete je převést do českého formátu “DD. MM. YYYY”:

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

8.4.7 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(s, p, n) a str_split_fixed(s, p, n), které rozdělí řetězec s v bodech, kde je nalezen vzor p. 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 s. Funkce str_split_fixed() musí mít zadaný počet n a vrací matici, jejíž řádky odpovídají prvkům vektoru s a sloupce jednotlivým nálezům (přebytečné sloupce jsou naplněné prázdným řetězcem "").

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"

Pokud je počet n nedostatečný, je nerozdělený zbytek řetězce vložen do posledního zadaného sloupce.

Příklad: rozdělit dobře formátované datum v české konvenci na den, měsíc a rok:

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" ""   ""

Příklad: rozdělit datum ve tvaru “DD.MM.YYYY” nebo “DD. MM. YYYY” nebo “DD-MM-YYYY” na den, měsíc a rok:

datum <- c("1-5-1999", "1.6.2001", "1. 2. 2003", "blábol")
str_split_fixed(datum, "(\\.\\s*|-)", 3)
##      [,1]     [,2] [,3]  
## [1,] "1"      "5"  "1999"
## [2,] "1"      "6"  "2001"
## [3,] "1"      "2"  "2003"
## [4,] "blábol" ""   ""

8.4.8 Extrakce slov

Funkce word() rozdělí řetězec na slova a vrátí slova se zadanými indexy. Použití je

word(s, start, end, sep)

kde s 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 fixed(" "), tj. 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.

8.4.9 Modifikace chování regulárních výrazů

Chování regulárních výrazů je možné ve funkcích z balíku stringr modifikovat pomocí čtyř funkcí:

  • fixed(pattern, ignore_case = FALSE) porovnává řetězec doslovně, takže znaky bere doslovně jako byty. Je to rychlé, ale nemusí to vždy fungovat pro ne-ASCII znaky. Pokud se má např. tečka chápat doslovně jako tečka a ne jako jakýkoli znak, stačí ji zabalit: fixed(".").

  • coll(pattern, ignore_case = FALSE, locale = NULL, ...) porovnává řetězce tak, že respektuje standard collation rules.

  • regex(pattern, ignore_case = FALSE, multiline = FALSE, comments = FALSE, dotall = FALSE, ...) je použit implicitně. Používá regulární výrazy podle standardu ICU a umožňuje je mírně modifikovat.

  • boundary(type = c("character", "line_break", "sentence", "word"), skip_word_none = TRUE, ...) hledá hranice mezi věcmi. Např. boundary("word") hledá hranice mezi slovy.

Argumenty funkcí mají následující významy:

  • pattern je vzor (regulární výraz), jehož chování chceme modifikovat

  • ignore_case zda se má ignorovat rozdíl mezi malými a velkými písmeny (TRUE chápe malá a velká písmena jako zaměnitelná)

  • locale určí locale, které se má použít pro porovnávání, viz stri_locale_list(); implicitně systémové locale

  • multiline – pokud je TRUE, pak $ a ^ matchují začátek a konec řádku; pokud je FALSE (což je implicitní hodnota), pak mačují začátek a konec celého řetězce

  • comments – pokud je TRUE, pak se ignorují mezerové znaky a komentáře začínající #; doslovné mezery je třeba escapovat pomocí zpětného lomítka

  • dotall – pokud je TRUE, tečka znamená nejen znaky, ale i konec řádku

  • type označí typ hranice, která se hledá; může nabývat hodnot "character", "line_break", "sentence", "word"

  • skip_word_none ignoruje “slova”, která neobsahují žádná písmena nebo číslice, tj. punctuation.

Příklady z dokumentace:

pattern <- "a.b"
strings <- c("abb", "a.b")
str_detect(strings, pattern)
## [1] TRUE TRUE
str_detect(strings, fixed(pattern))
## [1] FALSE  TRUE
str_detect(strings, coll(pattern))
## [1] FALSE  TRUE
# coll() is useful for locale-aware case-insensitive matching
i <- c("I", "\u0130", "i")
i
## [1] "I" "İ" "i"
str_detect(i, fixed("i", TRUE))
## [1]  TRUE FALSE  TRUE
str_detect(i, coll("i", TRUE))
## [1]  TRUE FALSE  TRUE
str_detect(i, coll("i", TRUE, locale = "tr"))
## [1] FALSE  TRUE  TRUE
# Word boundaries
words <- c("These are   some words.")
str_count(words, boundary("word"))
## [1] 4
str_split(words, " ")[[1]]
## [1] "These"  "are"    ""       ""       "some"   "words."
str_split(words, boundary("word"))[[1]]
## [1] "These" "are"   "some"  "words"
# Regular expression variations
str_extract_all("The Cat in the Hat", "[a-z]+")
## [[1]]
## [1] "he"  "at"  "in"  "the" "at"
str_extract_all("The Cat in the Hat", regex("[a-z]+", TRUE))
## [[1]]
## [1] "The" "Cat" "in"  "the" "Hat"
str_extract_all("a\nb\nc", "^.")
## [[1]]
## [1] "a"
str_extract_all("a\nb\nc", regex("^.", multiline = TRUE))
## [[1]]
## [1] "a" "b" "c"
str_extract_all("a\nb\nc", "a.")
## [[1]]
## character(0)
str_extract_all("a\nb\nc", regex("a.", dotall = TRUE))
## [[1]]
## [1] "a\n"

8.4.10 Funkce, které vrací seznamy

Mnoho funkcí z balíku stringr vrací seznamy. Oddíl “Functions that return lists” ve vinětě k balíku stringr ukazuje, jak dál jak dál zpracovávat výstupy těchto funkcí.