Les expressions régulières, pour quoi faire?

Les expressions régulières servent à effectuer des recherches de patterns dans les strings en définissant les règles qui régissent ces patterns.

Par exemple, si je cherche à trouver le mot "turlututu" dans le string "turlututu chapeau pointu", mon pattern est simplement "turlututu". Si en revanche, je cherche à trouver tous les mots débutant par une majuscule qui ne correspondent pas à un début de phrase dans "For the Night is Dark and full of Terrors." mon pattern est défini par un ensemble de règles, qu'il va falloir réussir à expliquer (coder) dans un language intelligible par R, i.e. le langage des expressions régulières.

J'ai longtemps éprouvé un profond désespoir face aux expressions régulières car elles me semblaient à la fois profondément utiles et terriblement inaccessibles... Dans mon usage ordinaire de R, j'arrive généralement à m'auto-former à de nouvelles méthodes en décortiquant des exemples d'application disponibles en ligne (par exemple sur stackoverflow). Pour ce qui est des expressions régulières, ces exemples me paraissaient totalement obscurs (et pour cause, une expression régulière c'est en quelque sorte un language à part entière...). Par exemple, pour trouver un pattern qui correspond à un mot qui commence par une majuscule mais qui n'est pas placé en début de phrase, il faut lui indiquer l'expression régulière "(?<![\\!\\.]\\s)\\b[:upper:][:lower:]+\\b"... WTF, n'est-ce pas?

Bon, heureusement, le package stringr est passé par là (cf mon précédent billet), et la lecture de ce tutoriel sur les expressions régulières m'a grandement aidée à y voir plus clair...

Pour "résumer" les différents aspects des expressions régulières que j'aborderai par la suite, j'ai produit la figure suivante:

Notez qu'il existe également une "cheatsheet" (moins rose mais plus exhaustive) ici...

Recherche de patterns avec stringr

Avant d'entamer les choses sérieuses (i.e. l'écriture des expressions régulières), une petite remarque.

Pour montrer les résultats de la recherche de patterns, je vais utiliser les fonctions du package stringr, et notamment une fonction str_view_all qui permet de montrer la partie d'un string correspondant à un pattern.

Ainsi, pour voir le résultat de recherche du pattern "youpi" dans les strings suivants, voici comment je peux procéder:

s=c("wip wip","youpi","youpla","boum")
str_view_all(s,"youpi")

## [1] "wip wip" "{youpi}" "youpla"  "boum"

(Quand on l'utilise sous RStudio, la fonction str_view_all ouvre un widget html où la partie du string correspondant au pattern apparaît sur un fond grisé. Ici, j'ai un peu "twisté" cette fonction pour pouvoir en montrer les résultats sur mon blog, mais ne vous laissez pas perturber par ce détail!)

Classes de caractères et groupes

On peut rechercher une classe de caractères en utilisant la notation [...].

Par exemple, si je veux rechercher toutes les voyelles dans mon string:

str_view_all("youp la boum",
             "[aeiou]")

## [1] "y{o}{u}p l{a} b{o}{u}m"

Remarquez bien la différence:

str_view_all(c("A132","f445","e345","C308","M2244","Z449","E18"),
             "[308]")

## [1] "A1{3}2"     "f445"       "e{3}45"     "C{3}{0}{8}" "M2244"      "Z449"       "E1{8}"

str_view_all(c("A132","f445","e345","C308","M2244","Z449","E18"),
             "308")

## [1] "A132"   "f445"   "e345"   "C{308}" "M2244"  "Z449"   "E18"

Si l'on veut désigner un caractère quelconque, alors on peut utiliser la notation .

Par exemple, si l'on souhaite rechercher n'importe quel caractère (excepté le retour à la ligne) suivi d'une lettre minuscule:

str_view_all(c("32a","B44","552","98eEf"),
             ".[a-z]")

## [1] "3{2a}"     "B44"       "552"       "9{8e}{Ef}"

Caractères spéciaux

Si je veux trouver tous les points, points d'interrogation ou points d'exclamation:

str_view_all(c("Allô, John-John?", "Ici Joe la frite.", "Surprise!"),
             "[\\.\\?\\!]")

## [1] "Allô, John-John{?}"  "Ici Joe la frite{.}" "Surprise{!}"

Remarquez qu'on ne cherche pas le pattern "[.?!]", mais le pattern "[\\.\\?\\!]". Voici pourquoi:

