Python · Traitement de texte

Expressions
Régulières

Syntaxe regex, groupes de capture, assertions, flags et module re Python — guide complet avec testeur interactif.

PATTERN
TEXTE

Introduction

Qu'est-ce qu'une regex ?

Une expression régulière (regex) est une séquence de caractères définissant un motif de recherche. Elle permet de chercher, valider, extraire ou transformer du texte avec précision.

En Python, le module re fournit toutes les fonctionnalités nécessaires. Une regex est compilée en automate fini qui parcourt la chaîne de gauche à droite.

Une regex est un langage dans le langage : sa syntaxe obéit à ses propres règles, indépendantes de Python.

Quand les utiliser ?

✓ OUI Valider un format (email, date, code postal)

✓ OUI Extraire des données structurées d'un texte libre

✓ OUI Nettoyer ou transformer du texte en masse

✓ OUI Parser des logs, fichiers de config, CSV non standard

✗ NON Parser du HTML/XML → utiliser BeautifulSoup

✗ NON Valider des formats très complexes (JSON, URL)

import re

# Chercher un mot exact
re.search(r"bonjour", "Dis bonjour stp")    # Match !
re.search(r"bonjour", "Bonjour le monde !")  # None (casse différente)

# Le r"..." = raw string : le \ n'est pas interprété par Python
# Toujours utiliser r"..." pour écrire des patterns
re.search(r"\d+", "age: 42")  # Match : "42"

Syntaxe de base

Classes de caractères

.
Tout caractèreSauf saut de ligne (sans DOTALL)
\d
ChiffreÉquivaut à [0-9]
\D
Non-chiffreÉquivaut à [^0-9]
\w
Caractère de motLettres, chiffres, underscore
\W
Non-motTout sauf [a-zA-Z0-9_]
\s
Espace blancEspace, tab, saut de ligne…
[abc]
Classe customUn parmi a, b ou c
[^abc]
NégationTout sauf a, b et c
[a-z]
PlageDe a à z (minuscules)
[A-Za-z]
Plages combinéesToute lettre
[0-9a-f]
HexadécimalUn chiffre hexa
[\w.\-]
Classe avec métasLe . et - sont littéraux ici

Ancres

^
Début de chaîneDébut de ligne
$
Fin de chaîneFin de ligne
\b
Frontière de motEntre \w et \W
\B
Non-frontièreÀ l'intérieur d'un mot
\A
Début absoluJamais affecté par MULTILINE
\Z
Fin absolueJamais affectée par MULTILINE

Quantificateurs

*
0 ou plusGourmand (prend le maximum)
+
1 ou plusGourmand
?
0 ou 1Rend l'élément optionnel
{n}
Exactement n\d{4} = 4 chiffres
{n,m}
Entre n et m\d{2,4} = 2 à 4 chiffres
{n,}
Au moins n\w{3,} = 3+ lettres
*?
Non-gourmandPrend le minimum
+?
Non-gourmandS'arrête dès que possible
??
Non-gourmandPréfère 0 à 1
# Gourmand vs Non-gourmand : différence cruciale
texte = "<b>gras</b> et <i>italique</i>"

re.findall(r"<.+>",  texte)   # ["<b>gras</b> et <i>italique</i>"]  ← trop gourmand
re.findall(r"<.+?>", texte)   # ["<b>", "</b>", "<i>", "</i>"]   ← correct

# Alternatives avec le pipe |  — testées de gauche à droite
re.search(r"erreur|err|er", "erreur")  # "erreur" ← plus long en premier
re.search(r"er|err|erreur", "erreur")  # "er"     ← piège !

Fonctions re.*

re.match()

Cherche le pattern uniquement en début de chaîne. Retourne un Match object ou None.

re.match(r"\d+", "42 ans")   # "42"
re.match(r"\d+", "age: 42")  # None ← pas en début

re.search()

Cherche la première occurrence n'importe où. Retourne un Match object ou None.

re.search(r"\d+", "age: 42")   # "42"
re.search(r"\d+", "aucun chiffre") # None

re.findall()

Retourne une liste de toutes les occurrences. Si des groupes sont présents, retourne les captures.

re.findall(r"\d+", "3 chats, 12 chiens")
# ["3", "12"]

