La manipulation de texte dans R

Charles Martin

Mars 2023

Librairies nécessaires

Cet atelier nécessitera des versions récentes des librairies readr (version 2.1 minimum), stringr (version 1.5 minimum), tidyr (version 1.3 minimum) et dplyr. Vous pouvez soit les activer individuellement, ou activer la méta-librairie tidyverse :

library(tidyverse)
── Attaching packages ─────────────────────────────────────── tidyverse 1.3.2 ──
✔ ggplot2 3.4.0      ✔ purrr   1.0.1 
✔ tibble  3.1.8      ✔ dplyr   1.0.10
✔ tidyr   1.3.0      ✔ stringr 1.5.0 
✔ readr   2.1.2      ✔ forcats 0.5.2 
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()

Comment se construit le texte dans R

Avant de se lancer dans la manipulation de texte comme tel, il importe de bien comprendre la nature du texte dans R et comment le construire.

Comment le texte est encodé dans l’ordinateur

Dans la mémoire de R, dans les fichiers CSV, etc. le texte est conservé dans une série de codes hexadécimaux On peut d’ailleurs voir les codes correspondant avec la fonction charToRaw :

charToRaw("Charles")
[1] 43 68 61 72 6c 65 73

Le “C” est encodé avec 43, le “h” par 68, etc.

Si on fait la mathématique de la chose, on réalise rapidement que ce système ne peut représenter que 16*16 = 256 symboles. Cela fonctionnait bien à une époque où les américains avaient la main mise sur l’informatique, mais ne fonctionne pas du tout pour conserver du texte provenant de n’importe quelle langue dans le monde.

Dans les années 1980-1990, une série de standards ont été développés pour permettre l’encodage de caractères autres que l’anglais. Entre autres, le ISO-8859-1 (Latin1) permettait l’encodage de la plupart des caractères utilisés en Europe de l’ouest, Latin2 en Europe de l’est, etc.

Aujourd’hui, il existe un standard international, nommé UTF-8, qui permet d’encoder l’ensemble des caractères imaginables, incluant même les Emoji.

La plupart des logiciels modernes utilisent cet encodage et l’utilisation d’accents dans les fichiers n’est plus un problème. Cependant, certaines applications plus vieilles et certains fichiers produits il y a un certain temps d’adhèrent pas nécessairement à cette convention.

C’est pour cette raison que parfois, en chargeant un fichier CSV, vous verrez une série de caractères bizarres dans vos données :

read_csv("Latin1.csv")
Rows: 2 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (1): Col3
dbl (2): Col1, Col2

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 2 × 3
   Col1  Col2 Col3            
  <dbl> <dbl> <chr>           
1     1     2 "All\xf4"       
2     3     4 "\xc0 la place?"

Avec essai/erreur, on peut tenter de deviner le bon encodage et le spécifier au moment du chargement :

read_csv("Latin1.csv",locale = locale(encoding = "Windows-1252"))
Rows: 2 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (1): Col3
dbl (2): Col1, Col2

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 2 × 3
   Col1  Col2 Col3       
  <dbl> <dbl> <chr>      
1     1     2 Allô       
2     3     4 À la place?
read_csv("Latin1.csv",locale = locale(encoding = "Latin1"))
Rows: 2 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (1): Col3
dbl (2): Col1, Col2

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 2 × 3
   Col1  Col2 Col3       
  <dbl> <dbl> <chr>      
1     1     2 Allô       
2     3     4 À la place?

Habituellement, si vous essayez Latin1, UTF-8 ou Windows-1252, vous êtes presque certaines d’être tombée sur le bon.

Les versions récentes de la librairie readr sont maintenant équipées d’une fonction vous permettant de laisser l’ordinateur faire ce sale boulot pour vous :

guess_encoding("Latin1.csv")
# A tibble: 2 × 2
  encoding   confidence
  <chr>           <dbl>
1 ISO-8859-1       0.62
2 ISO-8859-2       0.41

Création manuelle de texte

Maintenant, comment peut-on faire pour créer du texte dans R?

La façon la plus simple est de créer un objet chaîne de caractères, comme ceci :

chaine1 <- "Il faut l'essayer"
chaine2 <- 'Voici un autre "essai"'

Remarquez que l’on peut utiliser le guillemet simple ou double pour démarrer et terminer notre séquence.

En général, il est recommandé d’utiliser le guillemet double, sauf si votre chaîne de caractères en contient plusieurs.

On peut aussi inclure un guillemet double dans une séquence démarrée par un guillemet double, en utilisant un caractère échappement (escape character), soit le backslash (\) :

chaine3 <- "Je contient un \" et ça fonctionne tout de même"

Par conséquent, si vous voulez produire un backslash dans une chaîne de caractères, vous devrez le précéder d’un autre backslash :

backslash <- "\\"

Remarquez que, si vous envoyez une de ces chaînes de caractère à la console, on verra le caractère d’échappement :

chaine3
[1] "Je contient un \" et ça fonctionne tout de même"
backslash
[1] "\\"