. (comme nous l'avons vu précédemment), mais aussi ? et ! sont des caractères spéciaux dans le cadre des expressions régulières. Donc, pour dire qu'on parle d'un "vrai" point, point d'interrogation ou point d'exclamation, on utilise l'escape character \. L'expression régulière est donc [\.\?\!]...

Mais on ne s'arrête pas là... En effet, ce n'est pas directement une expression régulière que l'on passe à la fonction, mais plutôt une chaîne de caractères qui est elle-même "transformée" en expression régulière... Il faut donc utiliser l'escape character \ devant les \. Et voilà comment on se retrouve à passer le pattern "[\\.\\?\\!]".

Pour être sûre que vous ayiez bien compris, dans l'autre sens, cela donne:

  • on lui demande un pattern "\\."
  • la fonction l'interprète comme expression régulière \.
  • on recherche donc un "vrai" .

Mais revenons à nos moutons, les classes de caractères.

Caractères exclus

On peut définir une classe de caractères en listant les caractères qu'elle inclut, mais également en listant l'ensemble des caractères exclus en utilisant la notation [^...]:

Par exemple, pour trouver tous les caractères qui ne sont ni une voyelle ni un espace:

str_view_all("turlututu chapeau pointu",
             "[^aeiou ]")

## [1] "{t}u{r}{l}u{t}u{t}u {c}{h}a{p}eau {p}oi{n}{t}u"

Gammes de caractères

Enfin, on peut définir des classes de caractères correspondant à des gammes de valeurs en utilisant la notation [...-...]

Par exemple, pour trouver tous les chiffres entre 1 et 5:

str_view_all(c("3 petits cochons", "101 dalmations", "7 nains"),
             "[1-5]")

## [1] "{3} petits cochons" "{1}0{1} dalmations" "7 nains"

Pour trouver toutes les lettres entre A et F:

str_view_all(c("A132g","f445E","e345Z","C308d","M2244","Z449","E18M"),
             "[A-F]")

## [1] "{A}132g" "f445{E}" "e345Z"   "{C}308d" "M2244"   "Z449"    "{E}18M"

Pour trouver toutes les lettres entre A et F et a et e:

str_view_all(c("A132","f445","e345","C308","M2244","Z449","E18"),
             "[A-Fa-e]")

## [1] "{A}132" "f445"   "{e}345" "{C}308" "M2244"  "Z449"   "{E}18"

Classes prédéfinies

Notez qu'il existe des classes de caractères prédéfinies

  • \w: un caractère alphabétique, ou un chiffre, ou un underscore _
  • [:alnum:]: un caractère alphanumérique (caractère alphabétique ou chiffre)
  • [:alpha:]: une caractère alphabétique
  • [:lower:]: une caractère alphabétique minuscule
  • [:upper:]: une caractère alphabétique majuscule
  • [:digit:] (qu'on peut aussi écrire \d): un chiffre
  • [:punct:]: un caractère de ponctuation
  • [:space:]: un espace (espace simple, tabulation, tabulation verticale, nouvelle ligne, etc.)
  • [:blank:]: un "blanc" (espace simple ou tabulation)

Quelques exemples pour bien comprendre

Une lettre entre A and E, puis un chiffre entre 1 et 6, puis un point, puis "txt":

str_view_all(c("A2.txt","B5.png","C3.txt","E6.txt","E9.txt","F4.txt"), 
             "[A-E][1-6]\\.txt")

## [1] "{A2.txt}" "B5.png"   "{C3.txt}" "{E6.txt}" "E9.txt"   "F4.txt"

Un chiffre suivi d'un espace:

str_view_all(c("7 nains","3 petits cochons","101 dalmatiens"), 
             "[0-9] ")

## [1] "{7 }nains"          "{3 }petits cochons" "10{1 }dalmatiens"

Un caractère suivi d'un chiffre suivi d'un point:

str_view_all(c("/pouet3.kebop4.kekepwek.kwak"),
             ".[0-9]\\.")

## [1] "/poue{t3.}kebo{p4.}kekepwek.kwak"

Un caractère de ponctuation suivi d'un espace simple et d'une lettre en majuscule:

str_view_all(c("Allô? Ici John John.", "Allô? ici John-John"),
             "[:punct:] [:upper:]")

## [1] "Allô{? I}ci John John." "Allô? ici John-John"

Groupes et références arrières

Il est possible de créer des groupes au sein des expressions régulières, à l'aide de la notation (...)

Par exemple, je peux créer un premier groupe défini par une consonne suivie d'une voyelle à travers l'expression régulière ([^aeiou ][aeiou]), et un deuxième groupe défini de la même manière.

En l'état, utilisées avec la fonction str_view_all, l'usage des parenthèses dans l'expression régulière n'apporte rien de particulier...

str_view_all(c("tili tili woup lala tutu pop"),
             "([^aeiou ][aeiou])([^aeiou ][aeiou])")

## [1] "{tili} {tili} woup {lala} {tutu} pop"

En revanche, utilisées avec la fonction str_match_all, l'usage des parenthèses permet d'isoler différentes parties du pattern:

str_match_all(c("tili tili woup lala tutu pop"),
              "([^aeiou ][aeiou])([^aeiou ][aeiou])")

## [[1]]
##      [,1]   [,2] [,3]
## [1,] "tili" "ti" "li"
## [2,] "tili" "ti" "li"
## [3,] "lala" "la" "la"
## [4,] "tutu" "tu" "tu"

L'usage conjoint des groupes et des références arrières permet par ailleurs de rechercher des répétitions de motifs dans les patterns.

Ainsi, pour chercher un pattern composé d'une consonne suivi d'une voyelle, répété deux fois, on peut utiliser une référence arrière \1, \2, etc. (Comme d'habitude, pour passer l'expression régulière à la fonction on utilise un string, donc on double le \):

str_view_all(c("turlututu tralala"),
             "([^aeiou ][aeiou])\\1")

## [1] "turlu{tutu} tra{lala}"

Ici on recherche un motif (il n'y en a qu'un, donc il est numéroté "1"), répété immédiatement après sa première occurrence.

Imaginons qu'on ait plusieurs motifs. La numérotation des motifs se fait selon l'ordre dans lequel ils apparaissent dans l'expression régulière, et on peut ensuite placer la référence arrière à l'emplacement de notre choix:

Motif 1 puis motif 2 puis motif 1 puis motif 2:

str_view_all(c("tututuyutu, tututuyutu, tutuyutuyutuyutuyutututuyutu"),
             "([^aeiou ][aeiou])([^aeiou ][aeiou])\\1\\2")

## [1] "tututuyutu, tututuyutu, tu{tuyutuyu}{tuyutuyu}tututuyutu"

Motif 1 puis motif 1 puis motif 1 puis motif 2 puis motif 1:

str_view_all(c("tututuyutu, tututuyutu, tutuyutuyutuyutuyutututuyutu"),
             "([^aeiou ][aeiou])\\1\\1([^aeiou ][aeiou])\\1")

## [1] "{tututuyutu}, {tututuyutu}, tutuyutuyutuyutuyu{tututuyutu}"

Motif 1 puis motif 2 puis motif 2 puis motif 1:

str_view_all(c("tutuyututuyutu"),
             "([^aeiou ][aeiou])([^aeiou ][aeiou])\\2\\1")

## [1] "tutu{yututuyu}tu"

Quantificateurs

Les quantificateurs permettent de préciser le nombre d'occurrences consécutives d'une classe de caractères ou d'un groupe.

zéro ou un

On utilise la notation ? à la suite du caractère ou motif recherché.

str_view_all(c("file1990","fileB1990","file2005","fileAbis2005","fileA2005"),
             "file[:alpha:]?\\d\\d\\d\\d")

## [1] "{file1990}"   "{fileB1990}"  "{file2005}"   "fileAbis2005" "{fileA2005}"

zéro ou plus

On utilise la notation * à la suite du caractère ou motif recherché.

str_view_all(c("fileA0885","fileA","fileB","fileA862"),
             "fileA\\d*")

## [1] "{fileA0885}" "{fileA}"     "fileB"       "{fileA862}"

un ou plus

On utilise la notation + à la suite du caractère ou motif recherché.

str_view_all(c("fileA0885","fileA","fileB","fileA862"),
             "fileA\\d+")

## [1] "{fileA0885}" "fileA"       "fileB"       "{fileA862}"

exactement n fois

On utilise la notation {n} à la suite du caractère ou motif recherché.

str_view_all(c("fileA0885.txt","fileA15.txt","fileA1.txt","fileA862.txt","fileA56998.txt"),
             "fileA\\d{4}\\.txt")

## [1] "{fileA0885.txt}" "fileA15.txt"     "fileA1.txt"      "fileA862.txt"    "fileA56998.txt"

de n à m fois

On utilise la notation {n,m} à la suite du caractère ou motif recherché.

str_view_all(c("fileA0885.txt","fileA15.txt","fileA1.txt","fileA862.txt","fileA56998.txt"),
             "fileA\\d{2,4}\\.txt")

## [1] "{fileA0885.txt}" "{fileA15.txt}"   "fileA1.txt"      "{fileA862.txt}"  "fileA56998.txt"

Quelques exemples pour bien comprendre

str_view_all(c("radio gaga gaga", "radio gougou"),
                "radio\\s(\\w{2,3})\\1")

## [1] "{radio gaga} gaga" "{radio gougou}"

str_view_all(c("Mon adresse c'est joe.lafrite@patates.com",
               "Ecris-moi à petit.bertrand69@croquettes.com"),
                "\\w+\\.[:alpha:]+\\d*@\\w+\\.\\w+")

## [1] "Mon adresse c'est {joe.lafrite@patates.com}"   "Ecris-moi à {petit.bertrand69@croquettes.com}"

Ancres et assertions avant-arrière

Les ancres permettent de spécifier l'emplacement du motif par rapport à un mot ou à un string.

Début d'un string

L'ancre ^ fait référence au début d'un string.

# une majuscule en début de string:
str_view_all(c("Ah! C'est toi John-John?","Ben ça alors, Joe la Frite!"),
             "^[:upper:]")

## [1] "{A}h! C'est toi John-John?"    "{B}en ça alors, Joe la Frite!"

Fin d'un string

L'ancre $ fait référence à la fin d'un string.

str_view_all(c("Allô? John-John?"),
             "\\?$")

## [1] "Allô? John-John{?}"

Limites d'un mot

L'ancre \\b fait référence au début ou à la fin d'un mot.

# tous les mots se terminant par "a":
str_view_all(c("Carla","Lea","Armelle","Marie","Lisa","Alexia","Nina"),
             "a\\b")

## [1] "Carl{a}"  "Le{a}"    "Armelle"  "Marie"    "Lis{a}"   "Alexi{a}" "Nin{a}"

# tous les mots commençant par une majuscule:
str_view_all(c("hey Bertrand, aLLeZ vIeNs, il y aura Magda et John-John!"),
             "\\b[:upper:]")

## [1] "hey {B}ertrand, aLLeZ vIeNs, il y aura {M}agda et {J}ohn-{J}ohn!"

Assertions avant-arrière

Ces assertions servent à vérifier si un motif existe dans un pattern, sans inclure ce motif dans le résultat.

  • (?=...): Assertion avant
  • (?!...): Assertion avant négative
  • (?<=...): Assertion arrière
  • (?<!...): Assertion arrière négative

Exemple d'assertion avant: on cherche les nombres (\b\d+\b) qui sont suivis de " dalmatiens"

str_view_all( c("3 dalmatiens", "12 labradors","101 dalmatiens","4 loulous"),
              "\\b\\d+\\b(?= dalmatiens)")

## [1] "{3} dalmatiens"   "12 labradors"     "{101} dalmatiens" "4 loulous"

Exemple d'assertion avant négative: on cherche les nombres (\b\d+\b) qui ne sont pas suivi de " dalmatiens"

str_view_all( c("3 dalmatiens", "12 labradors","101 dalmatiens","4 loulous"),
              "\\d+\\b(?! dalmatiens)")

## [1] "3 dalmatiens"   "{12} labradors" "101 dalmatiens" "{4} loulous"

Exemple d'assertion arrière: on cherche les mots commençant par une majuscule ([:upper:][:lower:]*) précédés de "Mr ":

str_view_all( c("Mr X", "Mr Robot","James Bond","Mr Clean","Jon Snow"),
              "(?<=Mr )[:upper:][:lower:]*")

## [1] "Mr {X}"     "Mr {Robot}" "James Bond" "Mr {Clean}" "Jon Snow"

Exemple d'assertion arrière négative: on cherche tous les mots commençant par une majuscule et finissant les strings ([:upper:][:lower:]*$) précédés par autre chose que "Mr ":

str_view_all( c("Mr X", "Mr Robot","James Bond","Mr Clean","Jon Snow"),
              "(?<!Mr )[:upper:][:lower:]*$")

## [1] "Mr X"         "Mr Robot"     "James {Bond}" "Mr Clean"     "Jon {Snow}"