re.findall(r"(\w+)@(\w+)", "a@b c@d")
# [("a","b"), ("c","d")] ← tuples si 2+ groupes

re.finditer()

Retourne un itérateur de Match objects. Préférable sur de grands textes (économe en mémoire).

for m in re.finditer(r"\d+", "3 chats, 12 chiens"):
    print(m.group(), m.start(), m.end())
# "3"  0  1
# "12" 9  11

re.sub()

Remplace les occurrences. Accepte une fonction callable pour des remplacements dynamiques.

# Remplacement simple
re.sub(r"\d+", "N", "J'ai 3 chats")
# "J'ai N chats"

# Avec un callable
re.sub(r"\d+", lambda m: str(int(m.group())*2),
       "3 chats et 5 chiens")
# "6 chats et 10 chiens"

# Référence aux groupes dans le remplacement
re.sub(r"(\w+)@(\w+)", r"\1 [at] \2", "a@b")
# "a [at] b"

re.split()

Découpe la chaîne aux occurrences du pattern.

re.split(r"[,;|]", "a,b;c|d")
# ["a", "b", "c", "d"]

# Garder le délimiteur (groupe capturant)
re.split(r"([,;|])", "a,b;c")
# ["a", ",", "b", ";", "c"]

# Limiter le découpage
re.split(r",", "a,b,c,d", maxsplit=2)
# ["a", "b", "c,d"]

L'objet Match

m = re.search(r"(\d{2})/(\d{2})/(\d{4})", "Né le 15/03/1990")
if m:
    m.group()    # "15/03/1990"  ← match complet
    m.group(1)   # "15"
    m.group(2)   # "03"
    m.group(3)   # "1990"
    m.groups()   # ("15", "03", "1990")
    m.start()    # 7   ← position début
    m.end()      # 17  ← position fin
    m.span()     # (7, 17)

re.compile() — réutiliser un pattern

# Compiler une fois, utiliser plusieurs fois → performance
EMAIL_RE = re.compile(r"[\w.\-]+@[\w.\-]+\.\w{2,6}")

emails = EMAIL_RE.findall(texte_long)
valide = EMAIL_RE.match(user_input)

# re.compile() accepte aussi des flags
PATTERN = re.compile(r"\bbonjour\b", re.IGNORECASE | re.MULTILINE)

Groupes

Groupes de capture ( )

Les parenthèses capturent une partie du match, accessible via m.group(n).

m = re.search(r"(\w+)\s(\w+)", "Alice Dupont")
m.group(1)  # "Alice"
m.group(2)  # "Dupont"

Groupes nommés (?P<nom>)

Accéder aux captures par leur nom plutôt que par leur position.

m = re.search(
    r"(?P<jour>\d{2})/(?P<annee>\d{4})",
    "15/1990"
)
m.group("jour")   # "15"
m.group("annee")  # "1990"
m.groupdict()    # {"jour":"15","annee":"1990"}

Groupes non-capturants (?:)

Regrouper sans capturer — utile pour les quantificateurs et alternatives sans polluer les numéros de groupe.

# Avec capture : findall retourne le groupe
re.findall(r"(\d+)kg",   "3kg et 5kg")
# ["3", "5"]

# Non-capturant : findall retourne le match complet
re.findall(r"(?:\d+)kg", "3kg et 5kg")
# ["3kg", "5kg"]

# Alternative sans capturer
re.search(r"(?:chat|chien)s?", "des chiens")

Rétro-références \1, \2…

Référencer un groupe capturé dans le même pattern — utile pour détecter les répétitions.

# Détecter un mot dupliqué
re.search(r"\b(\w+)\s+\1\b", "le le problème")
# Match : "le le"

# Balise ouvrante = balise fermante
re.search(r"<(\w+)>.*?</\1>", "<b>texte</b>")
# Match : "<b>texte</b>"

# Dans re.sub() : \1 dans le remplacement
re.sub(r"(\w+) \1", r"\1", "le le problème")
# "le problème"

Flags

Les flags modifient le comportement du moteur. Ils se passent en 3e argument ou dans re.compile(), combinables avec |.

