Parcours
/
R
R
/
Programme
/
Expressions régulières
Ex

Expressions régulières en R

1 exercice

À propos de Expressions régulières

Les expressions régulières sont un moyen très polyvalent de faire de la recherche de motifs dans des strings, à l'aide d'un langage dédié (DSL) conçu pour cela.

Advanced

Comme beaucoup d'autres langages de programmation, R ne cherche pas à implémenter sa propre bibliothèque de regex. Il s'appuie plutôt sur le moteur d'expressions régulières ICU. ICU est semblable à la populaire bibliothèque PCRE2 utilisée par la plupart des langages récents, bien que certains cas limites y soient traités différemment.

De nombreuses fonctionnalités de regex sont intégrées à Base R, et tu peux en voir des exemples dans du code plus ancien.

Pour du nouveau code, il est vivement recommandé d'utiliser la bibliothèque stringr.

Comme indiqué dans le concept Strings, stringr dispose d'une bonne documentation, généralement écrite par les auteurs du paquet.

Note

Ce programme du cours R part du principe que tu connais déjà la syntaxe de base des regex. On se concentrera uniquement sur les fonctionnalités propres à R.

Voici quelques ressources pour rafraîchir tes connaissances en expressions régulières.

Beaucoup des fonctions déjà abordées dans le concept Strings sont conçues pour effectuer des recherches par regex par défaut.

On a gardé le concept Strings relativement simple en ne cherchant que sur des littéraux de chaîne, mais n'importe quel motif de regex peut être utilisé à la place.

Formellement, une regex est créée en R en appelant regex(). Par commodité, les fonctions de stringr traitent toute string fournie comme motif comme une regex. Dans l'exemple ci-dessous, le premier str_replace_all() est du sucre syntaxique pour la seconde version :

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"

Le mémento est un guide utile : toutes les fonctions qui prennent un argument pattern (imprimé en rouge) acceptent une regex, et la page 2 résume la syntaxe des regex.

Note que la fonction regex() peut tout de même être importante si tu as besoin de modifier les valeurs par défaut, comme la sensibilité à la casse ou la locale.

# default is case sensitive
"A Mixed Assortment" |> 
    str_replace_all(regex("[aeiou]", ignore_case = TRUE), "*")
#> [1] "* M*x*d *ss*rtm*nt"

De nombreux codes de regex comportent une barre oblique inversée, comme le \s utilisé plus haut. Dans les strings R normales, il faut échapper \ en le doublant, d'où "\\s".

Les barres obliques inversées peuvent se multiplier et devenir confuses, il peut donc être utile d'utiliser des chaînes brutes dans les cas complexes.

Par défaut, la syntaxe pour définir une chaîne brute est r"( )", mais d'autres délimiteurs sont autorisés. Comme les parenthèses ( ) et les crochets [ ] sont tous deux courants dans la syntaxe des regex (pour les groupes de capture et les groupes d'options), les accolades { } peuvent être un bon choix.

input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"

Détecter les correspondances

Voici quelques questions, et les fonctions qui peuvent aider à y répondre.

Certains exemples utilisent fruit, un vecteur de 80 noms de fruits fourni avec stringr pour s'entraîner.

Une string contient-elle une correspondance ? str_detect(), ou str_starts(), str_ends() pour éviter d'utiliser les ancres ^ ou $.

# 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

Renvoyer uniquement les strings qui contiennent une correspondance ? Utilise str_subset() pour obtenir les strings, str_which() pour obtenir les indices.

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

Combien de correspondances dans chaque string ? str_count()

# count vowels
fruit |> str_count("[aeiou]") |> head(10)
#> [1] 2 3 4 3 3 2 2 3 5 3

Où se trouve la correspondance ? str_view() pour un usage interactif, str_locate() ou str_locate_all() de façon plus générale.

# 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

Captures

On veut souvent savoir ce qui correspond. Pour cela, on inclut des groupes de capture entre parenthèses dans la regex.

Il est possible de faire référence à des captures au sein d'une même regex, à l'aide de \1, \2, etc. Par exemple, pour trouver des paires de lettres répétées :

fruit |> str_subset("(..)\\1")
[1] "banana"      "coconut"     "cucumber"    "jujube"      "papaya"      "salal berry"

Avec une syntaxe similaire, les captures peuvent être utilisées dans les chaînes de remplacement :

# double to triple
fruit |> 
  str_subset("(..)\\1") |> 
  str_replace("(..)\\1", "\\1\\1\\1")
[1] "bananana"      "cococonut"     "cucucumber"    "jujujube"      "papapaya"      "salalal berry"

Récupère les correspondances pour les traiter plus tard avec la fonction str_match() pour la première correspondance uniquement, ou str_match_all() pour toutes les correspondances.

Les groupes de capture ont tendance à produire une sortie compliquée dans n'importe quel langage. L'approche de R (qui est en plus vectorisée, ce qui ajoute une difficulté) consiste à générer une matrice à partir de str_match(), avec une ligne par string d'entrée, la colonne 1 contenant la string correspondante complète, et les colonnes restantes contenant les différents groupes de capture.

Utiliser des groupes nommés peut être un avantage, car ils deviennent les noms des colonnes de la matrice de résultats.

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"

On n'a pas beaucoup parlé des matrices dans ce programme du cours, mais il existe un document de concept que tu peux consulter. Les matrices s'intègrent mal à l'écosystème Tidyverse, même si elles font partie de Base R depuis longtemps.

En résumé rapide : considère les matrices comme des vecteurs à 2 indices, dans l'ordre [rows, cols]. Laisse l'un ou l'autre des indices vide pour obtenir la ligne ou la colonne entière.

Quand on récupère une seule ligne r avec m[r,] ou une seule colonne c avec m[, c], la valeur renvoyée est aplatie en un vecteur par défaut.

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

Inévitablement, str_match_all() produit une sortie encore plus structurée :

  • Le résultat est une liste, avec un élément par string d'entrée.
  • S'il n'y a qu'une seule string d'entrée, aplatis la sortie avec l'indexation [[1]] comme décrit dans le concept Lists.
  • Chaque élément est une matrice, avec des colonnes comme pour match().
  • Chaque ligne de la matrice correspond à une capture réussie.
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"

Bien sûr, les correspondances peuvent échouer. Le résultat est alors la valeur spéciale NA. Sois prêt à tester ce cas, comme décrit dans le concept Néant.

# failed match
recipe |> str_match("(not here)")
     [,1] [,2]
[1,] NA   NA  
Note

R for Data science fait remarquer :

str_match() renvoie une matrice, ce qui la rend peu pratique à utiliser.

Avec la note de bas de page :

Surtout parce qu'on ne parle jamais de matrices dans ce livre !

L'un des auteurs du livre a aussi écrit la bibliothèque stringr, et critique implicitement son propre travail.

Il sera intéressant de voir comment l'implémentation de cette fonction évolue dans les versions futures.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet

Apprends Expressions régulières