Les regex, enfin comprises

Tutoriels R
R bits
Les expressions régulières font peur à beaucoup de monde. Elles obéissent pourtant à une logique très simple. Décryptage avec stringr et quelques classiques de base R.
Auteur·rice

Thelma Panaïotis

Date de publication

31 juillet 2026

Mots clés

regex, expressions régulières, stringr, gsub, manipulation de texte, R

Les regex, enfin comprises

Ce charabia de symboles cache une logique très simple. Promis.

Pourquoi les regex font peur (et pourquoi elles ne devraient pas)

Il y a un rituel bien connu chez qui écrit du code : on cherche « comment extraire un numéro de téléphone d’une chaîne de caractère », on tombe sur une réponse qui mentionne du regex et un motif du genre ^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$ (pour un format US à dix chiffres), on copie-colle, ça marche, et on passe à autre chose sans avoir rien compris. Enfin, ça c’était à l’époque de Google et StackOverflow : maintenant ChatGPT, Claude et compagnie génèrent le regex encore plus vite. Mais le problème reste le même : on copie-colle un charabia de symboles qui fonctionne, sans savoir pourquoi. Moi aussi je suis passée par là.

Une regex, de l’anglais regular expression (expression régulière pour nous francophones), c’est en réalité juste un langage pour décrire un motif dans du texte. Rien de plus. Le mot « chat » est déjà une regex : elle décrit le motif « la lettre c, suivie de h, suivie de a, suivie de t ». Ce qui fait peur, ce sont les symboles qu’on ajoute pour décrire des motifs plus flexibles : « un chiffre », « n’importe quel caractère », « zéro ou plusieurs répétitions »… Une fois qu’on connaît la dizaine de symboles qui reviennent tout le temps, la plupart des regex se lisent presque comme une phrase.

Petit exemple pour se faire une idée. Est-ce qu’une chaîne contient un chiffre ?

library(stringr)

str_detect("adresse123", "[0-9]")
[1] TRUE
str_detect("adresse", "[0-9]")
[1] FALSE

[0-9] se lit « n’importe quel chiffre entre 0 et 9 ». str_detect() renvoie TRUE si le motif est trouvé quelque part dans la chaîne, FALSE sinon. C’est tout. Pas de magie.

Deuxième exemple, un cran au-dessus : on ne cherche plus juste un chiffre, mais un format précis. Une chaîne ressemble-t-elle à un code postal de France métropolitaine (5 chiffres) ?

codes <- c("83600", "83", "AB123", "830000")

str_detect(codes, "^[0-9]{5}$")
[1]  TRUE FALSE FALSE FALSE

Trois symboles suffisent à exprimer « exactement 5 chiffres, ni plus ni moins » : [0-9] pour un chiffre, {5} pour répéter exactement 5 fois, et les symboles ^ / $ pour dire « du tout début à toute fin de la chaîne » (sans eux, "830000" matcherait aussi, puisqu’il contient bien 5 chiffres consécutifs quelque part). Pas besoin de tout retenir dès maintenant : chacun de ces symboles est détaillé dans la section suivante. Ce qu’il faut surtout retenir ici c’est qu’une regex complexe n’est qu’un assemblage de briques simples, posées les unes après autres.

En R, deux mondes se côtoient pour manipuler du texte avec des regex. D’un côté, stringr, le package du tidyverse : des fonctions cohérentes, toutes préfixées str_, qui prennent la chaîne en premier argument. De l’autre, les fonctions de base R comme grepl(), gsub() ou regmatches() : plus anciennes, syntaxe un peu différente, mais toujours là dans énormément de code existant. On va utiliser stringr comme fil rouge, avec les équivalents base R en note à chaque étape clé.

Les briques de base, une par une

Une regex se construit en assemblant des éléments de base, si bien qu’une fois connus, on peut lire et écrire la grande majorité des motifs qu’on croise en pratique. Voici les plus courants.

Les caractères littéraux

La brique la plus simple : chercher du texte tel quel.

str_detect("bonjour tout le monde", "jour")
[1] TRUE

"jour" cherche exactement la séquence j-o-u-r, n’importe où dans la chaîne. C’est déjà une regex, juste sans symbole spécial.