FlagAbrév.InlineEffet
re.IGNORECASEre.I(?i)Insensible à la casse. A matche a et A.
re.MULTILINEre.M(?m)^ et $ matchent chaque début/fin de ligne.
re.DOTALLre.S(?s)Le . matche aussi les sauts de ligne \n.
re.VERBOSEre.X(?x)Espaces et # ignorés dans le pattern → regex documentées.
re.ASCIIre.A(?a)\w, \d, \s limités aux caractères ASCII.

re.VERBOSE — écrire des regex documentées

# ✗ Sans VERBOSE : illisible
EMAIL_RE = re.compile(r"^[\w.\-]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z]{2,6})+$")

# ✓ Avec VERBOSE : chaque partie est expliquée
EMAIL_RE = re.compile(r"""
    ^                       # début de la chaîne
    [\w.\-]+                # partie locale : lettres, chiffres, points, tirets
    @                       # arobase obligatoire
    [a-zA-Z0-9\-]+         # domaine principal
    (?:\.[a-zA-Z]{2,6})+   # extension(s) : .com, .co.uk, etc.
    $                       # fin de la chaîne
""", re.VERBOSE)

# ⚠️  En mode VERBOSE, pour matcher un espace utiliser \s ou [ ] ou \x20

# Combiner plusieurs flags
PATTERN = re.compile(r"motif", re.VERBOSE | re.IGNORECASE | re.MULTILINE)

Concepts avancés

Lookahead et Lookbehind (assertions de voisinage)

Ces constructions vérifient ce qui précède ou suit un match sans l'inclure dans le résultat.

(?=...)
Lookahead positifSuivi par... sans capturer
(?!...)
Lookahead négatifNon suivi par...
(?<=...)
Lookbehind positifPrécédé par... (longueur fixe)
(?<!...)
Lookbehind négatifNon précédé par...
# Lookahead positif : chiffres suivis de € (sans capturer €)
re.findall(r"\d+(?=€)", "Coûte 42€ ou 15€")   # ["42", "15"]

# Lookahead négatif : lignes de diff ajoutées (pas les en-têtes +++)
re.findall(r"^\+(?!\+\+).+", diff, re.MULTILINE)

# Lookbehind positif : valeur après "total: " (sans capturer le préfixe)
re.findall(r"(?<=total:\s)\d+", "total: 987 articles")  # ["987"]

# Lookbehind négatif : chiffres non précédés de #
re.findall(r"(?<!#)\b\d+\b", "id: 42, couleur: #ff0000")  # ["42"]

re.sub() avec un callable — transformations dynamiques

# Normaliser des dates vers ISO 8601
def to_iso(m):
    j, mo, a = m.group(1), m.group(2), m.group(3)
    return f"{a}-{mo}-{j}"

re.sub(r"(\d{2})/(\d{2})/(\d{4})", to_iso, "Né le 15/03/1990")
# "Né le 1990-03-15"

# Compteur stateful via une closure (dict mutable)
compteurs = {"h1": 0, "h2": 0}

def numeroter(m):
    niveau = len(m.group(1))  # nombre de #
    if niveau == 1:
        compteurs["h1"] += 1; compteurs["h2"] = 0
        return f"# {compteurs['h1']}. {m.group(2)}"
    else:
        compteurs["h2"] += 1
        return f"## {compteurs['h1']}.{compteurs['h2']}. {m.group(2)}"

re.sub(r"^(#{1,2})\s+(.+)", numeroter, texte, flags=re.MULTILINE)

Texte multi-lignes avec re.DOTALL

markdown = """```python
def hello():
    print("Hello")
```"""

# Sans DOTALL : . ne capture pas \n → rate les blocs multi-lignes
re.findall(r"```(\w*)\n(.+)```", markdown)          # []

# Avec DOTALL + non-gourmand : capture tout le bloc
re.findall(r"```(\w*)\n(.*?)```", markdown, re.DOTALL)
# [("python", 'def hello():\n    print("Hello")\n')]

Cas pratiques

✓ Bonne pratique

Toujours utiliser les raw strings r""

Le \ a une signification en Python (\n, \t…) ET en regex. Le raw string désactive l'interprétation Python et évite les bugs silencieux.

# ✗ Ambigu
re.search("\d+", texte)   # \d interprété par Python d'abord
# ✓ Toujours
re.search(r"\d+", texte)
✓ Bonne pratique

Compiler les patterns réutilisés

