Expressions
Régulières
Syntaxe regex, groupes de capture, assertions, flags et module re Python — guide complet avec testeur interactif.
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
Ancres
Quantificateurs
# 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 |.
| Flag | Abrév. | Inline | Effet |
|---|---|---|---|
| re.IGNORECASE | re.I | (?i) | Insensible à la casse. A matche a et A. |
| re.MULTILINE | re.M | (?m) | ^ et $ matchent chaque début/fin de ligne. |
| re.DOTALL | re.S | (?s) | Le . matche aussi les sauts de ligne \n. |
| re.VERBOSE | re.X | (?x) | Espaces et # ignorés dans le pattern → regex documentées. |
| re.ASCII | re.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 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
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)
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)
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)
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()
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
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
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"
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 . 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() 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
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
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
À 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.