Introduction
Les expressions régulières — communément appelées regex ou regexp — sont des séquences de caractères qui définissent un modèle de recherche. Elles font partie des outils les plus puissants dans l'arsenal d'un développeur, permettant de rechercher, valider, extraire et transformer du texte avec une seule expression compacte.
Que vous validiez une adresse e-mail dans un formulaire web, extrayiez des données de logs, ou effectuiez un remplacement complexe dans des milliers de fichiers, les expressions régulières vous permettent d'exprimer précisément ce que vous recherchez dans une notation concise et portable que comprend pratiquement tous les langages de programmation et la plupart des éditeurs de texte.
Brève Histoire des Expressions Régulières
L'histoire des expressions régulières remonte aux fondements de l'informatique théorique :
- 1951 — Le mathématicien Stephen Kleene formalise le concept de langages réguliers et introduit la notation étoile de Kleene (
*) dans le cadre de ses travaux sur la théorie des automates. - 1968 — Ken Thompson implémente les expressions régulières dans l'éditeur de texte QED, puis dans les outils Unix comme
grep,sedetawk. - 1986 — POSIX standardise deux variantes : BRE (Basic Regular Expressions) et ERE (Extended Regular Expressions).
- 1997 — Philip Hazel crée la bibliothèque PCRE (Perl Compatible Regular Expressions), qui devient le standard de facto pour les regex avancées.
- 1999 — ECMAScript 3 standardise l'objet
RegExpde JavaScript. - 2015 — ES6 ajoute les flags
u(Unicode) ety(sticky). - 2018 — ES2018 ajoute les groupes de capture nommés (
(?<nom>...)) et les assertions lookbehind.
POSIX vs PCRE vs JavaScript RegExp
| Fonctionnalité | BRE/ERE (POSIX) | PCRE | JavaScript RegExp |
|---|---|---|---|
| Lookahead | ✗ | ✓ | ✓ |
| Lookbehind | ✗ | ✓ | ✓ (ES2018+) |
| Groupes nommés | ✗ | ✓ | ✓ (ES2018+) |
| Correspondance non-greedy | ✗ | ✓ | ✓ |
| Unicode | Limité | ✓ | ✓ (avec flag u) |
| Rétro-références | ✓ | ✓ | ✓ |
Référence de la Syntaxe Core
Tableau de Référence Rapide
| Pattern | Signification |
|---|---|
. |
N'importe quel caractère sauf le saut de ligne |
^ |
Début de chaîne / début de ligne (avec flag m) |
$ |
Fin de chaîne / fin de ligne (avec flag m) |
\d |
N'importe quel chiffre [0-9] |
\D |
N'importe quel non-chiffre |
\w |
Caractère de mot [a-zA-Z0-9_] |
\W |
Caractère de non-mot |
\s |
Espace blanc (espace, tabulation, saut de ligne…) |
\S |
Caractère non-espace |
[abc] |
Classe de caractères — correspond à a, b ou c |
[^abc] |
Classe niée — correspond à tout sauf a, b, c |
[a-z] |
Plage de caractères |
* |
0 ou plusieurs fois (greedy) |
+ |
1 ou plusieurs fois (greedy) |
? |
0 ou 1 fois (greedy) |
{n} |
Exactement n fois |
{n,m} |
Entre n et m fois (greedy) |
*? +? ?? |
Équivalents lazy (non-greedy) |
(abc) |
Groupe de capture |
(?:abc) |
Groupe de non-capture |
(?<nom>abc) |
Groupe de capture nommé |
| |
Alternance — correspond à gauche OU droite |
(?=...) |
Lookahead positif |
(?!...) |
Lookahead négatif |
(?<=...) |
Lookbehind positif |
(?<!...) |
Lookbehind négatif |
Classes de Caractères
Les classes de caractères permettent de faire correspondre un caractère d'un ensemble. [aeiou] correspond à n'importe quelle voyelle. [a-zA-Z] correspond à n'importe quelle lettre. [^0-9] correspond à n'importe quel caractère qui n'est pas un chiffre.
Les classes abrégées les plus courantes :
\déquivaut à[0-9]\wéquivaut à[a-zA-Z0-9_]\scorrespond à l'espace, la tabulation (\t), le saut de ligne (\n), le retour chariot (\r) et autres espaces blancs
Quantificateurs : Greedy vs Lazy
Par défaut, les quantificateurs sont greedy (gourmands) — ils correspondent au maximum possible. Considérez la chaîne HTML <b>gras</b> et <i>italique</i> :
<.*> → correspond à toute la chaîne de <b> à </i> (greedy)
<.*?> → correspond successivement à <b>, </b>, <i>, </i> (lazy)
Ajouter ? après un quantificateur le rend lazy (non-greedy) : il correspond au minimum possible tout en permettant au pattern global de réussir.
Ancres
^correspond au début de la chaîne (ou début de ligne avec le flagm).$correspond à la fin de la chaîne (ou fin de ligne avecm).\bcorrespond à une frontière de mot — la transition entre un caractère de mot et un non-caractère de mot.\Bcorrespond à une non-frontière de mot.
Groupes et Rétro-références
Les groupes de capture (...) capturent le texte correspondant, auquel vous pouvez vous référer ensuite comme \1, \2, etc. (rétro-références), ou y accéder via le tableau de résultats.
Les groupes de non-capture (?:...) groupent des sous-patterns sans créer une capture, ce qui est plus efficace quand vous n'avez pas besoin de la valeur capturée.
Les groupes nommés (?<année>\d{4}) permettent de référencer les captures par nom (match.groups.année en JavaScript), rendant les patterns bien plus lisibles.
Lookahead et Lookbehind
Ces assertions de largeur zéro correspondent à une position, pas à un caractère :
\d+(?= euros) → correspond aux chiffres seulement s'ils sont suivis de " euros"
\d+(?! euros) → correspond aux chiffres qui ne sont PAS suivis de " euros"
(?<=\$)\d+ → correspond aux chiffres seulement s'ils sont précédés de "$"
(?<!\$)\d+ → correspond aux chiffres qui ne sont PAS précédés de "$"
Les lookaheads et lookbehinds ne consomment pas de caractères, donc le texte correspondant n'inclut pas la partie lookahead/lookbehind.
Flags
| Flag | Nom | Effet |
|---|---|---|
i |
Insensible à la casse | [a-z] correspond aussi à [A-Z] |
g |
Global | Trouve toutes les correspondances, pas seulement la première |
m |
Multilignes | ^ et $ correspondent aux limites de ligne |
s |
dotAll | . correspond aussi aux sauts de ligne |
u |
Unicode | Active la correspondance Unicode complète ; requis pour \p{} |
y |
Sticky | Ne correspond qu'à la position lastIndex |
x |
Verbeux/Étendu | Autorise les espaces et commentaires (PCRE/Python uniquement) |
Le flag x (verbeux) est particulièrement précieux pour documenter des patterns complexes :
import re
pattern = re.compile(r"""
^ # début de chaîne
(?P<year>\d{4}) # année sur 4 chiffres
-
(?P<month>0[1-9]|1[0-2]) # mois 01–12
-
(?P<day>0[1-9]|[12]\d|3[01]) # jour 01–31
$
""", re.VERBOSE)
Patterns Courants avec Exemples Réels
Validation d'E-mail
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
Décomposition :
^[a-zA-Z0-9._%+-]+— partie locale (lettres, chiffres et caractères spéciaux autorisés)@— arobase littéral[a-zA-Z0-9.-]+— nom de domaine\.[a-zA-Z]{2,}$— TLD de 2+ lettres
Correspondance d'URL
https?://(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+(?:/[^\s]*)?
Date ISO (YYYY-MM-DD)
\b\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])\b
Valide le mois 01–12 et le jour 01–31. Ne valide pas les plages de jours spécifiques aux mois (ex. le 30 février passerait la validation).
Adresse IPv4
\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b
Décomposition :
25[0-5]— 250–2552[0-4]\d— 200–249[01]?\d\d?— 0–199
Numéro de Téléphone (États-Unis)
\+?1?\s?[\(]?\d{3}[\)]?[-.\s]?\d{3}[-.\s]?\d{4}
Correspond aux formats comme (555) 123-4567, 555-123-4567, +1 555 123 4567.
Code Couleur Hexadécimal
#(?:[0-9A-Fa-f]{3}){1,2}\b
Correspond aux couleurs hex à 3 chiffres (#F00) et à 6 chiffres (#FF0000).
Les Expressions Régulières dans Différents Langages de Programmation
JavaScript
// Syntaxe littérale avec flags
const regex = /^hello\s+world$/im;
const match = "Hello World".match(regex);
// Syntaxe constructeur (utile pour les patterns dynamiques)
const term = "world";
const dynamic = new RegExp(`hello\\s+${term}`, "im");
// Remplacer toutes les occurrences
const result = "foo bar foo".replaceAll(/foo/g, "baz");
// Captures nommées (ES2018+)
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { year, month, day } = "2024-03-15".match(dateRegex).groups;
Python
import re
# Compiler pour réutilisation
pattern = re.compile(r'^hello\s+world$', re.IGNORECASE | re.MULTILINE)
match = pattern.match("Hello World")
# Trouver toutes les correspondances
dates = re.findall(r'\d{4}-\d{2}-\d{2}', text)
# Remplacer avec une fonction
result = re.sub(r'\b\d+\b', lambda m: str(int(m.group()) * 2), "1 plus 2 égale 3")
# Groupes nommés
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', "2024-03")
print(m.group('year')) # 2024
Java
import java.util.regex.*;
Pattern p = Pattern.compile("^hello\\s+world$",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher("Hello World");
boolean found = m.matches();
// Extraire des groupes
Pattern datePattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher dm = datePattern.matcher("Aujourd'hui est 2024-03-15");
if (dm.find()) {
String year = dm.group(1);
}
Go
import "regexp"
re := regexp.MustCompile(`(?im)^hello\s+world$`)
match := re.FindString("Hello World")
// Trouver toutes les sous-correspondances
dateRe := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
all := dateRe.FindAllStringSubmatch(text, -1)
for _, m := range all {
year, month, day := m[1], m[2], m[3]
_ = year; _ = month; _ = day
}
// Groupes nommés
namedRe := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})`)
match2 := namedRe.FindStringSubmatch("2024-03")
yearIdx := namedRe.SubexpIndex("year")
fmt.Println(match2[yearIdx]) // 2024
Performance et Backtracking Catastrophique
Comment Fonctionne le Backtracking
La plupart des moteurs de regex utilisent un matching basé sur NFA (Automate Fini Non Déterministe), ce qui signifie qu'ils peuvent essayer plusieurs chemins à travers le pattern lorsqu'une tentative échoue. Ce backtracking est ce qui permet les fonctionnalités comme les lookaheads et les rétro-références — mais peut aussi être un piège de performance.
Le Cas Catastrophique
Considérez le pattern (a+)+ appliqué à la chaîne "aaaaaX" :
- Le
+externe essaie de matcher autant de groupes que possible. - Quand le moteur atteint
Xet échoue, il fait du backtracking et essaie différentes façons de répartir les caractèresaentre les répétitions du groupe. - Pour une chaîne de longueur n, il y a 2^(n-1) répartitions possibles — menant à une complexité temporelle exponentielle.
(a+)+ sur "aaaaaaaaaaaaaaaaaX" → peut prendre des secondes ou des minutes !
D'autres patterns dangereux incluent (a|aa)+, (\w+\s*)+, et tout ce qui comporte des quantificateurs imbriqués sur des classes de caractères qui se chevauchent.
Comment l'Éviter
- Éviter les quantificateurs imbriqués sur le même ensemble de caractères :
(a+)+→ utilisera+à la place. - Utiliser des groupes atomiques
(?>...)ou des quantificateurs possessifsa++(PCRE) pour empêcher le backtracking dans les groupes déjà matchés. - Être spécifique : remplacer
.*par une classe de caractères qui exclut les délimiteurs (ex.[^"]*à l'intérieur de chaînes entre guillemets). - Ancrer les patterns quand c'est possible pour que le moteur échoue rapidement.
- Utiliser un timeout dans le code de production lors du traitement d'entrées non fiables.
Comment Lire une Expression Régulière Complexe
Décomposons ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ :
^ → ancre : début de chaîne
[a-zA-Z0-9._%+-]+ → un ou plusieurs caractères autorisés dans la partie locale
@ → @ littéral
[a-zA-Z0-9.-]+ → un ou plusieurs caractères de domaine
\. → point littéral (échappé)
[a-zA-Z]{2,} → TLD : 2 lettres ou plus
$ → ancre : fin de chaîne
Conseil : Utilisez le testeur de regex de ce site pour mettre en évidence chaque groupe capturé et voir exactement quelle partie de l'entrée correspond à chaque token. Construisez votre pattern de manière incrémentale — commencez par la pièce valide la plus simple, vérifiez-la, puis étendez-la.
Meilleures Pratiques
- Compiler une fois, utiliser souvent. La pré-compilation d'un pattern (ex.
re.compile()en Python,Pattern.compile()en Java) est beaucoup plus efficace que de le re-parser à chaque appel. - Préférer les groupes de non-capture
(?:...)quand la valeur capturée n'est pas nécessaire. Cela signale l'intention et évite l'allocation mémoire inutile. - Utiliser des raw strings pour les patterns. En Python, utiliser
r'\d+'plutôt que'\\d+'pour éviter le double échappement. En JavaScript, la syntaxe littérale/\d+/gère cela automatiquement. - Nommer vos captures.
(?<année>\d{4})est bien plus maintenable que de s'appuyer sur l'index de groupe\1. - Tester avec les cas limites : chaînes vides, chaînes qui correspondent presque-mais-pas-tout-à-fait, caractères Unicode et entrées très longues.
- Documenter les patterns complexes avec le flag
x(verbeux) en Python/PCRE, ou avec des commentaires inline dans votre code. - Ne jamais utiliser regex pour parser du HTML ou XML complet. Utiliser une bibliothèque de parsing appropriée.
- Valider les entrées côté serveur. La validation regex côté client améliore l'UX mais ne doit pas être la seule ligne de défense.
Questions Fréquemment Posées (FAQ)
Q : Quelle est la différence entre match() et search() en Python ?
R : re.match() ne correspond qu'au début de la chaîne. re.search() parcourt toute la chaîne pour trouver une correspondance. Utilisez re.fullmatch() pour exiger que le pattern corresponde à toute la chaîne.
Q : Pourquoi ^ à l'intérieur d'une classe de caractères [^abc] signifie-t-il quelque chose de différent ?
R : À l'intérieur d'une classe de caractères, ^ comme premier caractère nie la classe — il correspond à n'importe quel caractère qui n'est pas dans l'ensemble. En dehors d'une classe de caractères, ^ est une ancre pour le début de la chaîne.
Q : Puis-je utiliser regex pour parser du HTML ?
R : Pour des extractions simples et bien définies de structures HTML connues, regex peut fonctionner. Mais HTML n'est pas un langage régulier — il permet un imbrication arbitraire et des balises de fermeture optionnelles. Utilisez un parser HTML approprié (ex. BeautifulSoup en Python, DOMParser en JS) pour un parsing robuste.
Q : Quelle est la différence entre les quantificateurs greedy et possessifs ?
R : Les quantificateurs greedy font du backtracking — ils essaient la correspondance maximale et rendent des caractères si nécessaire. Les quantificateurs possessifs (ex. a++ en PCRE) ne rendent jamais rien — une fois qu'ils matchent, le match est verrouillé. Cela empêche le backtracking catastrophique mais peut aussi faire échouer un match qu'un quantificateur greedy aurait réussi.
Q : Comment faire correspondre un point littéral . ou une parenthèse ( ?
R : Échappez-les avec un backslash : \. correspond à un point littéral, \( correspond à une parenthèse ouvrante littérale.
Q : La regex est-elle sensible à la casse par défaut ?
R : Oui. Utilisez le flag i (/pattern/i en JavaScript, re.IGNORECASE en Python) pour activer la correspondance insensible à la casse.
Q : À quoi correspond \b ?
R : \b est une assertion de frontière de mot de largeur zéro. Elle correspond à la position entre un caractère de mot (\w) et un caractère de non-mot (\W).
Q : Comment tester si une chaîne entière correspond à un pattern ?
R : Ancrez avec ^ et $ : ^pattern$. En Python, vous pouvez aussi utiliser re.fullmatch(). En JavaScript, utilisez .test() avec des ancres ou vérifiez que match()[0].length === input.length.
Utilisez le testeur de regex de ce site pour expérimenter avec chaque pattern de ce guide. Collez n'importe quel pattern, saisissez votre chaîne de test et observez les correspondances surlignées en temps réel.