Cela survient parce que, par défaut, la fonction print de R (appelée implicitement) nous fournit non pas ce qu’il voit comme texte, mais ce que l’on devrait taper pour le reconstruire.

Si on veut voir la vraie représentation du texte (comme on la verra dans les graphiques, etc.), on peut utiliser la fonction str_view. Comparez ces deux sorties :

print(c(chaine1, chaine2, chaine3))
[1] "Il faut l'essayer"                              
[2] "Voici un autre \"essai\""                       
[3] "Je contient un \" et ça fonctionne tout de même"
str_view(c(chaine1, chaine2, chaine3))
[1] │ Il faut l'essayer
[2] │ Voici un autre "essai"
[3] │ Je contient un " et ça fonctionne tout de même

Il est aussi possible de créer des chaînes de caractères que l’on dit “brutes”, pour lesquelles, au moment de leur création, R n’essaie pas de gérer les caractères d’échappement. Pour cela, il faut débuter notre chaîne par r"( et la terminer par )"

complexe <- r"(L'apostrophe, le \ et même les "guillemets" ne posent plus de problèmes)"
str_view(complexe)
[1] │ L'apostrophe, le \ et même les "guillemets" ne posent plus de problèmes

On peut remplacer au besoin r"()" par r"[]", r"{}", etc.

Caractères d’échappement

Outre \" \' et \\, il existe une série d’autres caractères spéciaux lorsque l’on construit du texte dans R.

Entre autres :

str_view(c(
  "Saut\nde\nligne",
  "avec\tindentation",
  "\u00b5 mu",
  "\U0001f4a9 (sans commentaires)"
))
[1] │ Saut
    │ de
    │ ligne
[2] │ avec{\t}indentation
[3] │ µ mu
[4] │ 💩 (sans commentaires)

Vous pouvez taper ?'"' à la console pour plus de détails sur les possibilités des caractères d’échappement.

Création de texte par programmation

Pour créer et combiner du texte par programmation, il existe trois fonctions dans le tidyverse, soit str_c et str_glue pour travailler directement sur les objets (mutate, etc.) et str_flatten pour les cas où on tente de résumer du texte.

La fonction str_c fonctionne comme la fonction c, mais pour coller des morceaux de texte dans une chaîne, plutôt que coller des nombres dans un vecteur.

str_c("a","b","c")
[1] "abc"
str_c("Salut ",c("Charles","Vincent"))
[1] "Salut Charles" "Salut Vincent"

Son fonctionnement est très similaire à la fonction paste0, mais sa gestion des valeurs manquantes et des vecteurs de différentes longueurs (voir exemple précédent) est plus constante avec l’ensemble des autres fonctions du tidyverse.

Chaque élément passé à str_c peut évidemment être un objet contenant du texte, plutôt que le texte directement :

nom <- "Charles"
moment <- "aujourd'hui"
str_c("Bonjour ", nom, "! Comment allez-vous ", moment, "?")
[1] "Bonjour Charles! Comment allez-vous aujourd'hui?"

Bien que pratique, cette approche peut devenir fastidieuse si on a plusieurs morceaux de texte à rassembler et qu’il faut chaque fois fermer le guillemet, ajouter la virgule, ouvrir le guillemet, etc. sans rien oublier.

C’est là où entre en scène la fonction str_glue :

str_glue("Bonjour {nom}, comment allez-vous {moment}?")
Bonjour Charles, comment allez-vous aujourd'hui?

R remplacera automatiquement chaque mot entouré d’accolades par le contenu de la variable du même nom.

noms <- c("Pierre","Paul","Jacques")
str_glue("Bonjour {noms}, comment allez-vous {moment}?")
Bonjour Pierre, comment allez-vous aujourd'hui?
Bonjour Paul, comment allez-vous aujourd'hui?
Bonjour Jacques, comment allez-vous aujourd'hui?

Comme vous le constatez peut-être, str_c et str_glue fonctionneront très bien dans des contextes comme un mutate oû la fonction doit produire une série de chaînes de caractères en réponse à un vecteur.

Si notre opération doit impérativement retourner une seule chaîne, alors il faut utiliser la fonction str_flatten :

str_flatten(noms, ", ")
[1] "Pierre, Paul, Jacques"

On peut aussi contrôler le dernier séparateur pour faire de belles énumérations :

str_flatten(noms,", ",last = " et ")
[1] "Pierre, Paul et Jacques"
habitats <- tribble(
  ~Espece,~Habitat,
  "A","désert",
  "A","forêt",
  "B", "forêt",
  "A", "tundra"
)
habitats %>% 
  group_by(Espece) %>% 
  summarize(
    Nombre = n(),
    Liste = str_flatten(Habitat,", ")
  )
# A tibble: 2 × 3
  Espece Nombre Liste                
  <chr>   <int> <chr>                
1 A           3 désert, forêt, tundra
2 B           1 forêt                

L’extraction de texte

Une autre tâche extrêmement commune qui se produit dans nos analyses R est de devoir extraire de l’information textuelle à partir de variables déjà existantes.

