Le espressioni regolari sono un modo estremamente versatile per fare pattern matching sulle stringhe, usando un linguaggio specifico del dominio (DSL) progettato proprio per questo scopo.
Come molti altri linguaggi di programmazione, R non cerca di implementare una propria libreria di espressioni regolari. Si limita invece a usare il motore di espressioni regolari ICU. ICU è simile alla popolare libreria PCRE2 usata dalla maggior parte dei linguaggi più recenti, anche se alcuni casi limite vengono gestiti diversamente.
Molte funzionalità delle espressioni regolari sono integrate in Base R, e potresti vedere esempi in codice più vecchio.
Per il codice nuovo, è fortemente consigliato usare la libreria stringr.
Come notato nel concetto Strings, stringr ha una buona documentazione, di solito scritta dall'autore o dagli autori del pacchetto.
Questo sillabo di R presuppone che tu abbia già familiarità con la sintassi di base delle espressioni regolari. Ci concentreremo esclusivamente sulle funzionalità specifiche di R.
Qui di seguito sono elencate alcune risorse per rinfrescare le tue conoscenze sulle espressioni regolari.
Molte delle funzioni già descritte nel concetto Strings sono progettate per le ricerche con espressioni regolari.
Abbiamo mantenuto il concetto Strings relativamente semplice, facendo corrispondere solo stringhe letterali, ma al suo posto si può usare qualsiasi espressione regolare.
Formalmente, in R un'espressione regolare si crea chiamando regex().
Per comodità, le funzioni di stringr trattano qualsiasi stringa di pattern come un'espressione regolare.
Nell'esempio qui sotto, la prima str_replace_all() è zucchero sintattico per la seconda versione:
input <- "Some input string"
# "\\s" matches any whitespace character
input |> str_replace_all("\\s", "_")
[1] "Some_input_string"
input |> str_replace_all(regex("\\s"), "_")
[1] "Some_input_string"
Il cheatsheet è una guida utile: tutte le funzioni con un argomento pattern (stampato in rosso) accettano un'espressione regolare, e la pagina 2 riassume la sintassi delle espressioni regolari.
Nota che la funzione regex() può comunque essere importante se devi sovrascrivere i valori predefiniti, come la distinzione tra maiuscole e minuscole o le impostazioni locali.
# default is case sensitive
"A Mixed Assortment" |>
str_replace_all(regex("[aeiou]", ignore_case = TRUE), "*")
#> [1] "* M*x*d *ss*rtm*nt"
Molti codici delle espressioni regolari includono una barra rovesciata, come il \s usato sopra.
Nelle stringhe R normali, per fare l'escape di \ bisogna raddoppiarlo, da cui "\\s".
Le barre rovesciate possono moltiplicarsi e diventare confuse, quindi nei casi complessi può essere utile usare le stringhe raw.
Per impostazione predefinita, la sintassi per definire una stringa raw è r"( )", ma sono ammessi altri delimitatori.
Dato che le parentesi tonde ( ) e le parentesi quadre [ ] sono entrambe comuni nella sintassi delle espressioni regolari (per i gruppi di cattura e i gruppi di opzioni), le parentesi graffe { } potrebbero essere una buona scelta.
input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"
Ecco alcune domande e le funzioni che possono aiutarti a rispondere.
Alcuni esempi usano fruit, un vettore di 80 nomi di frutta incluso in stringr per fare pratica.
Una stringa contiene una corrispondenza?
str_detect(), oppure str_starts(), str_ends() per evitare di usare le ancore ^ o $.
# berries begining with b
fruit |> str_detect("^b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE
# alternative syntax
fruit |> str_starts("b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE
Restituire solo le stringhe che contengono una corrispondenza?
Usa str_subset() per ottenere le stringhe, str_which() per ottenere gli indici.
fruit |> str_subset("a...e")
[1] "apple" "blackberry" "mandarine" "nectarine" "pineapple" "pomegranate" "raspberry" "salal berry"
> fruit |> str_which("a...e")
[1] 1 7 48 51 62 64 70 73
Quante corrispondenze ci sono in ogni stringa?
str_count()
# count vowels
fruit |> str_count("[aeiou]") |> head(10)
#> [1] 2 3 4 3 3 2 2 3 5 3
Dov'è la corrispondenza?
str_view() per un uso interattivo, str_locate() o str_locate_all() in generale.
# str_view only shows matching elements, with match in < > delimiters
fruit |> str_view("a...e")
[1] │ <apple>
[7] │ bl<ackbe>rry
[48] │ mand<arine>
[51] │ nect<arine>
[62] │ pine<apple>
[64] │ pomegr<anate>
[70] │ r<aspbe>rry
[73] │ sal<al be>rry
# str_locate returns all elements
fruit |> str_locate("a...e") |> head(10)
start end
[1,] 1 5
[2,] NA NA
[3,] NA NA
[4,] NA NA
[5,] NA NA
[6,] NA NA
[7,] 3 7
[8,] NA NA
[9,] NA NA
[10,] NA NA
Spesso vogliamo sapere cosa corrisponde. Si ottiene includendo gruppi di cattura tra parentesi all'interno dell'espressione regolare.
È possibile fare riferimento alle catture all'interno della stessa espressione regolare, usando \1, \2, ecc.
Per esempio, per trovare coppie di lettere ripetute:
fruit |> str_subset("(..)\\1")
[1] "banana" "coconut" "cucumber" "jujube" "papaya" "salal berry"
Con una sintassi simile, le catture si possono usare nelle stringhe di sostituzione:
# double to triple
fruit |>
str_subset("(..)\\1") |>
str_replace("(..)\\1", "\\1\\1\\1")
[1] "bananana" "cococonut" "cucucumber" "jujujube" "papapaya" "salalal berry"
Raccogli le corrispondenze per elaborarle in seguito usando la funzione str_match() per la sola prima corrispondenza, oppure str_match_all() per tutte le corrispondenze.
I gruppi di cattura tendono a produrre un output complicato in qualsiasi linguaggio.
L'approccio di R (che è anche vettorizzato, come sfida aggiuntiva) consiste nel generare una matrice da str_match(), con una riga per ogni stringa di input, la colonna 1 che contiene l'intera stringa corrispondente e le colonne restanti che contengono i vari gruppi di cattura.
Usare gruppi con nome può essere un vantaggio, perché diventano i nomi delle colonne nella matrice dei risultati.
recipe <- "dissolve 25g sugar in 200ml water"
recipe |> str_match("(\\d+g) .* (\\d+ml)")
[,1] [,2] [,3]
[1,] "25g sugar in 200ml" "25g" "200ml"
# use named capture groups
recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
wt vol
[1,] "25g sugar in 200ml" "25g" "200ml"
Non abbiamo detto molto sulle matrici in questo sillabo, anche se c'è un documento del concetto che puoi consultare. Le matrici non si integrano bene con l'ecosistema Tidyverse, nonostante facciano parte di Base R da molto tempo.
In breve: tratta le matrici come vettori con 2 indici, nell'ordine [rows, cols].
Lascia vuoto uno dei due indici per ottenere l'intera riga o colonna.
Quando prendi una singola riga r con m[r,] o una singola colonna c con m[, c], il valore restituito viene appiattito in un vettore per impostazione predefinita.
> amounts <- recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
# get all of row 1, as a named vector
> amt1 <- amounts[1,]
> amt1
wt vol
"25g sugar in 200ml" "25g" "200ml"
# get a vector element
amt1["wt"] |> unname()
[1] "25g"
Inevitabilmente, str_match_all() produce un output con una struttura ancora più complessa:
[[1]], come descritto nel concetto Lists.match().mix <- "Mix 200g of sugar, 180g of flour and 150g of butter"
# raw output
> str_match_all(mix, "(\\d+)g")
[[1]]
[,1] [,2]
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"
# we just want the first list element
> str_match_all(mix, "(\\d+)g")[[1]]
[,1] [,2]
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"
# captures are in column 2, so get that as a vector
> str_match_all(mix, "(\\d+)g")[[1]][,2]
[1] "200" "180" "150"
Naturalmente, le corrispondenze possono fallire.
In quel caso il risultato sarà il valore speciale NA.
Preparati a verificarlo, come descritto nel concetto Nothingness.
# failed match
recipe |> str_match("(not here)")
[,1] [,2]
[1,] NA NA
R for Data Science osserva:
str_match()restituisce una matrice, quindi non è particolarmente facile da usare.
Con la nota a piè di pagina:
Soprattutto perché in questo libro non parliamo mai di matrici!
Uno degli autori del libro ha anche scritto la libreria stringr e sta implicitamente criticando il proprio lavoro.
Sarà interessante vedere come si evolverà l'implementazione di questa funzione nelle versioni future.