Si le même pattern est utilisé dans une boucle, compiler avec re.compile() évite de le recompiler à chaque itération.

# ✗ Recompile à chaque tour
for ligne in lignes:
    re.search(r"\d{4}-\d{2}-\d{2}", ligne)

# ✓ Compilé une seule fois
DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")
for ligne in lignes:
    DATE_RE.search(ligne)
✓ Bonne pratique

Documenter les regex complexes

Une regex de plus de 40 caractères mérite re.VERBOSE et des commentaires. La lisibilité a plus de valeur que la concision.

PHONE_RE = re.compile(r"""
    ^0              # commence par 0
    \d              # chiffre opérateur
    (?:[. ]?\d{2})  # groupes de 2 chiffres
    {4}             # 4 fois
    $               # fin exacte
""", re.VERBOSE)
✓ Bonne pratique

Tester le retour de match/search

Ces fonctions retournent None si aucun match. Toujours tester avant d'appeler .group(), sous peine d'un AttributeError.

# ✗ AttributeError si None
email = re.search(r"[\w.]+@[\w.]+", texte).group()

# ✓ Sécurisé
m = re.search(r"[\w.]+@[\w.]+", texte)
if m:
    email = m.group()
✗ À éviter

Utiliser .* sans délimiteur clair

.* est gourmand et peut capturer bien plus que prévu. Préférer .*? ou une classe négative comme [^>]+.

# ✗ Capture tout entre le 1er < et le DERNIER >
re.findall(r"<.+>", "<a>X</a><b>Y</b>")
# ["<a>X</a><b>Y</b>"]  ← trop gourmand !
# ✓ Non-gourmand
re.findall(r"<.+?>", "<a>X</a><b>Y</b>")
# ["<a>", "</a>", "<b>", "</b>"]  ← correct
✗ À éviter

Oublier les frontières \b

Sans \b, un pattern peut matcher à l'intérieur d'un mot et produire des faux positifs difficiles à détecter.

# ✗ "ERROR" matche dans "CRITICAL_ERROR"
re.search(r"ERROR", "CRITICAL_ERROR")   # Match !
# ✓ Avec frontière de mot
re.search(r"\bERROR\b", "CRITICAL_ERROR") # None
re.search(r"\bERROR\b", "Code ERROR 404")  # Match
✗ À éviter

Ordre des alternatives

Python teste de gauche à droite et s'arrête au premier match. Toujours placer les alternatives plus longues en premier.

# ✗ "er" matche avant "erreur"
re.search(r"er|err|erreur", "erreur")  # "er" !
# ✓ Du plus long au plus court
re.search(r"erreur|err|er", "erreur")  # "erreur"
✗ À éviter

Parser du HTML avec des regex

Le HTML est récursif et irrégulier. Une regex cassera sur des cas réels (attributs imbriqués, commentaires, CDATA…).

# ✗ Fragile sur du vrai HTML
re.findall(r'href="(.*?)"', html)

# ✓ Utiliser BeautifulSoup
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
liens = [a["href"] for a in soup.find_all("a", href=True)]

Pièges courants

⚠️ Le point ne matche pas les sauts de ligne par défaut

Le . matche tout sauf \n. Utiliser re.DOTALL ou [\s\S] pour inclure les sauts de ligne.

texte = "ligne1\nligne2"
re.match(r".+", texte).group()               # "ligne1" ← s'arrête au \n
re.match(r".+", texte, re.DOTALL).group()    # "ligne1\nligne2"
⚠️ re.match() n'ancre pas la fin de chaîne

re.match() ancre le début mais PAS la fin. Un pattern peut matcher un préfixe valide d'une chaîne invalide. Ajouter $ pour valider complètement.

# ✗ Matche "123" dans "123abc" !
re.match(r"\d+", "123abc")   # Match : "123"
# ✓ Ancrer la fin
re.match(r"^\d+$", "123abc") # None
re.match(r"^\d+$", "123")    # Match
⚠️ findall() avec groupes retourne les captures, pas les matches

Dès qu'il y a un groupe (), findall() retourne les contenus capturés. Utiliser (?:) si on veut les matches complets.