La librairie tidyr fournit une série de fonctions pensées expressément pour ce genre de situation : la famille separate_, qui comprend 4 fonctions, soit :

Dans tous les cas, les fonctions vont analyser le texte contenu dans une variable, et le séparer en morceaux. Soit en vous créant une observation par morceau (_longer), ou soit en créant une colonne par morceau (_wider).

Le découpage des morceaux peut être basé soit sur un séparateur (_delim), ou soit basé sur la position du texte (_position)

Ces fonctions sont très pratiques, par exemple, quand vous avez encodé de l’information dans le nom des sites de votre expérience :

experience_a <- tibble(
  nom_site = c("CT01","CT02","CT03","TR01","TR02","TR03")
)
experience_a
# A tibble: 6 × 1
  nom_site
  <chr>   
1 CT01    
2 CT02    
3 CT03    
4 TR01    
5 TR02    
6 TR03    

Il suffit de fournir à la fonction le nombre de caractères de chacun des morceaux, avec le nom que doit contenir cette colonne au final.

experience_a %>% 
  separate_wider_position(nom_site,c(traitement = 2, no_replicat = 2) )
# A tibble: 6 × 2
  traitement no_replicat
  <chr>      <chr>      
1 CT         01         
2 CT         02         
3 CT         03         
4 TR         01         
5 TR         02         
6 TR         03         

Si nos informations avaient plutôt été sur ce format :

experience_b <- tibble(
  nom_site = c("CT-1","CT-10","R-1","R-100")
)
experience_b
# A tibble: 4 × 1
  nom_site
  <chr>   
1 CT-1    
2 CT-10   
3 R-1     
4 R-100   

on aurait pu les extraire en se basant sur la présence du séparateur, comme ceci :

experience_b %>% 
  separate_wider_delim(nom_site,delim = "-",names = c("traitement","no_replicat"))
# A tibble: 4 × 2
  traitement no_replicat
  <chr>      <chr>      
1 CT         1          
2 CT         10         
3 R          1          
4 R          100        

Enfin, il pourrait arriver que l’information de plusieurs observations soit encodée dans une même cellule :

experience_c <- tibble(
  site = c("A","B"),
  resultats_visites = c("0,0,1","1,0,1"),
  latitude = c(46,47),
  longitude = c(-72,-72.5)
)
experience_c
# A tibble: 2 × 4
  site  resultats_visites latitude longitude
  <chr> <chr>                <dbl>     <dbl>
1 A     0,0,1                   46     -72  
2 B     1,0,1                   47     -72.5

On peut alors utiliser une fonction _longer pour recréer chacune des observation :

experience_c %>% 
  separate_longer_delim(resultats_visites, delim = ",")
# A tibble: 6 × 4
  site  resultats_visites latitude longitude
  <chr> <chr>                <dbl>     <dbl>
1 A     0                       46     -72  
2 A     0                       46     -72  
3 A     1                       46     -72  
4 B     1                       47     -72.5
5 B     0                       47     -72.5
6 B     1                       47     -72.5

Pour des opérations plus simples, vous pouvez aussi aller chercher directement des bouts de texte avec la fonction str_sub.

Elle permet d’utiliser des numéros de position positifs quand on veut calculer à partir du début, et des numéros de position négatifs quand on veut calculer à partir de la fin :

str_sub("LongTexte",5,9)
[1] "Texte"
str_sub("LongTexte",-5, -1)
[1] "Texte"

Les expressions régulières (regex)

Maintenant que nous avons vu la plupart des fonctions permettant de traiter le texte par programmation, nous allons nous attaquer à une deuxième façon soit les expression régulières (regular expressions), communément appelées par leur acronyme anglais : regex!

Les regex sont des séquences de caractères permettant de définir un patron de recherche à utiliser, de façon hyper efficace.

Le prix à payer pour cette efficacité est que les regex sont parfois difficiles à lire et à déboguer. Certains parlent parfois de magie noire, à cause de leur côté un peu obscur et imprévisible. Mais bien maîtrisés, ils sont extrêmement puissants.

Exploration avec str_view

Pour faciliter notre apprentissage des regex, nous allons utiliser une fonction qui permet de les tester visuellement, soit la fonction str_view.

Pour illustrer nos exemples, nous utiliserons la colonne name de la base de données msleep, qui est founie avec la librairie ggplot2

head(msleep$name)
[1] "Cheetah"                    "Owl monkey"                
[3] "Mountain beaver"            "Greater short-tailed shrew"
[5] "Cow"                        "Three-toed sloth"          

Pour se simplifier la vie, nous allons par contre extraire tous les noms dans un nouveau vecteur, que nous allons aussi convertir en minuscules

noms <- msleep$name %>% str_to_lower()
head(noms)
[1] "cheetah"                    "owl monkey"                
[3] "mountain beaver"            "greater short-tailed shrew"
[5] "cow"                        "three-toed sloth"          

À la base, un regex est une séquence de caractères à chercher :