Les classes de caractères

Une classe de caractères représente « un caractère parmi un ensemble ». C’est là que ça devient utile.

Symbole Signification
[abc] a, b ou c
[a-z] une lettre minuscule
[0-9] ou \\d un chiffre
\\w un caractère alphanumérique (lettre, chiffre ou _)
\\s un espace (espace, tabulation, retour à la ligne)
. n’importe quel caractère (sauf retour à la ligne)

En R, le backslash \ doit être doublé dans une chaîne de caractères classique (\\d), car \ est lui-même un caractère d’échappement. C’est une source fréquente d’erreurs pour qui débute.

Un exemple concret : est-ce qu’une chaîne contient au moins un chiffre ?

str_detect(c("code42", "sans chiffre"), "\\d")
[1]  TRUE FALSE

Les quantificateurs

Un quantificateur précise combien de fois répéter ce qui précède.

Symbole Signification
* zéro ou plusieurs fois
+ une ou plusieurs fois
? zéro ou une fois (optionnel)
{n} exactement n fois
{n,m} entre n et m fois

Par exemple, \\d+ veut dire « un ou plusieurs chiffres à la suite », c’est-à-dire un nombre entier :

str_extract("le prix est 1250 euros et 12 centimes", "\\d+")
[1] "1250"

str_extract() renvoie la première portion de texte qui correspond au motif, ici "1250".

Le ? sert souvent à rendre un caractère optionnel, pour couvrir deux façons d’écrire un même code. Un cas fréquent en pratique : un code de lot ou de zone, saisi parfois avec un tiret, parfois sans.

codes_lot <- c("FR-001", "FR001", "FR-002", "US-001")

str_detect(codes_lot, "FR-?001")
[1]  TRUE  TRUE FALSE FALSE

"FR-?001" se lit « FR, suivi d’un tiret optionnel, suivi de 001 » : ça matche "FR-001" et "FR001", qu’il y ait un tiret ou non, mais pas "FR-002" (mauvais numéro) ni "US-001" (mauvais indicatif). Ici, le ? change vraiment le résultat : sans lui, le motif littéral "FR-001" ne matcherait que la version avec tiret, et raterait "FR001".

Les ancres

Les ancres ne représentent pas un caractère : elles précisent une position dans la chaîne.

Symbole Signification
^ début de la chaîne
$ fin de la chaîne

Elles peuvent s’utiliser seules ou ensemble. Prenons une liste de poissons :

poissons <- c("thon rouge", "thon blanc", "maquereau", "requin marteau")

# Commence par "thon"
str_detect(poissons, "^thon")
[1]  TRUE  TRUE FALSE FALSE
# Finit par "eau"
str_detect(poissons, "eau$")
[1] FALSE FALSE  TRUE  TRUE

"^thon" matche les deux thons, mais pas les autres éléments, qui ne commence pas par ces lettres. "eau$" matche "maquereau" et "requin marteau", qui se terminent tous les deux par ces lettres, mais pas les thons.

AstuceLe symbole ^ : négation ou ancre selon le contexte
  • À l’intérieur de crochets […], en première position : ^ veut dire « négation ». [^a-z0-9] se lit « tout caractère qui n’est PAS une lettre minuscule ni un chiffre ».
  • En dehors de crochets, seul : ^ veut dire « ancre de début de chaîne », comme dans "^thon" ci-dessus.
# Négation : un caractère qui n'est PAS une lettre
str_detect("abc1", "[^a-z]")
[1] TRUE
# Ancre : la chaîne commence par "abc"
str_detect("abc1", "^abc")
[1] TRUE

Les deux ancres combinées permettent d’exiger que le motif couvre la chaîne entière, du début à la fin, pas juste une portion :

# Contient au moins un chiffre, n'importe où
str_detect("daurade123", "\\d")
[1] TRUE
# Est composé UNIQUEMENT de chiffres, du début à la fin
str_detect("daurade123", "^\\d+$")
[1] FALSE
str_detect("123456", "^\\d+$")
[1] TRUE

Sans les ancres, "^\\d+$" deviendrait simplement "\\d+", qui matcherait aussi "daurade123" puisqu’il contient des chiffres quelque part. Les ancres forcent le motif à couvrir la chaîne entière.

