library(stringr)
str_detect("adresse123", "[0-9]")[1] TRUE
str_detect("adresse", "[0-9]")[1] FALSE
Thelma Panaïotis
31 juillet 2026
regex, expressions régulières, stringr, gsub, manipulation de texte, R
Ce charabia de symboles cache une logique très simple. Promis.
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 ?
[1] TRUE
[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) ?
[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é.
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.
La brique la plus simple : chercher du texte tel quel.
"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.
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 ?
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() 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.
[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 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 :
[1] TRUE TRUE FALSE FALSE
[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.
^ : négation ou ancre selon le contexte
[…], 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 ».^ veut dire « ancre de début de chaîne », comme dans "^thon" ci-dessus.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 :
[1] TRUE
[1] FALSE
[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.
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 (...).
[1] TRUE FALSE
[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.
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îneSoit : « 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.
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.
Un classique après un import : des noms de colonnes avec des espaces, des majuscules et des caractères bizarres.
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.
str_to_lower() (pas de regex nécessaire).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 ».
[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.
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 ?
Première tentative avec \\d+, qui ne gère que les chiffres :
Ç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 :
-?\\d+[.,]\\d+ rendu optionnel avec ?Ce qui donne, en assemblant les briques :
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.
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.
On découpe le problème en trois blocs, dans l’ordre où ils apparaissent :
@ : un ou plusieurs caractères alphanumériques (ou ., _, -) → [\\w.-]+@ littéral@ : un nom de domaine avec au moins un point, suivi d’une extension → [\\w-]+\\.[a-z]+[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.
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.
Le motif qui ouvrait cet article, ^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$, se lit maintenant (presque) sans effort :
^...$),\(?),\d{3}), une parenthèse fermante optionnelle (\)?),[-.\s]?),De quoi valider un numéro américain écrit (555) 123-4567, 555-123-4567 ou 5551234567.
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.