str_view(noms,"shrew")
 [4] │ greater short-tailed <shrew>
[17] │ lesser short-tailed <shrew>
[73] │ musk <shrew>
[79] │ tree <shrew>

La fonction str_view nous montre tous les éléments du vecteur de noms qui contenaient la séquence recherchée (“shrew”). Remarquez que dans les sorties, la partie qui correspondait à notre regex a été entourée de < > et, si vous regardez les sorties dans la console, elle sera aussi d’une autre couleur.

Remarquez que les regex sont sensibles à la casse (aux différences majuscules/minuscules). Le patron “SHREW” ne retourne par exemple aucun résultat :

str_view(noms, "SHREW")

La construction de séquences de recherche

Outre les séquences de caractères recherchés, un regex peut aussi contenir des méta-caractères permettant de contrôler plus précisément la recherche.

Le premier que nous verrons est le . qui, comme un joker, permet de remplacer n’importe quel caractère.

On peut l’utiliser par exemple pour trouver tous les noms contenant le mot gris en anglais, peut importe qu’il soit écrit gray ou grey :

str_view(noms,"gr.y")
[32] │ <gray> seal
[33] │ <gray> hyrax

Il existe ensuite trois méta-caractères permettant de choisir le nombre de fois que chacun des patrons recherché doit survenir:

Par exemple, tous les noms contenant un a suivi d’au moins un s

str_view(noms, "as+")
[21] │ <as>ian elephant
[26] │ pat<as> monkey
[47] │ northern gr<ass>hopper mouse
[59] │ c<as>pian seal
[67] │ e<as>tern american mole
[76] │ e<as>tern american chipmunk

Remarquez dans ce cas que notre patron contient le 2e s dans “grass hopper”

Avec le ?, la présence du caractère devient optionnelle. On peut attraper par exemple tous les noms contenant ham ou am, comme ceci :

str_view(noms,"h?am")
[20] │ north <am>erican opossum
[27] │ western <am>erican chipmunk
[40] │ golden <ham>ster
[67] │ eastern <am>erican mole
[76] │ eastern <am>erican chipmunk

Enfin, avec l’astérisque, on peut rendre un patron optionnel, et ce, même si il se répète. On peut par exemple chercher tous les noms contenant “oo”, ou avec n’importe quel nombre de p entre les deux o, comme ceci :

str_view(noms,"op*o")
[20] │ north american <opo>ssum
[35] │ mong<oo>se lemur
[37] │ thick-tailed <oppo>sum
[54] │ bab<oo>n
[61] │ potor<oo>

Les parenthèses carrées, quant à elles, permettent de définir une série d’alternatives pouvait être cherchées. On peut par exemple trouver tous les rats, les chats et les chauve-souris comme ceci :

str_view(noms,"[cbr]at")
[16] │ african giant pouched <rat>
[22] │ big brown <bat>
[28] │ domestic <cat>
[43] │ little brown <bat>
[44] │ round-tailed musk<rat>
[64] │ labo<rat>ory <rat>
[68] │ cotton <rat>
[69] │ mole <rat>

L’accent circonflexe permet d’inverser la condition de la parenthèse carrée. On pourrait par exemple chercher toutes les fois où “at” apparaît, mais que ce n’est pas un chat ou un rat :

str_view(noms,"[^cr]at")
 [4] │ gr<eat>er short-tailed shrew
[11] │ g<oat>
[22] │ big brown <bat>
[26] │ <pat>as monkey
[43] │ little brown <bat>

On peut aussi combiner les parenthèses carrées et les méta-caractères définissant les nombres d’apparitions. On pourrait par exemple chercher tous les mots contenant deux voyelles suivies d’une ou plusieurs consonnes, comme ceci :

str_view(noms,"[aeiou][aeiou][^aeiou]+")
 [1] │ ch<eet>ah
 [3] │ m<ount><ain b><eav>er
 [4] │ gr<eat>er short-t<ail>ed shrew
 [6] │ thr<ee-t><oed sl>oth
 [7] │ northern fur s<eal>
 [8] │ vesper m<ous>e
[10] │ r<oe d><eer>
[11] │ g<oat>
[12] │ g<uin><ea p>ig
[16] │ african g<iant p><ouch>ed rat
[17] │ lesser short-t<ail>ed shrew
[19] │ tr<ee hyr>ax
[21] │ as<ian >elephant
[25] │ <eur>op<ean h>edgehog
[32] │ gray s<eal>
[35] │ mong<oos>e lemur
[37] │ thick-t<ail>ed opposum
[39] │ mongol<ian g>erbil
[42] │ h<ous>e m<ous>e
[44] │ r<ound-t><ail>ed muskrat
... and 18 more

Remarquez que dans plusieurs instances, notre patron a été trouvé à plusieurs reprises. Remarquez aussi que notre définition de “consonne” n’est pas tout à fait juste. Comme les espaces ne sont pas des voyelles, ils sont aussi capturés par notre patron de recherche.

