11  Práce s řetězci

Analýza dat nezahrnuje jen práci s čísly, ale i s řetězci (texty). Když pomineme textovou analýzu jako takovou, velmi často jsou data zabalena v textovém balastu a je třeba je z něj extrahovat. R má v základním balíku base mnoho užitečných funkcí pro práci s řetězci. Tyto funkce však mají často složité a vzájemně nekonzistenční rozhraní. Proto se zde místo nich podíváme na funkce implementované v balíku stringr (a také jednu funkci z balíku glue), který výrazně zjednodušuje práci s řetězci a stále pokrývá velkou většinu toho, co člověk potřebuje. Jména všech funkcí z balíku stringr začínají str_.

Poznámka: Balík stringr je uživatelsky přívětivý wrapper nad funkcemi balíku stringi; proto často vypisuje chyby ze stringi a stejně tak část dokumentace je třeba hledat v tomto balíku.

Pro práci s touto kapitolou je tedy nezbytné načíst balík stringr do paměti počítače:

library(stringr)

V této kapitole se naučíte

a mnoho dalšího.

11.1 Základy: řetězce v R

R ukládá řetězce ve vektorech datového typu character. Řetězec se zadává mezi dvěma uvozovkami nebo dvěma apostrofy. Uvozovky a apostrofy nejde míchat, ale je možné uzavřít apostrof mezi uvozovky nebo naopak:

"Petr řekl: 'Už tě nemiluji.'"
'Agáta odpověděla: "Koho to zajímá?"'

Kromě uvozovek a apostrofů má v řetězcích zvláštní význam i zpětné lomítko \. To slouží ke dvěma účelům: 1) uvozuje speciální znaky jako např. "\n" (konec řádku), "\t" (tabelátor) apod., a 2) zbavuje speciální znaky jejich zvláštního významu. To provedeme tak, že zvláštnímu znaku předřadíme zpětné lomítko. Tímto způsobem je možné zbavit zvláštního významu i uvozovky, apostrofy a zpětná lomítka a uvést je uvnitř řetězce:

s <- "Petr na to odvětil: \"Je to proto, že vypadáš jako \\.\""