Les regroupements

Un quantificateur ne s’applique qu’au caractère (ou à la classe) qui le précède immédiatement. Pour répéter un groupe entier de caractères, on l’entoure de parenthèses (...).

codes <- c("1212", "1222")

str_detect(codes, "^(12)+$")
[1]  TRUE FALSE
str_detect(codes, "^12+$")
[1] FALSE  TRUE

Avec le regroupement (12)+, seul "1212" matche : c’est bien une répétition du bloc complet “12”. Sans les parenthèses, 12+ veut dire tout autre chose : « un 1, suivi de 2 répété une ou plusieurs fois », ce matche "1222" mais pas "1212" (le second “1” casse la série de 2). Le regroupement change donc entièrement ce que le quantificateur répète.

AstuceComment lire une regex composée

Face à un motif comme ^(19|20)\\d{2}$, on le décompose brique par brique plutôt que de le lire d’un bloc :

  • ^ : début de la chaîne
  • (19|20) : le groupe “19” ou “20”
  • \\d{2} : suivi de deux chiffres
  • $ : fin de la chaîne

Soit : « la chaîne est composée de 4 chiffres, commençant par 19 ou 20 », un motif utile pour repérer une année entre 1900 et 2099. C’est exactement ce réflexe de décomposition qu’on va appliquer aux exemples plus corsés de la section suivante.

Les fonctions stringr à connaître, et le raisonnement pour les cas corsés

Le mapping mental

Quatre familles de fonctions couvrent la grande majorité des besoins :

Besoin Fonction stringr Équivalent base R
Détecter (vrai/faux) str_detect() grepl()
Extraire str_extract(), str_extract_all() regmatches()
Remplacer str_replace(), str_replace_all() sub(), gsub()
Découper str_split() strsplit()

Petite nuance importante : str_extract() ne remplace que la première occurrence trouvée, str_extract_all() remplace toutes les occurrences. Le même principe s’applique à sub() (une fois) contre gsub() (toutes les fois) en base R.

x <- "poulpe crabe poulpe"

str_replace(x, "poulpe", "seiche")
[1] "seiche crabe poulpe"
str_replace_all(x, "poulpe", "seiche")
[1] "seiche crabe seiche"

Cas pratique 1 : nettoyer des noms de colonnes

Un classique après un import : des noms de colonnes avec des espaces, des majuscules et des caractères bizarres.

noms_colonnes <- c("Nom Espece", "Prix au kg (€)", "Zone de peche")

Le raisonnement : on veut passer de "Nom Espece" à quelque chose comme nom_espece. Ça se décompose en plusieurs étapes distinctes, qu’on traite une par une plutôt que de chercher un seul motif magique qui ferait tout d’un coup.

  1. Tout mettre en minuscules, avec str_to_lower() (pas de regex nécessaire).
  2. Remplacer tout ce qui n’est pas une lettre ou un chiffre par un underscore.
  3. Nettoyer les underscores en trop (doublons, ou en bord de chaîne).

L’étape 2 utilise [^a-z0-9], la classe négative vue en section précédente : « tout caractère qui n’est pas une lettre minuscule ni un chiffre ».

noms_propres <- noms_colonnes |> 
  str_to_lower() |> 
  str_replace_all("[^a-z0-9]+", "_") |> 
  str_replace_all("^_|_$", "")

noms_propres
[1] "nom_espece"    "prix_au_kg"    "zone_de_peche"

"[^a-z0-9]+" remplace tout bloc de caractères indésirables par un seul underscore (le + évite d’avoir plusieurs _ à la suite pour "(€)"). Et "^_|_$" utilise cette fois ^ et $ comme ancres, combinées à | (« ou »), ce qui matche un underscore en tout début ou en toute fin de chaîne, pour le retirer.

Ici, noms_colonnes ne contient volontairement aucun accent : avec [^a-z0-9], un è serait traité comme indésirable et remplacé par un _ ("esp_ce" plutôt que "espece"). En pratique, mieux vaut de toute façon passer par le package janitor et sa fonction clean_names(), qui gère ce nettoyage (accents compris) en une ligne. Mais comprendre le mécanisme sous-jacent reste utile pour ajuster le résultat quand clean_names() ne fait pas exactement ce qu’il faut.