Un autre caractère spécial particulièrement utile est la barre verticale (|). Ce dernier permet de définir des séquences alternatives (un OU). Par exemple, pour trouver tous les écureuils et les tamias, on pourrait écrire :

str_view(noms,"squirrel|chipmunk")
[27] │ western american <chipmunk>
[66] │ <squirrel> monkey
[70] │ arctic ground <squirrel>
[71] │ thirteen-lined ground <squirrel>
[72] │ golden-mantled ground <squirrel>
[76] │ eastern american <chipmunk>

Remarquez que notre séquence a aussi détecté le Singe-écureuil. Nous verrons plus loin comment remédier à ce problème.

Fonctions basées sur les regex

Maintenant que nous avons vu la mécanique de base, nous allons faire un petit survol des fonctions permettant l’utilisation des regex.

Pour bien illustrer ces fonctions, nous allons nous créer un petit tableau de données contenant les noms des animaux en minuscule (comme nous avons utilisé dans la précédente section) combiné à une autre colonne d’information pour illustrer l’application sur des tableaux.

tableau <- tibble(
  noms = noms,
  sommeil = msleep$sleep_total
)

L’application la plus pratique sera sans aucun doute de combiner le filter de dplyr avec la fonction str_detect.

tableau %>% 
  filter(str_detect(noms, "squirrel|chipmunk"))
# A tibble: 6 × 2
  noms                           sommeil
  <chr>                            <dbl>
1 western american chipmunk         14.9
2 squirrel monkey                    9.6
3 arctic ground squirrel            16.6
4 thirteen-lined ground squirrel    13.8
5 golden-mantled ground squirrel    15.9
6 eastern american chipmunk         15.8

Une autre chose que l’on voudra souvent faire est de remplacer le patron trouvé par autre chose. Par exemple, si finalement les experts décidaient que tous les tamias et les écureuils s’appelaient désormais des squimunk, on pourrait faire ceci :

tableau %>% 
  mutate(
    noms = str_replace(noms,"squirrel|chipmunk","squimunk")
  ) %>% slice(70:80)
# A tibble: 11 × 2
   noms                           sommeil
   <chr>                            <dbl>
 1 arctic ground squimunk            16.6
 2 thirteen-lined ground squimunk    13.8
 3 golden-mantled ground squimunk    15.9
 4 musk shrew                        12.8
 5 pig                                9.1
 6 short-nosed echidna                8.6
 7 eastern american squimunk         15.8
 8 brazilian tapir                    4.4
 9 tenrec                            15.6
10 tree shrew                         8.9
11 bottle-nosed dolphin               5.2

Remarquez que j’utilise la fonction slice pour vous montrer les lignes où notre remplacement a été fait.

À l’extrême, on pourrait décider de remplacer tous les “e” par une autre lettre :

tableau %>% 
  mutate(
    noms = str_replace(noms, "e","X")
  )
# A tibble: 83 × 2
   noms                       sommeil
   <chr>                        <dbl>
 1 chXetah                       12.1
 2 owl monkXy                    17  
 3 mountain bXaver               14.4
 4 grXater short-tailed shrew    14.9
 5 cow                            4  
 6 thrXe-toed sloth              14.4
 7 northXrn fur seal              8.7
 8 vXsper mouse                   7  
 9 dog                           10.1
10 roX deer                       3  
# … with 73 more rows

Comme vous le constatez, str_replace ne change que la première instance trouvée. Pour remplacer toutes les instances, il faut passer par la fonction str_replace_all :

tableau %>% 
  mutate(
    noms = str_replace_all(noms, "e","X")
  )
# A tibble: 83 × 2
   noms                       sommeil
   <chr>                        <dbl>
 1 chXXtah                       12.1
 2 owl monkXy                    17  
 3 mountain bXavXr               14.4
 4 grXatXr short-tailXd shrXw    14.9
 5 cow                            4  
 6 thrXX-toXd sloth              14.4
 7 northXrn fur sXal              8.7
 8 vXspXr mousX                   7  
 9 dog                           10.1
10 roX dXXr                       3  
# … with 73 more rows

Si on laisse le deuxième argument vide (“”) dans les str_replace, on demande essentiellement à R d’enlever cette séquence. Il peut parfois être plus élégant et lisible dans ces cas de simplement utiliser la fonction str_remove (ou str_remove_all). On pourrait éliminer tous les e de nos données comme ceci :

tableau %>% 
  mutate(
    noms = str_remove_all(noms, "e")
  )
# A tibble: 83 × 2
   noms                   sommeil
   <chr>                    <dbl>
 1 chtah                     12.1
 2 owl monky                 17  
 3 mountain bavr             14.4
 4 gratr short-taild shrw    14.9
 5 cow                        4  
 6 thr-tod sloth             14.4
 7 northrn fur sal            8.7
 8 vspr mous                  7  
 9 dog                       10.1
10 ro dr                      3  
# … with 73 more rows

Il existe des dizaines d’autres fonctions permettant de travailler avec des regex, entre autres str_subset, str_which et str_countque je vous encourage fortement à explorer par vous-même