re.findall(r"\d+kg",    "3kg et 5kg")  # ["3kg","5kg"]  ← complets
re.findall(r"(\d+)kg",  "3kg et 5kg")  # ["3","5"]      ← groupes
re.findall(r"(?:\d+)kg","3kg et 5kg")  # ["3kg","5kg"]  ← non-capturant
⚠️ {3,6} accepte aussi 4 et 5

Pour valider exactement 3 ou 6 caractères (pas 4 ni 5), utiliser une alternative explicite {6}|{3}, pas {3,6}.

# Couleur hex : 3 OU 6 chiffres hex (pas 4 ni 5)
re.match(r"^#[0-9a-fA-F]{3,6}$", "#abc12")   # Match ! ← faux positif (5 chars)
# ✓ Alternative explicite
re.match(r"^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$", "#abc12")  # None
re.match(r"^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$", "#abc")    # Match
⚠️ Les caractères spéciaux dans les classes [ ]

À l'intérieur de [...], la plupart des métacaractères sont littéraux. Le - doit être en premier ou dernier pour être littéral.

# Le . dans une classe = littéralement un point
re.match(r"^[\w.@-]+$", "alice.bob@mail.fr")  # ✓ Match
# ✓ - en dernier = littéral
re.match(r"[a-z-]",  "-")  # Match
# ⚠️  - au milieu entre deux classes = plage de caractères ASCII
re.match(r"[a-zA-Z]", "B") # Match (plage a-z et A-Z)

Testeur interactif

Testez vos patterns en temps réel. Les occurrences trouvées sont surlignées en vert.

PATTERN
FLAGS
TEXTE

Cheat Sheet

Classes de caractères
.Tout caractère (sauf \n sans DOTALL)a.c → "abc", "axc"
\d / \DChiffre / Non-chiffre\d{4} → "2025"
\w / \WCaractère de mot / non-mot\w+ → un mot entier
\s / \SEspace blanc / non-blanc\s+ → séparateur
[abc]Un parmi les caractères listés[aeiou] → une voyelle
[^abc]Tout sauf les caractères listés[^\n]+ → ligne entière
[a-z]Plage de caractères[A-Za-z0-9]
Ancres
^ / $Début / fin de chaîne (ou ligne avec M)^\d+$ → ligne de chiffres
\b / \BFrontière / non-frontière de mot\bcat\b ≠ "catalog"
\A / \ZDébut / fin absolus (ignorent MULTILINE)\A = vrai début
Quantificateurs
* / *?0+ gourmand / non-gourmand<.*?> → balise seule
+ / +?1+ gourmand / non-gourmand\w+ → un mot
? / ??0 ou 1 gourmand / non-gourmandcolou?r → color/colour
{n} / {n,m}Exactement n / entre n et m\d{4} → année
Groupes et alternatives
(...)Groupe capturant → m.group(n)(\d+)° → chiffres
(?:...)Groupe non-capturant(?:ab)+ → "ababab"
(?P<nom>...)Groupe nommé → m.group("nom")(?P<annee>\d{4})
\1, \2Rétro-référence au groupe n(\w+) \1 → doublon
a|bAlternative (pipe) — gauche à droitechat|chien
Assertions de voisinage (lookaround)
(?=...)Lookahead positif — suivi par...\d+(?=€) → prix
(?!...)Lookahead négatif — non suivi par...\d+(?!€) → non-prix
(?<=...)Lookbehind positif — précédé par...(?<=@)\w+
(?<!...)Lookbehind négatif — non précédé par...(?<!#)\d+
Fonctions du module re
re.match(p, s)Match en début de chaîne → Match | Nonevalider un format
re.search(p, s)Première occurrence → Match | Nonedétecter une présence
re.findall(p, s)Toutes les occurrences → listextraire des données
re.finditer(p, s)Itérateur de Match objectsgrands textes
re.sub(p, r, s)Remplacer → str (r = str ou callable)transformer
re.split(p, s)Découper → listdélimiteurs multiples
re.compile(p, f)Compiler → pattern réutilisableperformance en boucle
Flags
re.I / re.IGNORECASEInsensible à la casse(?i) inline
re.M / re.MULTILINE^ et $ sur chaque ligne(?m) inline
re.S / re.DOTALL. matche aussi \n(?s) inline
re.X / re.VERBOSEEspaces et # ignorés → commentaires(?x) inline