Cas pratique 2 : extraire un nombre décimal dans du texte libre

Extraire "1250" d’une phrase, on l’a déjà fait. Mais que se passe-t-il avec des nombres décimaux ou négatifs ?

textes <- c(
  "Le solde est de -42.5 euros",
  "Température : 18,3°C",
  "Aucune valeur numérique ici"
)

Première tentative avec \\d+, qui ne gère que les chiffres :

str_extract(textes, "\\d+")
[1] "42" "18" NA  

Ça coupe la partie décimale ("42" au lieu de "-42.5") et perd le signe négatif. On raisonne étape par étape sur ce qu’on veut vraiment matcher :

  • un signe moins optionnel : -?
  • un ou plusieurs chiffres : \\d+
  • un séparateur décimal optionnel (point ou virgule), suivi de chiffres : [.,]\\d+ rendu optionnel avec ?

Ce qui donne, en assemblant les briques :

str_extract(textes, "-?\\d+[.,]?\\d*")
[1] "-42.5" "18,3"  NA     

Chaque brique reste indépendante et peut se tester séparément si le résultat surprend. C’est la meilleure façon de déboguer une regex qui ne fait pas ce qu’on attend : la couper en morceaux, et vérifier chaque morceau isolément.

Cas pratique 3 : valider un format d’adresse e-mail (simplifié)

Un motif souvent copié-collé sans être compris. Voici comment le construire soi-même, en partant de la structure d’une adresse : quelquechose@domaine.extension.

emails <- c("contact@pyranhia.eu", "pas-un-email", "test@site")

On découpe le problème en trois blocs, dans l’ordre où ils apparaissent :

  1. avant le @ : un ou plusieurs caractères alphanumériques (ou ., _, -) → [\\w.-]+
  2. le @ littéral
  3. après le @ : un nom de domaine avec au moins un point, suivi d’une extension → [\\w-]+\\.[a-z]+
pattern_email <- "^[\\w.-]+@[\\w-]+\\.[a-z]+$"

str_detect(emails, pattern_email)
[1]  TRUE FALSE FALSE

"test@site" est rejeté à juste titre : pas de point ni d’extension après le @. C’est un motif volontairement simplifié, la vraie spécification des adresses e-mail est bien plus permissive et complexe.

À vous de jouer

Rien ne vaut la pratique pour que ces briques deviennent des réflexes. Voici un petit défi en 8 niveaux, du plus simple au plus corsé, pour tester ce qu’on vient de voir : classes de caractères, quantificateurs, ancres, regroupements, négation, et leurs combinaisons.

Figure 1: Écrivez un motif regex qui capture tous les mots à trouver, sans capturer les pièges. Indices et solutions disponibles à chaque niveau en cas de blocage.
AstuceRetour sur l’exemple du début

Le motif qui ouvrait cet article, ^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$, se lit maintenant (presque) sans effort :

  • ancré du début à la fin (^...$),
  • une parenthèse ouvrante optionnelle (\(?),
  • trois chiffres (\d{3}), une parenthèse fermante optionnelle (\)?),
  • un séparateur optionnel ([-.\s]?),
  • trois chiffres,
  • un séparateur optionnel,
  • quatre chiffres.

De quoi valider un numéro américain écrit (555) 123-4567, 555-123-4567 ou 5551234567.

Pour aller plus loin

Les regex ne se limitent pas à stringr : elles apparaissent partout, dans grep en ligne de commande, dans les éditeurs de texte, dans presque tous les langages de programmation. La syntaxe de base présentée ici (classes, quantificateurs, ancres) se retransporte quasiment telle quelle d’un contexte à l’autre.

Pour tester un motif sans relancer R à chaque essai, regex101.com est une référence : coloration syntaxique en direct, explication de chaque brique du motif, et bibliothèque d’exemples. Pratique pour vérifier une regex avant de la coller dans son code (penser à choisir le mode “PCRE” ou équivalent pour rester proche de la syntaxe R).

Envie d’aller plus loin en R ?

Manipulation de données, nettoyage, construction de modèles… J’aborde tout ça avec des cas concrets et réels dans mes formations.