Caractères d’échappement (encore!)

Bon, alors, si j’avais à vous demander d’écrire un regex pour détecter un point (.), vous feriez quoi?

texte <- "Première phrase. Deuxième phrase."
str_view(texte,".")
[1] │ <P><r><e><m><i><è><r><e>< ><p><h><r><a><s><e><.>< ><D><e><u><x><i><è><m><e>< ><p><h><r><a><s><e><.>

Évidemment non. Un point est un joker, qui peut remplacer n’importe quel caractère.

Peut-être avec un caractère d’échappement?

str_view(texte,"\.")
Error: '\.' is an unrecognized escape in character string starting ""\."

Ah non, ça ne marche pas parce que, au moment d’écrire la chaîne de caractères, R remplace les \-quelquechose en les encodant, et \., ça ne veut rien dire pour lui.

Alors, oui, il faut utiliser \\. pour détecter un simple petit point!

str_view(texte,"\\.")
[1] │ Première phrase<.> Deuxième phrase<.>

Un backslash au niveau de R et un backslash au niveau de l’interprétation du regex.

Maintenant, si on se prépare une chaîne de caractère avec un backslash dedans :

bs <- "a\\b"
str_view(bs)
[1] │ a\b

Comment on fait pour le retrouver avec un regex?

Eh oui, ça nous prendra 4 backslash pour en trouver un seul.

str_view(bs, "\\\\")
[1] │ a<\>b

Au moment de créer la chaîne de caractères dans R, on en perd 2 à l’encodage, puis au niveau du regex, on en perd un dernier!

Si on veut se simplifier un peu la vie, on peut se rappeller que R nous permet de créer des chaînes de caractères brutes, qui ne passent pas par l’étape d’encodage :

bs2 <- r"(a\n)"
str_view(bs2, r"(\\)")
[1] │ a<\>n

Ce n’est pas idéal, mais ça élimine au moins un niveau d’abstraction.

Caractères spéciaux

Tout comme les regex comprennent leur propre syntaxe, ils comprennent aussi une série de caractères spéciaux qui leur sont propres.

Voyons tout d’abord les caractères d’ancrage. Les regex comprennent deux caractères d’ancrage soit ^ et $ désignant respectivement le début et la fin d’une chaîne de caractères

On peut par exemple utiliser un ancrage pour trouver tous les noms de mammifères qui se terminent par squirrel, et ainsi éliminer de nos résultats le singe-écureuil :

str_view(noms, "squirrel")
[66] │ <squirrel> monkey
[70] │ arctic ground <squirrel>
[71] │ thirteen-lined ground <squirrel>
[72] │ golden-mantled ground <squirrel>

vs.

str_view(noms, "squirrel$")
[70] │ arctic ground <squirrel>
[71] │ thirteen-lined ground <squirrel>
[72] │ golden-mantled ground <squirrel>

Si on combine les deux caractères d’ancrage, on s’assure que la chaîne de caractères ne contient que le texte demandé. Rien de plus, rien de moins.

str_view(noms,"pig")
[12] │ guinea <pig>
[74] │ <pig>

vs.

str_view(noms, "^pig$")
[74] │ <pig>

Les regex comprennent aussi des caractères spéciaux nous évitant les longues énumérations. On peut par exemple utiliser \w pour trouver n’importe quelle lettre ou chiffre.

Ainsi, pour trouver tous les noms d’espèces composés de deux mots, on pourrait faire ceci :

str_view(noms,"^\\w+ \\w+$")
 [2] │ <owl monkey>
 [3] │ <mountain beaver>
 [8] │ <vesper mouse>
[10] │ <roe deer>
[12] │ <guinea pig>
[19] │ <tree hyrax>
[21] │ <asian elephant>
[25] │ <european hedgehog>
[26] │ <patas monkey>
[28] │ <domestic cat>
[31] │ <pilot whale>
[32] │ <gray seal>
[33] │ <gray hyrax>
[35] │ <mongoose lemur>
[36] │ <african elephant>
[39] │ <mongolian gerbil>
[40] │ <golden hamster>
[42] │ <house mouse>
[45] │ <slow loris>
[55] │ <desert hedgehog>
... and 14 more

Il existe aussi série d’autres caractères semblables, dont voici un aperçu :

Au delà des caractères +, ? et *, les regex nous permettent aussi un contrôle plus fin du nombre de répétition d’un patron, à l’aide des accolades. Pour trouver tous les noms commençant par un mots de 3 lettres suivi d’un espace, on pourrait faire ceci :

str_view(noms,"^\\w{3}\\s")
 [2] │ <owl >monkey
[10] │ <roe >deer
[22] │ <big >brown bat
[83] │ <red >fox

Remarquez qu’il existe aussi un caractère spécial permettant de détecter les frontières entourant un mot : \b.

Si on veut trouver tous les noms comprenant un mot d’exactement 3 lettres, on pourrait faire ceci :