Podobně můžete v řetězcích zadat i znaky Unicode pomocí \u a příslušného čísla. Znak velká omega (\(\Omega\)) je např. možné zadat jako \u03a9. (Tabulku Unicode najdete na adrese https://unicode-table.com/.)

Funkce print(), která se používá při implicitním vypsání obsahu proměnné, vypisuje na začátku i na konci řetězců uvozovky a všechny speciální znaky vypisuje se zpětnými lomítky. Pokud chcete vypsat řetězec “normálně”, můžete použít funkci cat():

print(s)
[1] "Petr na to odvětil: \"Je to proto, že vypadáš jako \\.\""
cat(s)
Petr na to odvětil: "Je to proto, že vypadáš jako \."

Kromě funkcí print() a cat() existuje mnoho dalších funkcí pro výpis řetězců. Nejvýznamnější z nich uvádí tabulka 11.1. Na detaily použití těchto funkcí se podívejte do dokumentace.

Tabulka 11.1: Vybrané funkce pro výpis řetězců.
funkce účel
print() generická funkce pro tisk objektů
noquote() tisk bez uvozovek
cat() tisk obsahu řetězců; spojuje více řetězců
format() formátování proměnných před tiskem
toString() konverze na řetězec
sprintf() formátování řetězců ve stylu jazyka C

V R je potřeba rozlišovat mezi prázdným řetězcem (tj. řetězcem "") a prázdným vektorem řetězců:

s1 <- ""            # vektor délky 1 obsahující prázdný řetězec
length(s1)
[1] 1
s2 <- character(0)  # vektor typu character, který má nulovou délku,
length(s2)          # tj. neobsahuje žádné řetězce
[1] 0
s3 <- character(5)  # vektor pěti prázdných řetězců
length(s3)
[1] 5
s3
[1] "" "" "" "" ""

Všimněte si, že funkce length() vrací délku vektoru, ne délku řetězce, který vektor snad obsahuje.

R má dva speciální vektory, které obsahují písmena anglické abecedy:

letters  # malá písmena anglické abecedy
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"
LETTERS  # velká písmena anglické abecedy
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

Pokud potřebujete porovnat, zda jsou dva řetězce stejné, můžete použít operátor == i funkci all.equal(). (Samozřejmě můžete použít i >, < apod. pro zjištění, který řetězec by by byl umístěn ve slovníku dříve; později v této kapitole však uvidíte funkce, které vám dají nad řazením řetězců větší kontrolu.)

"abc" == "abc"
[1] TRUE
"abc" == "ABC"
[1] FALSE
all.equal("abc", "abc", "abc")
[1] TRUE

Sofistikovaněji můžete řetězce porovnat i pomocí funkce str_equal() z balíku stringr. Ten umožňuje zadat locale, ve kterém se má porovnání provést (výchozí locale je angličtina) a ignorovat velikost písmen:

str_equal("abc", "ABC")
[1] FALSE
str_equal("abc", "ABC", ignore_case = TRUE)
[1] TRUE

Poznámka: Počítač dokáže zpracovávat jen sekvence jedniček a nul. Aby bylo jasné, jak binární čísla interpretovat jako text, musí být jasné kódování tohoto textu. Každý řetězec může mít v R jiné kódování. Nativně umí R dvě kódování: ISO Latin 1 a UTF-8. Kromě toho zvládá i nativní kódování operačního systému počítače, na kterém běží. Většinou se o kódování nemusíte nijak starat. Problém může nastat jen v případě přenosu dat mezi různými operačními systémy (v rámci Windows i mezi různými lokalizacemi). Naštěstí umí R změnit kódování i převést text z jednoho kódování do jiného. Pokud potřebujete data přenášet mezi počítači, zvažte použití standardního kódování UTF-8. Základní dokumentaci ke kódování poskytuje help("Encoding"). Ke konverzi kódování řetězců je možné použít funkci iconv() z balíku base nebo funkci str_conv() z balíku stringr. Detaily použití obou funkcí najdete v dokumentaci.

11.2 Základní operace

Nejdříve se podíváme na základní operace s řetězci: na to, jak řetězce spojovat, třídit, replikovat, nahrazovat, srovnávat a zjišťovat jejich délku. Ve zbytku kapitoly se pak budeme věnovat pokročilejší práci s řetězci pomocí regulárních výrazů.

Zjištění délky řetězce

Často potřebujeme zjistit délku jednotlivých řetězců. To nemůžeme udělat pomocí funkce length(), protože ta nevrací délku řetězce, ale délku vektoru řetězců. Nebude to fungovat ani v případě jediného řetězce, protože i to je vektor o délce 1. (Pamatujte, že R nemá skalár: to, co vypadá jako jeden řetězec, je ve skutečnosti vektor řetězců o délce 1.)

Ke zjištění délky řetězce slouží funkce str_length(), která vrací vektor délek jednotlivých řetězců ve vektoru:

s1 <- "Ahoj!"
s2 <- c("A", "Bb", "Ccc", NA)
length(s1)  # délka (= počet prvků) vektoru s1
[1] 1
length(s2)  # délka (= počet prvků) vektoru s2
[1] 4
str_length(s1)  # vektor délek (= počtu znaků) řetězce s1
[1] 5
str_length(s2)  # vektor délek (= počtu znaků) řetězců ve vektoru s2
[1]  1  2  3 NA

Pozor: Technicky vzato vrací funkce str_length() počet tzv. “code points”. Ty většinou odpovídají jednotlivým znakům, ne však vždy. Např. znak á může být v paměti počítače reprezentován jako jeden znak nebo dva znaky (a a akcent). Ve druhém případě vrátí funkce str_length() délku 2. V takovém případě je bezpečnější počítat počet znaků pomocí funkce str_count(), viz dále. Příklad najdete v dokumentaci funkce. Pokud jsou však anglické i české znaky zadané obvyklým způsobem, pak bude funkce str_length() vracet počet znaků v jednotlivých řetězcích.

Podobně funkce str_width() vypíše, kolik místa zabere daný řetězec při použití neproproportionalního písma (např. v terminálu).

Spojení řetězců

Ke spojení více řetězců do jednoho slouží funkce str_c(..., sep = "", collapse = NULL). Funkce bere libovolný počet řetězců, vypustí z nich NULL (ale ne prázdné vektory řetězců a prázdné řetězce "") a zbylé řetězce spojí do jednoho řetězce. Funkce str_c() implicitně odděluje jednotlivé řetězce prázdným řetězcem, tj. spojí jednotlivé řetězce těsně za sebe. Oddělovací řetězec je možné nastavit pomocí pojmenovaného parametru sep. (Všimněte si, že prázdný řetězec "" funkce nevypustila.)

str_c("Jednou", "", "budem", "možná", NULL, "dál!")
[1] "Jednoubudemmožnádál!"
str_c("Jednou", "", "budem", "možná", NULL, "dál!", sep = " ")
[1] "Jednou  budem možná dál!"
str_c("Jednou", "", "budem", "možná", NULL, "dál!", sep = "-")
[1] "Jednou--budem-možná-dál!"

Všimněte si, že zařazení prázdného vektoru vede na prázdný řetězec:

str_c(
     "Jednou", "", "budem", character(0), "možná", NULL, "dál!",
     sep = "-"
)
character(0)

Někdy potřebujeme spojit vektory řetězců. Funkce str_c() spojí odpovídající prvky jednotlivých vektorů. Balík stringr v novějších verzích nepodporuje obvyklou recyklaci vektorů, takže buď musejí mít všechny vektory stejnou délku, nebo musí mít délku 1. Pokud tedy chceme kratší vektor recyklovat, musíme to provést sami, např. pomocí funkce rep(). (Rozumnější použití uvidíte dále.)

s1 <- c("a", "b", "c", "d")
s2 <- 1:4  # automatická koerze převede čísla na řetězce
str_c(s1, s2)
[1] "a1" "b2" "c3" "d4"

Pokud navíc chceme výsledný vektor spojit do jednoho řetězce, můžeme použít parametr collapse, kterým se nastavuje řetězec oddělující jednotlivé dílčí vektory (funguje i prázdný řetězec ""):

str_c(s1, s2, collapse = "-")
[1] "a1-b2-c3-d4"
str_c(s1, s2, collapse = "")
[1] "a1b2c3d4"

Celkově funguje funkce str_c() takto: své parametry chápe jako vektory řetězců. Tyto vektory sestaví do matice, kde každý vstupní vektor tvoří jeden sloupec této matice (přitom prodlouží vektory délky 1 na délku ostatních vektorů). Pak vloží mezi jednotlivé sloupce řetězec sep a spojí každý řádek do jednoho řetězce, takže výsledkem je jeden vektor řetězců. Pokud je navíc zadán řetězec collapse (tj. není NULL), funkce vloží tento řetězec mezi jednotlivé prvky tohoto vektoru a spojí je do jednoho řetězce. Tabulka 11.2 ukazuje, co udělá výraz str_c(s1, s2, sep = "-").

Tabulka 11.2: Ukázka toho, jak funguje výraz str_c(s1, s2, sep = "-").
s1 (sep) s2 výsledek
a - 1 a-1
b - 2 b-2
c - 3 c-3
d - 1 d-1

Pokud má kterýkoli řetězec ve spojovaných vektorech hodnotu NA, pak je výsledné spojení také NA:

s1 <- c("a", "b", NA)
s2 <- 1:3
str_c(s1, s2)
[1] "a1" "b2" NA  
str_c(s1, s2, collapse = "-")
[1] NA

Některé zajímavé příklady použití funkce str_c() jsme vybrali z dokumentace funkce:

str_c("Letter", letters, sep = ": ")
 [1] "Letter: a" "Letter: b" "Letter: c" "Letter: d" "Letter: e" "Letter: f"
 [7] "Letter: g" "Letter: h" "Letter: i" "Letter: j" "Letter: k" "Letter: l"
[13] "Letter: m" "Letter: n" "Letter: o" "Letter: p" "Letter: q" "Letter: r"
[19] "Letter: s" "Letter: t" "Letter: u" "Letter: v" "Letter: w" "Letter: x"
[25] "Letter: y" "Letter: z"
str_c(letters[-26], " comes before ", letters[-1])
 [1] "a comes before b" "b comes before c" "c comes before d" "d comes before e"
 [5] "e comes before f" "f comes before g" "g comes before h" "h comes before i"
 [9] "i comes before j" "j comes before k" "k comes before l" "l comes before m"
[13] "m comes before n" "n comes before o" "o comes before p" "p comes before q"
[17] "q comes before r" "r comes before s" "s comes before t" "t comes before u"
[21] "u comes before v" "v comes before w" "w comes before x" "x comes before y"
[25] "y comes before z"
str_c(letters, collapse = ", ")
[1] "a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z"

Pokud jako v předchozím případě potřebujeme spojit vektor řetězců do jednoho řetězce, můžeme použít i jednodušší funkci str_flatten(string, collapce = ""):

str_flatten(letters, collapse = ", ")
[1] "a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z"

Ta umí i pár dalších triků: je možné nastavit zvlášť oddělovač pro poslední dva řetězce pomocí parametru last a je možné ignorovat hodnoty NA pomocí parametru na.rm:

s <- c("cukr", "káva", "limonáda")
str_flatten(s, collapse = ", ", last = " a ")
[1] "cukr, káva a limonáda"

Při práci s angličtinou se hodí i varianta této funkce s názvem str_flatten_comma(), která používá tzv. Oxford comma, jak ukazuje příklad převzatý z její dokumentace:

# str_flatten_comma knows how to handle the Oxford comma
str_flatten_comma(letters[1:3], ", and ")
[1] "a, b, and c"
str_flatten_comma(letters[1:2], ", and ")
[1] "a and b"

Doplnění hodnot do řetězců

Někdy potřebujeme spojit několik řetězců tak, že na vybraná místa do textu “vlepíme” hodnoty z vybraných proměnných – např. když chceme sestavit výpis z účtu:

cislo_uctu <- 854397
zustatek <- 12365.30
str_c("Na účtu číslo ", cislo_uctu, " je aktuální zůstatek ",
      format(zustatek, big.mark = ".", decimal.mark = ",", nsmall = 2),
      " Kč.")
[1] "Na účtu číslo 854397 je aktuální zůstatek 12.365,30 Kč."

Využít ke “vlepení” hodnot do řetězce funkci str_c(), jak to ukazuje výše uvedený příklad, je možné, ale výsledek není právě přehledný. Místo toho můžeme použít speciální funkci glue() z balíku glue:

library(glue)
hezky_zustatek <- format(zustatek, big.mark = '.',
                                decimal.mark = ',', nsmall = 2)
glue("Na účtu číslo {cislo_uctu} je aktualni zustatek {hezky_zustatek} Kč.")
Na účtu číslo 854397 je aktualni zustatek 12.365,30 Kč.

Funkce glue() vezme jeden nebo více řetězců, slepí je dohromady a na místa ve složených závorkách vloží hodnoty proměnných z aktuálního prostředí R, přesněji výsledek výpočtu, takže předchozí kód bychom mohli napsat i takto:

glue(
     "Na účtu číslo {cislo_uctu} je aktualni zustatek ",
     "{format(zustatek, big.mark = '.', decimal.mark = ',', nsmall = 2)} Kč."
)
Na účtu číslo 854397 je aktualni zustatek 12.365,30 Kč.

Hodnoty vložených proměnných můžeme zadat přímo jako parametry funkce glue():

glue("Na účtu číslo {cislo_uctu} je aktualni zustatek {zustatek} Kč.",
     cislo_uctu = 24683, zustatek = 123)
Na účtu číslo 24683 je aktualni zustatek 123 Kč.

Je také možné je vytáhnout ze seznamu nebo z prostředí (environment). K tomu slouží parametr .envir:

person <- list(
     name = "Joe",
     age = 40
)
glue(
     "Jmenuji se {person$name} a příští rok mi bude ",
     "{person$age + 1} let."
)
Jmenuji se Joe a příští rok mi bude 41 let.
glue(
     "Jmenuji se {name} a příští rok mi bude {age + 1} let.",
     .envir = person
)
Jmenuji se Joe a příští rok mi bude 41 let.

Funkce glue() má několik zajímavých vlastností: zahazuje počáteční a koncová bílá místa (mezery apod.) včetně prvního a posledního zalomení řádku, ostatní zalomení a mezery však respektuje:

glue("
   Toto je jakýsi text.
        A zde pokračuje...
     ")
Toto je jakýsi text.
     A zde pokračuje...
glue("

   Toto je jakýsi text.
        A zde pokračuje...

        ")

Toto je jakýsi text.
     A zde pokračuje...

Zalomení řádků je možné zabránit pomocí (zdvojeného) lomítka na jeho konci:

glue("Zde nějaký text začíná, \\
     a zde pokračuje.")
Zde nějaký text začíná, a zde pokračuje.

Pokud z nějakého důvodu potřebujeme v textu použít složené závorky, máme dvě možnosti: buď je zdvojit, nebo pomocí parametrů .open a .close nastavit jiný počátek a konec nahrazované oblasti na jiný znak:

glue("Zde je {{jméno}}.")
Zde je {jméno}.
# vhodné např. při exportu do LaTeXu
glue(
     "Jméno výherce soutěže je {\\bf <<name>>}.", .open = "<<", .close =">>",
     name = "Joe"
)
Jméno výherce soutěže je {\bf Joe}.

Balík stringr nabízí podobnou funkcionalitu ve funkcích str_glue() a str_interp(). Funkce str_glue() je v podstatě jen wrapper nad funkcí glue() a používá se stejně:

str_glue("Jmenuji se {name} a příští rok mi bude {age + 1} let.",
         name = "Jana", age = 40)
Jmenuji se Jana a příští rok mi bude 41 let.

Naproti tomu funkce str_interp() má poněkud jiné použití. V jejím případě musí mít nahrazovaný text buď podobu ${}, kde ve složených závorkách je nějaký výraz, nebo $[]{}, kde v hranatých závorkách je formát a v složených výraz. Formát určuje typicky formátování čísel – např. .2f znamená, že se vytisknou dvě desetinná místa (detaily formátu viz nápověda k funkci sprintf()). Pokud chceme zadat vkládané hodnoty přímo v této funkci, je třeba je zabalit do seznamu.

str_interp(
     "Na účtu číslo ${cislo_uctu} je aktualni zustatek ${zustatek} Kč."
)
[1] "Na účtu číslo 854397 je aktualni zustatek 12365.3 Kč."
str_interp(
     "Na účtu číslo $[d]{cislo_uctu} je aktualni zustatek $[.2f]{zustatek} Kč."
)
[1] "Na účtu číslo 854397 je aktualni zustatek 12365.30 Kč."
str_interp(
     "Na účtu číslo $[d]{cislo_uctu} je aktualni zustatek $[.2f]{zustatek} Kč.",
     list(cislo_uctu = 51349, zustatek = 17)
)
[1] "Na účtu číslo 51349 je aktualni zustatek 17.00 Kč."

Pro použití s trubkami existují varianty funkcí glue() a str_glue(): glue_data() a str_glue_data():

person |> 
     str_glue_data(
          "Jmenuji se {name} a příští rok mi bude {age + 1} let."
     )
Jmenuji se Joe a příští rok mi bude 41 let.

Poznámka: Funkce glue() a spol. nevracejí řetězec, ale objekt třídy glue, která od řetězce dědí. Většinou to ničemu nevadí. Pokud však potřebujete získat řetězec, můžete výsledek převést pomocí funkce as.character().

Řazení řetězců

Pro setřídění řetězců většinou stačí základní funkce sort() a order(). Pro složitější případy, kdy je např. třeba třídit v cizím locale, nabízí balík stringr tři funkce:

  • str_sort(x, decreasing = FALSE, na_last = TRUE, locale = "en", numeric = FALSE, ...) setřídí vektor řetězců x
  • str_order(x, decreasing = FALSE, na_last = TRUE, locale = "en", numeric = FALSE, ...) vrací celé číslo, které odpovídá pořadí daného řetězce ve vektoru x (podobně jako funkce order())
  • str_rank(x, locale = "en", numeric = FALSE, ...) vrací rank daného řetězce ve vektoru x

Přitom x je vektor řetězců, který má být setříděn. decreasing je logická hodnota (implicitní hodnota je FALSE; pak třídí od nejnižšího k nejvyššímu; TRUE třídí od nejvyššího k nejnižšímu). na_last je logická hodnota (implicitní hodnota je TRUE, při které funkce umístí hodnoty NA na konec vektoru; FALSE je umístí na začátek na začátek vektoru; NA je vyhodí). locale označuje v jakém locale se má třídit (implicitně v systémovém). Pokud je logická hodnota numeric nastavena na TRUE, pak číslice řadí jako čísla, ne jako řetězce; implicitní hodnota je FALSE. ... označují další parametry přidané do stri_opts_collator.

s <- c("cukr", "čaj", "čaj", "káva", "limonáda", "rum")
str_sort(
     s,
     decreasing = TRUE,
     locale = "cs"
)
[1] "rum"      "limonáda" "káva"     "čaj"      "čaj"      "cukr"    
str_order(
     s,
     locale = "cs"
)
[1] 1 2 3 4 5 6
str_rank(
     s,
     locale = "cs"
)
[1] 1 2 2 4 5 6

Všimněte si, jak rozdílně řeší funkce str_order() a str_rank() stejné řetězce.

Odstranění a tvorba duplicit

Někdy je potřeba nějaký řetězec “zmnožit”. K tomu slouží funkce str_dup(string, times), který vezme vektor string, zopakuje jej times-krát a výsledek spojí. To se hodí např. při načítání mnoha textových sloupců pomocí balíku readr. Při tom je někdy užitečné říct funkci read_csv(), že všechny sloupce tabulky mají typ character. K tomu slouží řetězec mnoha “c”:

str_dup("c", 28)
[1] "cccccccccccccccccccccccccccc"

Funkce str_dup() také recykluje všechny své parametry:

str_dup(c("a", "b", "c"), 1:3)
[1] "a"   "bb"  "ccc"

Někdy chceme naopak z řetězce duplicity odstranit. K tomu můžeme použít základní funkci unique():

s <- c("čaj", "čaj", "Čaj", "káva", "limonáda", "rum")
unique(s)
[1] "čaj"      "Čaj"      "káva"     "limonáda" "rum"     

Mírně sofistikovanější je funkce str_unique(), která umožňuje nasavit locale (implicitně anglické) a ignorovat velikost písmen:

str_unique(s)
[1] "čaj"      "Čaj"      "káva"     "limonáda" "rum"     
str_unique(s, ignore_case = TRUE)
[1] "čaj"      "káva"     "limonáda" "rum"     

Odstranění okrajových mezer

Někdy dostaneme řetězec, který začíná nebo končí “bílými znaky” (mezerami, tabelátory, novými řádky “\n” apod.). Tato situace vznikne např. tehdy, když řetězec vznikl rozdělením delšího řetězce na části. Tyto bílé znaky je často vhodné odstranit. K tomu slouží funkce str_trim(string, side), kde string je řetězec a side označuje stranu, ze které se mají bílé znaky odstranit:

s1 <- c("Ahoj,", " lidi, ", "jak ", "se", " máte?")
str_trim(s1, "left")   # odstraní mezery zleva
[1] "Ahoj,"  "lidi, " "jak "   "se"     "máte?" 
str_trim(s1, "right")  # odstraní mezery zprava
[1] "Ahoj,"  " lidi," "jak"    "se"     " máte?"
str_trim(s1, "both")   # odstraní mezery z obou stran
[1] "Ahoj," "lidi," "jak"   "se"    "máte?"
str_trim(s1)   # totéž -- "both" je implicitní hodnota side
[1] "Ahoj," "lidi," "jak"   "se"    "máte?"

Funkce str_squish(string) funguje podobně, ale odstraní navíc i všechny přebytečné prázdné znaky – na začátku řetězce string, na jeho konci i opakované znaky uvnitř:

str_squish("  Toto  je   koktavý  text.   ")
[1] "Toto je koktavý text."

Zarovnání a ořež řetězců na stejnou délku a do odstavce

Někdy je užitečné zarovnat řetězce na stejnou délku přidáním bílých (nebo jiných) znaků. K tomu slouží funkce str_pad(string, width, side, pad = " "), kde string je vektor zarovnávaných řetězců, width je minimální délka výsledného řetězce, side je strana, na kterou se mají výplňové znaky přidat (implicitně je to left) a pad je výplňový řetězec (implicitně mezera).

str_pad(c("Ahoj", "lidi"), 7)
[1] "   Ahoj" "   lidi"
str_pad(c("Ahoj", "lidi"), 9, side = "both", pad = "-")
[1] "--Ahoj---" "--lidi---"

Delší řetězce funkce nemění:

str_pad(c("Ahoj", "malé zelené bytosti z Viltvodlu VI"), width = 7)
[1] "   Ahoj"                            "malé zelené bytosti z Viltvodlu VI"

V některých případech potřebujeme delší řetězec zkrátit na určitou délku tak, že se jeho část vynechá. K tomu slouží funkce str_trunc(string, width, side = c("right", "left", "center"), ellipsis = "..."), která zkrátí řetězec string na délku width znaků. Přitom přebytečnou část textu na straně side vynechá a nahradí znaky ellipsis:

s <- "Toto je přehnaně dlouhý text, který toho mnoho neříká."
str_trunc(s, 20, "right")
[1] "Toto je přehnaně ..."
str_trunc(s, 20, "left")
[1] "...oho mnoho neříká."
str_trunc(s, 20, "center")
[1] "Toto je p... neříká."

Zarovnat řetězec do odstavce umožňuje funkce str_wrap(string, width = 80, indent = 0, exdent = 0), kde strings je zarovnávaný řetězec, width je cílová šířka sloupce (implicitně 80 znaků), indent je odsazení prvního řádku a exdent je odsazení následujících řádků (oboje implicitně 0 znaků):

s1 <- "Na počátku bylo Slovo, to Slovo bylo u Boha, to Slovo byl Bůh. To bylo na počátku u Boha. Všechno povstalo skrze ně a bez něho nepovstalo nic, co jest. V něm byl život a život byl světlo lidí. To světlo ve tmě svítí a tma je nepohltila."
cat(str_wrap(s1, 60))
Na počátku bylo Slovo, to Slovo bylo u Boha, to Slovo byl
Bůh. To bylo na počátku u Boha. Všechno povstalo skrze ně
a bez něho nepovstalo nic, co jest. V něm byl život a život
byl světlo lidí. To světlo ve tmě svítí a tma je nepohltila.

Konverze malých a velkých písmen

Ke konverzi malých písmen na velká slouží funkce str_to_upper(string, locale = "en"), ke konverzi na malá písmena funkce str_to_lower(string, locale = "en"), kde string je převáděný řetězec. Funkce str_to_sentence(string, locale = "en") “opraví” velikost písmen ve větě po konci věty. Parametr locale umožňuje zadat popis jazykových pravidel pro převod znaků. Parametr je nepovinný a pokud není zadán, použije se anglické locale.

s <- "Nějaký malý textík..."
str_to_lower(s, locale = "cz")
[1] "nějaký malý textík..."
str_to_upper(s, locale = "cz")
[1] "NĚJAKÝ MALÝ TEXTÍK..."
str_to_sentence("a Toto je děs, že Marku? no jistě! Tož tak.")
[1] "A toto je děs, že marku? No jistě! Tož tak."

Formátování čísel

Někdy potřebujeme vytisknout čísla v “pěkném formátu”. To znamená, že musíme převést dané číslo na zformátovaný řetězec, který dané číslo prezentuje v požadovaném tvaru. K tomu slouží funkce format() ze základního balíku base. Funkce format() je generická a umí formátovat mnoho různých objektů. Pro čísla má následující parametry:

format(x, trim = FALSE, digits = NULL, nsmall = 0L,
       justify = c("left", "right", "centre", "none"),
       width = NULL, na.encode = TRUE, scientific = NA,
       big.mark   = "",   big.interval = 3L,
       small.mark = "", small.interval = 5L,
       decimal.mark = getOption("OutDec"),
       zero.print = NULL, drop0trailing = FALSE, ...)

kde x je numerický vektor. Další užitečné parametry jsou zejména nsmall (minimální počet desetinných míst), big.mark a decimal.mark (znak oddělující tisíce a desetinná místa), scientific (pokud je TRUE, pak vypisuje čísla ve vědecké notaci), width (minimimální počet znaků) a justify (zarovnání). Další parametry jsou popsané v dokumentaci.

x <- 123456.3
print(x)
[1] 123456.3
print(format(x, big.mark = ".", decimal.mark = ",", nsmall = 2))
[1] "123.456,30"
print(format(x, scientific = TRUE))
[1] "1.234563e+05"
print(format(x, width = 10, justify = "left"))
[1] "  123456.3"

Nahrazení chybějících hodnot řetězcem

Někdy máme vektor řetězců, ve kterém některé hodnoty chybí, tj. mají hodnotu NA, a my potřebujeme tyto hodnoty nahradit nějakým textem. K tomu slouží funkce str_replace_na(string, replacement = "NA"), kde string je vektor řetězců a replacement je řetězec, který má v řetězci string nahradit chybějící hodnoty. Pokud chceme např. nahradit chybějící hodnoty slovem “neznámé”, můžeme to udělat takto:

poloha <- c("nahoře", "dole", NA, "vlevo")
str_replace_na(poloha, replacement = "neznámá")
[1] "nahoře"  "dole"    "neznámá" "vlevo"  

Výběr a náhrada pomocí indexů

Někdy je třeba z řetězce vybrat jeho část. Pokud známe pozici prvního a posledního znaku, který chceme vybrat, můžeme použít funkce str_sub(string, start = 1L, end = -1L), kde string je řetězec, ze kterého vybíráme, start je pozice znaku začátku výběru a end je pozice konce výběru (včetně). Implicitní hodnota start je 1 (tj. od začátku vektoru), implicitní hodnota end je \(-1\) (tj. do konce vektoru). Pozice znaků mohou být zadány i jako záporná čísla – ta se počítají od konce vektoru, takže např. \(-1\) je poslední znak vektoru.

s1 <- "Sol 6: Katastrofa na Marsu."
str_sub(s1, start = 8, end = str_length(s1))
[1] "Katastrofa na Marsu."
str_sub(s1, start = 8, end = -1)  # totéž
[1] "Katastrofa na Marsu."
str_sub(s1, 8)  # totéž
[1] "Katastrofa na Marsu."
str_sub(s1, end = 5)
[1] "Sol 6"

Funkce str_sub() recykluje všechny své parametry. Pokud chceme např. vybrat stejné pozice z každého vektoru v řetězci:

s2 <- c(s1, "Sol 7: Nová naděje.")
str_sub(s2, start = c(8, 8))
[1] "Katastrofa na Marsu." "Nová naděje."        
str_sub(s2, 8)  # totéž
[1] "Katastrofa na Marsu." "Nová naděje."        

Stejně tak je však možné recyklovat i řetězec a vybrat z něj naráz dva pod-řetězce:

str_sub(s1, start = c(8, 22), end = c(17, 26))
[1] "Katastrofa" "Marsu"     

Pokud je řetěz kratší než výběr znaků, vrátí funkce str_sub() tolik znaků, kolik může (někdy i prázdný řetězec):

str_sub(s1, start = 25, end = 50)
[1] "su."
str_sub(s1, start = 40, end = 50)
[1] ""

Funkci str_sub() je možné použít i k náhradě části řetězce:

str_sub(s1, 22, 26) <- "rudé planetě"
s1
[1] "Sol 6: Katastrofa na rudé planetě."

Funkce str_sub() extrahuje z každého řetězce ve vektoru jeden pod-řetězec. Podobná funkce str_sub_all() dokáže z každého řetězce ve vektoru extrahovat několik částí současně. Vrací seznam, kde každému prvku vektoru odpovídá jeden prvek seznamu. Jednotlivé extrahované části tvoří prvky vektorů uložených v tomto seznamu:

str_sub_all(s2, start = c(1, 8), end = c(5, 18))
[[1]]
[1] "Sol 6"       "Katastrofa "

[[2]]
[1] "Sol 7"       "Nová naděje"

11.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. 20061. č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 zaměříme na regulární výrazy používané balíkem stringr. (Plné definice těchto standardů najdete v dokumentaci funkcí, help("regex", package = "base") pro POSIX a help("stringi-search-regex", package = "stringi") pro ICU, a ve vinětě Regular expressions balíku stringr.)

Základy

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ězce abc, 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: \\\\.

Pokud potřebujete zabezpečit větší počet souvislých znaků, můžete použít dvojici značek \Q...\E, kde ... označuje zabezpečené znaky:

str_extract(c("abc", "a.c"), "\\Qa.c\\E")
[1] NA    "a.c"

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 některé často užívané výčty existují speciální symboly. Tabulka 11.3 uvádí nejdůležitější z nich. Kromě těchto rozsahů mohou regulární výrazy v R obsahovat i POSIXové rozsahy, které mají zvláštní tvar [:jméno:] a mohou se vyskytovat jen ve vnějším rozsahu, tj. jako [[:jméno:]] nebo např. [a[:jméno:]b] apod. Jejich seznam uvádí tabulka 11.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.1

Tabulka 11.3: Přehled základních rozsahů používaných v regulárních výrazech.
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
Tabulka 11.4: Přehled POSIXových rozsahů používaných v regulárních výrazech.
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:] !, ", #, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, >, ?, @, [, \, ], ^, _, `,{,

Kromě základních rozsahů uvedených výše, existují i další speciální rozsahy, které umožňují zadávat hexadecimální čísla, hledat emoji nebo znaky podle jejich vlastností (např. jen velká písmena, různé druhy uvozovek apod.). Tyto rozsahy najdete ve viněte Regular expressions k balíku stringr.

Kvantifikátory

Znaky *+?{ jsou kvantifikátory, které řídí počet opakování předchozího znaku, rozsahu nebo skupiny. Jejich význam shrnuje tabulka 11.5.

Tabulka 11.5: Přehled POSIXových rozsahů používaných v regulárních výrazech.
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:

s1 <- '"Už je to dobré," řekl Josef. "Pojdme si zaplavat."'
# jeden řetězec od první po poslední uvozovku
str_extract_all(s1, '".*"')
[[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 *?:

# vektor dvou řetězců, v každém je jedna přímá řeč
str_extract_all(s1, '".*?"')
[[1]]
[1] "\"Už je to dobré,\""     "\"Pojdme si zaplavat.\""

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.

Pokud máme řetězce, které obsahují více řádků, můžeme v regulárním výrazu nastavit parametr multiline = TRUE (více k modifikaci regulárních výrazů v oddíle 11.5). Pak ^ znamená začátek každého řádku a $ znamená konec řádku. K dispozici jsou pak i značky \A, která znamená začátek řetězce, \z konec řetězce a \Z konec řetězce před posledním ukončením řádku, pokud existuje. (V R je třeba zapsat \A jako "\\A".)

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 (logické “nebo”). 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:]].)

Speciální “rozhlížející se” (look-ahead a look-behind) skupiny hledají znaky, ale nezařazují je do výsledku, tj. tyto znaky “nesežerou”, ale pouze indikují jejich přítomnost. Tyto skupiny uvádí tabulka 11.6.

Tabulka 11.6: Speciální “rozhlížející se” skupiny.
skupina význam
(?=...) indikuje, že na pozici jsou znaky ...
(?!...) indikuje, že na pozici znaky ... nejsou
(?<=...) indikuje, že před pozicí jsou znaky ...
(?<!...) indikuje, že před pozicí znaky ... nejsou

Vyhodnocování zleva

Regulární výrazy se vyhodnocuje zleva a (až na výjimku “rozhlížejících se” výrazů) se nevracejí. To může mít nečekané důsledky. Řekněme, že ve webové stránce chcete najít všechny odkazy. Zdánlivě by to měl vyřešit následující postup:

s <- '<h1>Title</h1><a href="https://stringr.tidyverse.org/">link</a><p>text</p>'
str_extract(s, "<.*?href.*?>")
[1] "<h1>Title</h1><a href=\"https://stringr.tidyverse.org/\">"

Zatímco druhý líný výraz .*? se vyhodnotí správně, první se vyhodnotí neočekávaně. Marek Gagolewski to vysvětluje takto: “The matching algorithm goes as follows: once the first < is matched, the regexp engine tries to pinpoint A, but without restarts if A is found, i.e., without looking back where there is a match. So the matching is only performed left-to-right. It’s a feature, not a bug.”

Způsob, jak problém obejít, je následující:

str_extract(s, "<[^<]*?href.*?>")
[1] "<a href=\"https://stringr.tidyverse.org/\">"

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:

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

r <- str_c("\\d{1,2}\\.\\s*(\\d{1,2}\\.|leden|únor|březen|duben|červen|červenec|srpen|",
           "září|říjen|listopad|prosinec)\\s*\\d{4}")
dat <- c("1. 6. 2001", "1.1.2012", "1. červen 2016", "ahoj")
str_detect(dat, r)
[1]  TRUE  TRUE  TRUE FALSE

Nakonec se ještě podíváme na rozdíl mezi normálními a “rozhlížejícími se” skupinami. Řekněme, že chceme z vektoru extrahovat peněžní částky, které se skládají z několika číslic, jedné mezery a písmen Kč. “Rozhlížející se” skupina nám umožní extrahovat pouze částku, i když se hledají řetězce, které kromě číslic obsahují i mezeru a Kč:

s <- c("100 Kč", "200", "300 Kč")
str_extract(s, "\\d+( Kč)")
[1] "100 Kč" NA       "300 Kč"
str_extract(s, "\\d+(?= Kč)")
[1] "100" NA    "300"

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 funkci str_view(string, pattern = NULL, match = TRUE, html = FALSE, use_escapes = FALSE). Tato funkce v základu bere vektor řetězců a zobrazí jej. Pokud je navíc zadán parameter pattern, zvýrazní v nich znaky nalezené pomocí regulárního výrazu pattern. V takovém případě str_view() implicitně zobrazuje jen ty prvky řetězce string, který odpovídá regulárnímu výrazu pattern. Pokud chcete zobrazit i ty, které neodpovídají, musíte zadat match = FALSE. Pokud navíc nastavíte parametr html = TRUE, pak funkce zobrazí výsledek v RStudiu v záložce Viewer.

Pohodlné testování regulárních výrazů umožňuje i webová stránka http://regexr.com/ a doplněk RStudia RegExplain. Protože není zatím součástí CRANu, nainstalujete jej takto:

devtools::install_github("gadenbuie/regexplain")

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 a nástrojů pro zpracování XML.

Pamatujte také, že regulární výrazy obsahují mnohem více než to, co jste viděli v tomto oddílu. Plný výčet detailů je na těchto (podle mého mínění velmi nepřehledných) stránkách 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.

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

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

Pokud hledáme výskyt nějakého regulárního výrazu jen na začátku nebo konci řetězce, můžeme k tomu použít “kotvy” (anchors). Balík stringr však nabízí i dvě funkce, které to zjednodušují: str_starts() a str_ends(), které mají stejné parametry jako str_detect(). Řekněme, že ve vektoru s1 hledáme řetězce, které končí na “ka”:

str_detect(s1, "ka$")
[1]  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE
str_ends(s1, "ka")
[1]  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE

Při použití str_ends() si tedy ušetříme znak $ na konci regulárního výrazu. Podobně bychom si ušetřili i úvodní znak ^ ve funkci str_starts().

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

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           

Další možnost, jak vyřešit náš problém, je použít “rozhlížející se” skupinu, v tomto případě zpěthledící skupinu:

str_extract(tweet, "(?<=#)[[:alpha:]]+")
[1] "Brno"        "LSE"         "ParisHilton" NA           

Navíc od verze 1.5 umí funkce str_extract() extrahovat skupiny přímo. K tomu slouží parametr group, který určuje, kterou skupinu má funkce extrahovat:

str_extract(tweet, r, group = 1)
[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           

S pomocí str_extract() to můžeme udělat podobně:

cisla2 <- str_extract(cisla, r, group = 2:4)
purrr::map_chr(
     1:nrow(cisla2),
     ~ str_c(cisla2[., ],
     collapse = "-")
)
[1] "777-666-555" "734-123-456" NA           

Indexy řetězců, které splňují 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.

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"

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 práci s volně formátovanými daty má mnoho užitečných funkcí balík lubridate, viz oddíl 5.2.)

Funkce str_split() má ještě dvě specializované verze: Funkce str_split_1(string, pattern) bere jako svůj vstup jediný řetězec (tj. vektor řetězců délky 1) a rozděluje jej do vektoru řetězců. Naproti tomu funkce str_split_i(string, pattern, i) bere vektor řetězců, rozdělí jej podle vzoru pattern, a pak vrátí v jednom vektoru jen i. prvky:

str_split_1("1.6.2001", "\\.\\s*")
[1] "1"    "6"    "2001"
str_split_i(
     c("1.1.2021", "1.6.2001", "1.12.2021"),
     "\\.\\s*",
     2
)
[1] "1"  "6"  "12"

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.

11.5 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í: regex(), fixed(), coll() a boundary(). Nejdůležitější z nich je funkce regex(). Pokaždé, když zadáte regulární výraz, funkce z balíku stringr na něj tiše uplatní funkci regex(), takže dva následující výrazy jsou zcela ekvivalentní:

s1 <- c("máma", "Máma", "MÁMA")
str_detect(s1, "(.)á\\1a")
[1]  TRUE FALSE FALSE
str_detect(s1, regex("(.)á\\1a"))
[1]  TRUE FALSE FALSE

Funkce regex() umožňuje poněkud modifikovat chování regulárního výrazu. K tomu jí slouží čtyři parametry. Parametr ignore_case umožňuje vypnout rozdíl mezi malými a velkými písmeny (case sensitivity):

str_detect(s1, regex("(.)á\\1a", ignore_case = TRUE))
[1] TRUE TRUE TRUE

Normálně v regulárních výrazech v balíku stringr znamenají znaky ^ a $ začátek a konec řetězce. Pokud mají znamenat začátek a konec řádku (což je obvyklejší), je třeba použít parametr multiline:

s2 <- "1. nakoupíš 7 vajec.\n2. umyješ podlahu\n3. nakrmíš králíky"
cat(s2)
1. nakoupíš 7 vajec.
2. umyješ podlahu
3. nakrmíš králíky
str_extract_all(s2, "^\\d+\\.")
[[1]]
[1] "1."
str_extract_all(s2, regex("^\\d+\\.", multiline = TRUE))
[[1]]
[1] "1." "2." "3."

Zbývající dva parametry jsou méně důležité: parametr dotall způsobí, že . zahrne všechny znaky, včetně konce řádku \n. Parametr comments umožňuje přidávat do regulárního výrazu komentáře. Pěkný příklad ukazuje viněta k balíku stringr:

phone <- regex("
  \\(?     # optional opening parens
  (\\d{3}) # area code
  [)- ]?   # optional closing parens, dash, or space
  (\\d{3}) # another three numbers
  [ -]?    # optional space or dash
  (\\d{3}) # three more numbers
  ", comments = TRUE)

str_match("514-791-8141", phone)
     [,1]          [,2]  [,3]  [,4] 
[1,] "514-791-814" "514" "791" "814"
#>      [,1]          [,2]  [,3]  [,4] 
#> [1,] "514-791-814" "514" "791" "814"

(Komentář je také možné zadat přímo v regulárním výrazu pomocí speciální skupiny (?#...), kde ... označují text komentáře.)

Alternativou funkce regex() je funkce fixed(). Ta zajistí, že se výraz bere doslovně. Řekněme, že chceme spočítat počet souvětí v řetězci a že počet souvětí aproximujeme počtem teček (naše souvětí vždy končí tečkou, ne vykřičníkem nebo otazníkem, a text neobsahuje žádné zkratky, pořadová čísla ani tečky nevyužívá nijak jinak než na konci vět). Abychom našli všechny výskyty tečky v řetězci, můžeme ji buď zbavit speciálního významu pomocí zpětného lomítka, nebo použít funkci fixed():

s3 <- "Máma má máso. Ema má mísu. Ó my se máme."
str_count(s3, "\\.")
[1] 3
str_count(s3, fixed("."))
[1] 3

Podobně jako regex() je i ve funkci fixed() možné vypnout rozdíl mezi malými a velkými písmeny pomocí parametru ignore_case.

Funkce fixed() porovnává řetězec doslovně, takže znaky bere doslovně jako byty, což nemusí vždy správně fungovat pro znaky, které nejsou součástí kódu ASCII (zhruba pro znaky, které nejsou součástí anglické abecedy). Pokud chceme takové znaky brát doslovně, je potřeba použít funkci cols(), které je navíc možné nastavit locale jazyka (implicitní jazyk je angličtina).

Poslední funkce pro modifikaci chování regulárních výrazů je funkce boundary(), která umožňuje nastavit okraje regulárního výrazu. Platné volby jsou “character”, “line_break”, “sentence” a “word”). Nejužitečnější je  boundary("word"), které hledá hranice mezi slovy:

s4 <- "Svítilo zářilo zlaté slunce na pobřeží na lagunce."
str_split(s4, boundary("word"))
[[1]]
[1] "Svítilo" "zářilo"  "zlaté"   "slunce"  "na"      "pobřeží" "na"     
[8] "lagunce"
str_extract_all(s4, boundary("word"))
[[1]]
[1] "Svítilo" "zářilo"  "zlaté"   "slunce"  "na"      "pobřeží" "na"     
[8] "lagunce"

  1. Rozsah \w byl historicky definován jako [a-zA-z0-9_], tj. neobsahoval znaky s diakritikou. Zdá se však, že nyní je zahrnuje.↩︎