str_view(noms, "\\b\\w{3}\\b")
 [2] │ <owl> monkey
 [5] │ <cow>
 [7] │ northern <fur> seal
 [9] │ <dog>
[10] │ <roe> deer
[12] │ guinea <pig>
[16] │ african giant pouched <rat>
[22] │ <big> brown <bat>
[28] │ domestic <cat>
[43] │ little brown <bat>
[64] │ laboratory <rat>
[68] │ cotton <rat>
[69] │ mole <rat>
[74] │ <pig>
[82] │ arctic <fox>
[83] │ <red> <fox>

Si on ne mentionne qu’un chiffre, les accolades cherchent un nombre exact de détections, mais on peut aussi spécifier deux valeurs, qui donneront les bornes du nombre de détection acceptable.

Par exemple, pour trouver tous les noms d’animaux comprenant un mot de 3 à 5 lettres, on écrirait ceci :

str_view(noms, "\\b\\w{3,5}\\b")
 [2] │ <owl> monkey
 [4] │ greater <short>-tailed <shrew>
 [5] │ <cow>
 [6] │ <three>-<toed> <sloth>
 [7] │ northern <fur> <seal>
 [8] │ vesper <mouse>
 [9] │ <dog>
[10] │ <roe> <deer>
[11] │ <goat>
[12] │ guinea <pig>
[15] │ <star>-<nosed> <mole>
[16] │ african <giant> pouched <rat>
[17] │ lesser <short>-tailed <shrew>
[18] │ <long>-<nosed> armadillo
[19] │ <tree> <hyrax>
[20] │ <north> american opossum
[21] │ <asian> elephant
[22] │ <big> <brown> <bat>
[23] │ <horse>
[26] │ <patas> monkey
... and 37 more

Priorité d’opération

Tout comme en mathématiques, l’évaluation d’un regex ne s’effectue pas nécessairement de gauche à droite. Il existe un ordre de priorité entre les opérations.

En règle générale, les caractères contrôlant le nombre de répétitions ont préséance sur ceux définissant des alternatives.

Par exemple ab+ sera interprété comme a(b+). ^a|b$ sera interprété comme (^a)|(b$), etc.

Contrairement à l’algèbre, ces priorités sont très difficiles à retenir, et le plus simple est probablement d’utiliser autant de parenthèses que nécessaire pour clarifier le patron recherché.

Regroupements et réutilisation

Outre la clarification des priorités, les parenthèses ont une autre utilisation particulière dans les regex : elles créent des groupes qui peuvent être réutilisés. Chacun des groupes définis par les parenthèses est numéroté automatiquement par R. Ces numéros peuvent ensuite être utilisés ailleurs dans le regex. Le premier groupe sera nommé \1, le deuxième \2, etc.

On peut par exemple trouver tous les animaux dont le nom comporte une double lettre, comme ceci :

str_view(noms,"(\\w)\\1")
 [1] │ ch<ee>tah
 [6] │ thr<ee>-toed sloth
[10] │ roe d<ee>r
[14] │ chinchi<ll>a
[17] │ le<ss>er short-tailed shrew
[18] │ long-nosed armadi<ll>o
[19] │ tr<ee> hyrax
[20] │ north american opo<ss>um
[30] │ gira<ff>e
[35] │ mong<oo>se lemur
[37] │ thick-tailed o<pp>osum
[43] │ li<tt>le brown bat
[47] │ northern gra<ss>ho<pp>er mouse
[48] │ ra<bb>it
[49] │ sh<ee>p
[50] │ chimpanz<ee>
[54] │ bab<oo>n
[56] │ po<tt>o
[57] │ d<ee>r mouse
[60] │ co<mm>on porpoise
... and 9 more

Le \w attrape une lettre, les parenthèses créent un groupe (ici le numéro 1), et ensuite un réutilise ce groupe pour créer le doublon.

Par le même principe, on peut aussi trouver tous les noms qui commencent et finissent par la même lettre :

str_view(noms,"^(\\w).*\\1$")
[10] │ <roe deer>
[12] │ <guinea pig>
[45] │ <slow loris>
[67] │ <eastern american mole>

La réutilisation de patrons est aussi applicable avec la fonction str_replace.

Elle peut permettre par exemple d’inverser les deux premiers mots de chaque nom :

str_replace(noms,"^(\\w+)\\s(\\w+)(.*)", "\\2 \\1\\3")
 [1] "cheetah"                        "monkey owl"                    
 [3] "beaver mountain"                "short greater-tailed shrew"    
 [5] "cow"                            "three-toed sloth"              
 [7] "fur northern seal"              "mouse vesper"                  
 [9] "dog"                            "deer roe"                      
[11] "goat"                           "pig guinea"                    
[13] "grivet"                         "chinchilla"                    
[15] "star-nosed mole"                "giant african pouched rat"     
[17] "short lesser-tailed shrew"      "long-nosed armadillo"          
[19] "hyrax tree"                     "american north opossum"        
[21] "elephant asian"                 "brown big bat"                 
[23] "horse"                          "donkey"                        
[25] "hedgehog european"              "monkey patas"                  
[27] "american western chipmunk"      "cat domestic"                  
[29] "galago"                         "giraffe"                       
[31] "whale pilot"                    "seal gray"                     
[33] "hyrax gray"                     "human"                         
[35] "lemur mongoose"                 "elephant african"              
[37] "thick-tailed opposum"           "macaque"                       
[39] "gerbil mongolian"               "hamster golden"                
[41] "vole "                          "mouse house"                   
[43] "brown little bat"               "round-tailed muskrat"          
[45] "loris slow"                     "degu"                          
[47] "grasshopper northern mouse"     "rabbit"                        
[49] "sheep"                          "chimpanzee"                    
[51] "tiger"                          "jaguar"                        
[53] "lion"                           "baboon"                        
[55] "hedgehog desert"                "potto"                         
[57] "mouse deer"                     "phalanger"                     
[59] "seal caspian"                   "porpoise common"               
[61] "potoroo"                        "armadillo giant"               
[63] "hyrax rock"                     "rat laboratory"                
[65] "striped african mouse"          "monkey squirrel"               
[67] "american eastern mole"          "rat cotton"                    
[69] "rat mole"                       "ground arctic squirrel"        
[71] "thirteen-lined ground squirrel" "golden-mantled ground squirrel"
[73] "shrew musk"                     "pig"                           
[75] "short-nosed echidna"            "american eastern chipmunk"     
[77] "tapir brazilian"                "tenrec"                        
[79] "shrew tree"                     "bottle-nosed dolphin"          
[81] "genet"                          "fox arctic"                    
[83] "fox red"                       

Nous avons ici 3 duos de parenthèses, donc 3 groupes. Le premier attrape le premier mot (\w+), le deuxième le second mot (encore \w+) et le dernier attrape tout le reste (.*). Ensuite, on reconstruit une chaîne de caractères, mais en plaçant le deuxième groupe (\2) avant le premier (\1).

Mise en garde

Comme dit si bien Spider-man : À grand pouvoir correspond grande responsabilité.

C’est particulièrement vrai lorsque l’on parle de regex. Vaut mieux y aller avec modération. Il peut parfois être plus lisible pour votre “vous” futur de séparer ce que vous avez à faire en quelques opérations simples et lisibles, plutôt que d’essayer de tout faire dans un regex.

Je vous présente en conclusion un regex classique, utilisé dans une librairie du langage de programmation Perl pour valider si une adresse de courriel est valide ou non (https://metacpan.org/release/RJBS/Email-Valid-1.200/source/lib/Email/Valid.pm) :

[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\
xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xf
f\n\015()]*)*\)[\040\t]*)*(?:(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\x
ff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"[^\\\x80-\xff\n\015
"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[\040\t]*(?:\([^\\\x80-\
xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80
-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*
)*(?:\.[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\
\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\
x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x8
0-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"[^\\\x80-\xff\n
\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[\040\t]*(?:\([^\\\x
80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^
\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040
\t]*)*)*@[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([
^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\
\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\
x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-
\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()
]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\
x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\04
0\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\
n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\
015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?!
[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\
]]|\\[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\
x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\01
5()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*)*|(?:[^(\040)<>@,;:".
\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]
)|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[^
()<>@,;:".\\\[\]\x80-\xff\000-\010\012-\037]*(?:(?:\([^\\\x80-\xff\n\0
15()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][
^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)|"[^\\\x80-\xff\
n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[^()<>@,;:".\\\[\]\
x80-\xff\000-\010\012-\037]*)*<[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?
:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-
\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:@[\040\t]*
(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015
()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()
]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\0
40)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\
[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\
xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*
)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80
-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x
80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t
]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\
\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])
*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x
80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80
-\xff\n\015()]*)*\)[\040\t]*)*)*(?:,[\040\t]*(?:\([^\\\x80-\xff\n\015(
)]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\
\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*@[\040\t
]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\0
15()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015
()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(
\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|
\\[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80
-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()
]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x
80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^
\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040
\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".
\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff
])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\
\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x
80-\xff\n\015()]*)*\)[\040\t]*)*)*)*:[\040\t]*(?:\([^\\\x80-\xff\n\015
()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\
\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*)?(?:[^
(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-
\037\x80-\xff])|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\
n\015"]*)*")[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|
\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))
[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80-\xff
\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\x
ff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(
?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\
000-\037\x80-\xff])|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\
xff\n\015"]*)*")[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\x
ff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)
*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*)*@[\040\t]*(?:\([^\\\x80-\x
ff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-
\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)
*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\
]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\]
)[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-
\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\x
ff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(
?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80
-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<
>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x8
0-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\040\t]*(?:
\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]
*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)
*\)[\040\t]*)*)*>)

Références

La plupart du matériel de cet atelier est fortement inspiré des chapitres en construction de la prochaine édition de R for Data Science de Hadley Wickham, disponibles en ligne ici :

https://r4ds.hadley.nz/strings.html

https://r4ds.hadley.nz/regexps.html