41  Les expressions régulières (regex)

Les expressions régulières (ou regex) sont un mini-langage pour décrire des motifs de texte. Elles permettent de valider un email, extraire tous les numéros de téléphone d’un document, nettoyer un fichier mal formaté… Python expose cette puissance via le module standard re. Les regex sont un sujet majeur du niveau Expert TOSA.

🎯 Pourquoi c’est difficile (mais utile)

Les regex ont une syntaxe dense qu’on ne peut pas deviner : \d+, [^abc], (.+?)… Il faut les apprendre. La bonne nouvelle : une fois les bases acquises, c’est un outil utilisé toute votre vie de développeur.

41.1 Premier contact

Le module re fournit des fonctions pour chercher/manipuler des motifs dans des chaînes.

import re

texte = "Contactez-nous au 04 90 12 34 56 ou au 06 11 22 33 44."

# Chercher tous les numéros de téléphone (format XX XX XX XX XX)
numeros = re.findall(r"\d{2} \d{2} \d{2} \d{2} \d{2}", texte)
print(numeros)
['04 90 12 34 56', '06 11 22 33 44']

En 5 caractères spéciaux (\d{2}), on a extrait tous les numéros. C’est la puissance des regex.

Pourquoi le r"..." ?

Le préfixe r (raw string) désactive l’interprétation des \ par Python. Sans lui, "\d" serait interprété par Python avant d’arriver à regex, et certains motifs casseraient.

Règle absolue : toujours utiliser r"..." pour écrire un motif regex.

41.2 Les caractères de base

Caractères littéraux

Dans un motif, la plupart des caractères représentent eux-mêmes.

import re

print(re.findall(r"chat", "le chat et le chien"))
print(re.findall(r"python", "Python n'est pas python"))    # sensible à la casse
['chat']
['python']

Caractères spéciaux à connaître

Ces caractères ont un sens spécial et doivent être échappés avec \ pour être matchés littéralement :

. ^ $ * + ? { } [ ] ( ) \ |

Exemple : pour chercher un vrai point, il faut \. :

import re

print(re.findall(r"\.", "fin. début."))
['.', '.']

41.3 Les classes de caractères

Une classe représente un caractère appartenant à un ensemble.

Classes prédéfinies

Classe Sens
\d Un chiffre (0-9)
\D Un non-chiffre
\w Un caractère de mot : lettre, chiffre, underscore
\W Un non-mot
\s Un espace (espace, tabulation, newline…)
\S Un non-espace
. N’importe quel caractère (sauf \n par défaut)
import re

texte = "Alice a 30 ans, Bob en a 25."

print(re.findall(r"\d", texte))       # chiffres isolés
print(re.findall(r"\w+", texte))      # mots entiers
['3', '0', '2', '5']
['Alice', 'a', '30', 'ans', 'Bob', 'en', 'a', '25']

Classes personnalisées avec [...]

import re

# Un caractère parmi a, e, i, o, u
print(re.findall(r"[aeiou]", "bonjour"))

# Une plage : a-z, A-Z, 0-9
print(re.findall(r"[A-Z]", "Alice et Bob ont 30 ans"))

# Négation avec ^
print(re.findall(r"[^aeiou ]", "bonjour"))    # ni voyelles ni espaces
['o', 'o', 'u']
['A', 'B']
['b', 'n', 'j', 'r']
Dans une classe [...], beaucoup de caractères perdent leur sens spécial

[.+] matche un vrai point ou un vrai plus, pas « n’importe quoi ou 1+ ».

Seuls quelques caractères restent spéciaux dans [...] : ], \, -, ^.

41.4 Les quantificateurs

Les quantificateurs indiquent combien de fois un motif peut se répéter.

Quantificateur Sens
* 0 ou plus
+ 1 ou plus
? 0 ou 1 (optionnel)
{n} Exactement n fois
{n,} Au moins n
{n,m} Entre n et m
import re

# 1 ou plusieurs chiffres
print(re.findall(r"\d+", "abc 123 def 4567"))

# Exactement 5 chiffres (code postal)
print(re.findall(r"\d{5}", "zip 75001 ou 69002 (mais pas 999)"))

# Entre 2 et 4 chiffres
print(re.findall(r"\d{2,4}", "1 22 333 4444 55555"))

# "Ah" avec 0 ou plus de "h" supplémentaires
print(re.findall(r"Ahh*", "Ah Ahh Ahhh Aaah"))
['123', '4567']
['75001', '69002']
['22', '333', '4444', '5555']
['Ah', 'Ahh', 'Ahhh']

Gourmandise : *?, +?, ??

Par défaut, les quantificateurs sont gourmands : ils matchent le maximum possible. Pour forcer une matching minimale, on ajoute un ?.

import re

texte = "<b>gras</b> et <i>italique</i>"

# Gourmand : capture TOUT jusqu'au dernier >
print(re.findall(r"<.+>", texte))

# Paresseux : s'arrête au premier >
print(re.findall(r"<.+?>", texte))
['<b>gras</b> et <i>italique</i>']
['<b>', '</b>', '<i>', '</i>']
⚠️ Piège TOSA classique

.+ (gourmand) vs .+? (paresseux) est une question fréquemment posée. Retenez : le ? rend le quantificateur paresseux.

41.5 Ancres

Les ancres ne matchent pas des caractères, mais des positions.

Ancre Position
^ Début de chaîne (ou ligne en mode multiligne)
$ Fin de chaîne (ou ligne)
\b Frontière de mot
\B Non-frontière de mot
import re

# Doit commencer par une majuscule
print(re.findall(r"^[A-Z]\w+", "Alice est là"))
print(re.findall(r"^[A-Z]\w+", "alice est là"))  # rien

# Mot entier "chat" (pas "chats" ni "chaton")
print(re.findall(r"\bchat\b", "chat chats chaton"))

# Mot qui finit par "-tion"
print(re.findall(r"\w+tion\b", "action réaction autonomie direction"))
['Alice']
[]
['chat']
['action', 'réaction', 'direction']

41.6 Groupes avec (...)

Les parenthèses capturent une portion du match.

import re

texte = "Alice a 30 ans, Bob en a 25."

# Sans groupes : on obtient la correspondance complète
print(re.findall(r"\w+ a \d+", texte))

# Avec groupes : on obtient des tuples
print(re.findall(r"(\w+) a (\d+)", texte))
['Alice a 30', 'en a 25']
[('Alice', '30'), ('en', '25')]

Chaque (...) crée un groupe capturé, accessible par son indice (1, 2, …).

Groupes non capturants : (?:...)

Si on veut juste grouper sans capturer, on utilise (?:...) :

import re

# Grouper pour l'alternative, sans capturer
texte = "J'aime les chats et les chiens"
print(re.findall(r"(?:chat|chien)s?", texte))
['chats', 'chiens']

Alternative : |

Le pipe | signifie « ou » dans un motif.

import re

print(re.findall(r"chat|chien", "chat chien poisson"))
print(re.findall(r"ro(uge|se)", "rouge rose noir"))    # capture la partie variable
['chat', 'chien']
['uge', 'se']

41.7 Les fonctions principales de re

re.search — trouver la première occurrence

Renvoie un objet Match ou None.

import re

texte = "Alice a 30 ans"
match = re.search(r"\d+", texte)

if match:
    print(f"Trouvé : '{match.group()}'")
    print(f"Position : {match.start()}-{match.end()}")
else:
    print("Rien")
Trouvé : '30'
Position : 8-10

re.match — ancrage au début

Similaire à search, mais uniquement au début de la chaîne.

import re

print(re.match(r"\d+", "123abc"))       # OK
print(re.match(r"\d+", "abc123"))       # None : ne commence pas par chiffre
<re.Match object; span=(0, 3), match='123'>
None
match vs search
  • re.match(r"motif", s)re.search(r"^motif", s).
  • En pratique, search est plus fréquent. Utilisez match seulement si vous voulez explicitement l’ancre au début.

re.findall — toutes les occurrences

Renvoie une liste. Si le motif contient des groupes, renvoie les tuples des groupes.

import re

# Sans groupes : liste de chaînes
print(re.findall(r"\d+", "a1 b22 c333"))

# Avec groupes : liste de tuples
print(re.findall(r"(\w+)=(\d+)", "age=30 taille=180 poids=75"))
['1', '22', '333']
[('age', '30'), ('taille', '180'), ('poids', '75')]

re.finditer — itérateur sur les matches

Utile quand on veut les positions ou les détails des matches :

import re

texte = "Alice a 30, Bob a 25, Charlie a 28."

for m in re.finditer(r"(\w+) a (\d+)", texte):
    print(f"'{m.group(0)}' à la position {m.start()} — nom={m.group(1)}, age={m.group(2)}")
'Alice a 30' à la position 0 — nom=Alice, age=30
'Bob a 25' à la position 12 — nom=Bob, age=25
'Charlie a 28' à la position 22 — nom=Charlie, age=28

re.sub — substitution

Remplace les matches par une chaîne.

import re

texte = "Mon tél : 01-23-45-67-89"

# Remplacer les tirets par des espaces
print(re.sub(r"-", " ", texte))

# Remplacer par une fonction (plus puissant)
def anonymiser(m):
    return "X" * len(m.group())

print(re.sub(r"\d", anonymiser, "Mon tél : 04 90 12 34 56"))
Mon tél : 01 23 45 67 89
Mon tél : XX XX XX XX XX

Référence aux groupes dans la substitution

On peut référencer les groupes capturés dans la chaîne de remplacement avec \1, \2, etc.

import re

# Inverser prénom et nom
texte = "Marie Curie, Isaac Newton, Albert Einstein"
print(re.sub(r"(\w+) (\w+)", r"\2 \1", texte))
Curie Marie, Newton Isaac, Einstein Albert

re.split — découper selon un motif

import re

# Découper selon un ou plusieurs espaces/tabulations
texte = "mot1    mot2\tmot3  mot4"
print(re.split(r"\s+", texte))

# Découper selon plusieurs séparateurs
print(re.split(r"[,;:]", "a,b;c:d"))
['mot1', 'mot2', 'mot3', 'mot4']
['a', 'b', 'c', 'd']

41.8 Compiler un motif

Pour un motif réutilisé plusieurs fois, on peut le compiler avec re.compile — c’est plus rapide.

import re

motif = re.compile(r"\d+")

print(motif.findall("abc 123 def 456"))
print(motif.search("abc 789"))
print(motif.sub("X", "a1 b22 c333"))
['123', '456']
<re.Match object; span=(4, 7), match='789'>
aX bX cX

Les objets Pattern ont les mêmes méthodes (findall, search, sub…) que le module re.

41.9 Drapeaux (flags)

Les drapeaux modifient le comportement de la regex.

Drapeau Effet
re.IGNORECASE / re.I Insensible à la casse
re.MULTILINE / re.M ^ et $ matchent aussi les débuts/fins de ligne
re.DOTALL / re.S . matche aussi \n
re.VERBOSE / re.X Autorise des espaces et commentaires dans le motif
import re

# Insensible à la casse
print(re.findall(r"python", "Python PYTHON python", flags=re.IGNORECASE))

# Multiligne : ^ et $ par ligne
texte = "Alice 30\nBob 25\nCharlie 28"
print(re.findall(r"^\w+", texte, flags=re.MULTILINE))
['Python', 'PYTHON', 'python']
['Alice', 'Bob', 'Charlie']

41.10 Exemples pratiques

Valider un email

import re

EMAIL_REGEX = re.compile(r"^[\w.+-]+@[\w-]+\.[\w.-]+$")

def est_email_valide(adresse):
    return bool(EMAIL_REGEX.match(adresse))

for test in ["alice@example.com", "bob.smith@domain.fr", "pas_valide", "a@b"]:
    print(f"{test:<30}{est_email_valide(test)}")
alice@example.com              → True
bob.smith@domain.fr            → True
pas_valide                     → False
a@b                            → False
Validation d’email : un faux problème

La « vraie » regex pour valider un email selon la RFC 5322 fait plusieurs centaines de caractères. La regex ci-dessus est suffisante en pratique pour 99 % des cas, mais ne couvre pas tous les emails légaux (ex: adresses avec caractères internationaux).

Règle pragmatique : utilisez une regex simple pour filtrer les typos évidents, puis envoyez un email de confirmation.

Extraire des numéros de téléphone français

import re

texte = """
Mon numéro : 06 11 22 33 44
Bureau : 01.23.45.67.89
International : +33 6 78 90 12 34
Urgence : 112
"""

# Motif pour numéros français (format flexible)
motif = re.compile(r"(?:\+33\s?|0)\d(?:[\s.-]?\d{2}){4}")

numeros = motif.findall(texte)
print("Numéros trouvés :")
for n in numeros:
    print(f"  {n}")
Numéros trouvés :
  06 11 22 33 44
  01.23.45.67.89
  +33 6 78 90 12 34

Nettoyer du HTML

import re

html = "<p>Bonjour <b>Alice</b>, comment <i>ça va</i> ?</p>"

# Supprimer toutes les balises
texte_propre = re.sub(r"<[^>]+>", "", html)
print(texte_propre)
Bonjour Alice, comment ça va ?

Extraire des dates

import re

texte = "Réunion le 15/03/2026, puis le 22-04-2026 et enfin le 2026-05-10."

# Format flexible avec - ou /
motif = re.compile(r"\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}-\d{2}-\d{2}")

print(motif.findall(texte))
['15/03/2026', '22-04-2026', '2026-05-10']

41.11 Tester ses regex

Des outils en ligne facilitent l’apprentissage :

  • regex101.com : éditeur avec explication du motif.
  • regexr.com : similaire, avec bibliothèque de patterns.

Mettre son motif, voir ce qu’il match, c’est ultra-pédagogique. Utilisez ces outils pour apprendre.


🧩 Quiz 1.1 — Regex

Question 1

Que matche \d+ ?

  1. Un chiffre unique
  2. Un ou plusieurs chiffres
  3. Zéro ou plusieurs chiffres
  4. N’importe quel caractère

b) Un ou plusieurs chiffres+ signifie « au moins 1 ».

import re
print(re.findall(r"\d+", "abc 12 xyz 345"))
['12', '345']

Question 2

Quelle différence entre .+ et .+? ?

  1. Aucune
  2. Le premier est gourmand, le second paresseux
  3. Le second est gourmand
  4. .+? est invalide

b).+ match autant que possible (gourmand), .+? match le minimum (paresseux).

import re
texte = "<a><b>"
print(re.findall(r"<.+>", texte))    # gourmand : tout
print(re.findall(r"<.+?>", texte))   # paresseux : chaque balise
['<a><b>']
['<a>', '<b>']

Question 3

Pourquoi utiliser r"..." dans les regex ?

  1. Ça compile plus vite
  2. Pour désactiver l’interprétation des \ par Python
  3. C’est obligatoire syntaxiquement
  4. Pour rendre la regex insensible à la casse

b) — sans r, Python interprète les \ avant la regex, ce qui peut poser problème ("\n" = saut de ligne, r"\n" = les 2 caractères \n).

Question 4

Que matche [^abc] ?

  1. Les caractères a, b ou c
  2. Les caractères qui ne sont pas a, b ou c
  3. Le caractère ^
  4. Une erreur de syntaxe

b)^ au début d’une classe [...] signifie négation.

import re
print(re.findall(r"[^aeiou ]", "bonjour"))
['b', 'n', 'j', 'r']

Question 5

Que renvoie re.findall(r"(\w+)@(\w+)", "alice@gmail") ?

  1. ["alice", "gmail"]
  2. ["alice@gmail"]
  3. [("alice", "gmail")]
  4. None

c) [("alice", "gmail")] — quand le motif contient des groupes, findall retourne des tuples des groupes capturés.

import re
print(re.findall(r"(\w+)@(\w+)", "alice@gmail"))
[('alice', 'gmail')]

Question 6

Quelle est la différence entre re.match et re.search ?

  1. match cherche partout, search au début
  2. match ne fonctionne qu’avec les chaînes courtes
  3. match n’est disponible qu’à partir de Python 3.10
  4. match cherche au début, search n’importe où

d)match est implicitement ancré au début. search cherche n’importe où.

import re
print(re.match(r"\d+", "abc123"))     # None
print(re.search(r"\d+", "abc123"))    # match 123
None
<re.Match object; span=(3, 6), match='123'>

Question 7

Comment remplacer tous les espaces par des underscores dans "bonjour tout le monde" ?

  1. re.sub(r" ", "_", s)
  2. re.sub("_", " ", s)
  3. re.findall(r" ", s)
  4. s.split(" ")

a)re.sub(motif, remplacement, chaîne).

import re
print(re.sub(r" ", "_", "bonjour tout le monde"))
bonjour_tout_le_monde

Note : "bonjour tout le monde".replace(" ", "_") fait la même chose sans regex.

Question 8

Que matche ^\d{5}$ ?

  1. Un code postal français de 5 chiffres exactement
  2. 5 chiffres n’importe où
  3. Une erreur
  4. Des chiffres suivis d’espaces

a)^ et $ ancrent au début/fin, \d{5} exige exactement 5 chiffres. Une chaîne doit être uniquement 5 chiffres.

import re
motif = re.compile(r"^\d{5}$")
print(bool(motif.match("75001")))    # True
print(bool(motif.match("75001 Paris")))   # False
print(bool(motif.match("7500")))     # False (4 chiffres)
True
False
False

✏️ Exercice 1.1 — Valider des identifiants

Écrivez une fonction identifiant_valide(s) qui vérifie qu’une chaîne :

  • Commence par une lettre minuscule,
  • Contient uniquement des lettres minuscules, chiffres et underscores,
  • Fait entre 3 et 15 caractères.
import re

def identifiant_valide(s):
    ...

for test in ["alice", "bob_42", "A_Uppercase", "ab", "un_nom_vraiment_tres_long", "123bob"]:
    print(f"{test:<30}{identifiant_valide(test)}")
import re

MOTIF = re.compile(r"^[a-z][a-z0-9_]{2,14}$")

def identifiant_valide(s):
    return bool(MOTIF.match(s))


for test in ["alice", "bob_42", "A_Uppercase", "ab", "un_nom_vraiment_tres_long", "123bob", "valid_user"]:
    print(f"{test:<30}{identifiant_valide(test)}")
alice                          → True
bob_42                         → True
A_Uppercase                    → False
ab                             → False
un_nom_vraiment_tres_long      → False
123bob                         → False
valid_user                     → True
  • ^ : début de chaîne.
  • [a-z] : une lettre minuscule.
  • [a-z0-9_]{2,14} : 2 à 14 caractères autorisés (total 3-15 avec la première lettre).
  • $ : fin de chaîne.

Remarque : l’usage de re.compile est préférable car la fonction sera appelée souvent.


✏️ Exercice 1.2 — Extraire des URL

À partir d’un texte, extrayez toutes les URL commençant par http:// ou https://.

import re

texte = """
Visitez https://python.org pour la documentation,
et http://regex101.com pour tester vos regex.
Pas valide : ftp://exemple.com
"""

urls = ...
print(urls)
import re

texte = """
Visitez https://python.org pour la documentation,
et http://regex101.com pour tester vos regex.
Pas valide : ftp://exemple.com
"""

motif = re.compile(r"https?://\S+")

urls = motif.findall(texte)
print(urls)
['https://python.org', 'http://regex101.com']
  • https? : http suivi de s optionnel (le ? sur le s précédent).
  • :// : littéral.
  • \S+ : un ou plusieurs caractères non-espace (on s’arrête au premier espace).

Amélioration possible : retirer la ponctuation finale éventuelle (,, .) avec un motif plus fin :

import re

texte = "Voir https://python.org, puis https://exemple.fr."

motif = re.compile(r"https?://[\w.-]+(?:/\S*)?")

print(motif.findall(texte))
['https://python.org', 'https://exemple.fr.']

✏️ Exercice 1.3 — Anonymiser des téléphones

Remplacez tous les numéros de téléphone français dans un texte par XX XX XX XX XX.

import re

texte = """
Appelle-moi au 06 11 22 33 44 ou au 01-23-45-67-89.
En urgence : 112 (pas à remplacer).
Fixe : 04.90.12.34.56
"""

resultat = ...
print(resultat)
import re

texte = """
Appelle-moi au 06 11 22 33 44 ou au 01-23-45-67-89.
En urgence : 112 (pas à remplacer).
Fixe : 04.90.12.34.56
"""

# Numéro français = 10 chiffres, groupés 2 par 2, séparateurs variables
motif = re.compile(r"0\d(?:[\s.-]?\d{2}){4}")

resultat = motif.sub("XX XX XX XX XX", texte)
print(resultat)

Appelle-moi au XX XX XX XX XX ou au XX XX XX XX XX.
En urgence : 112 (pas à remplacer).
Fixe : XX XX XX XX XX
  • 0\d : premier groupe de 2 chiffres (commence toujours par 0).
  • (?:[\s.-]?\d{2}){4} : 4 fois :
    • [\s.-]? : séparateur optionnel (espace, point ou tiret).
    • \d{2} : 2 chiffres.
  • (?:...) : groupe non-capturant (on n’a pas besoin de récupérer les parties).

Le 112 n’est pas remplacé car il ne fait que 3 chiffres.


✏️ Exercice 1.4 — Compter les mots d’un texte

Écrivez une fonction compter_mots(texte) qui renvoie un dict {mot: occurrences}, en ignorant la ponctuation et la casse.

import re

def compter_mots(texte):
    ...

texte = "Le chat dort. Le chien court. Le chat et le chien jouent !"
print(compter_mots(texte))
import re
from collections import Counter

def compter_mots(texte):
    mots = re.findall(r"\b\w+\b", texte.lower())
    return dict(Counter(mots))


texte = "Le chat dort. Le chien court. Le chat et le chien jouent !"
result = compter_mots(texte)

# Affichage trié par occurrences décroissantes
for mot, n in sorted(result.items(), key=lambda t: t[1], reverse=True):
    print(f"  {mot:<10} {n}")
  le         4
  chat       2
  chien      2
  dort       1
  court      1
  et         1
  jouent     1
  • texte.lower() : uniformiser la casse.
  • \b\w+\b : un mot entier (lettres, chiffres, underscores).
  • Counter du module collections compte les occurrences — plus élégant qu’une boucle manuelle.

Cette fonction est la brique de base d’un analyseur de texte.


À retenir

Points clés du chapitre
  1. r"..." toujours pour les motifs regex.
  2. Classes : \d (chiffre), \w (mot), \s (espace), [a-z] (plage), [^...] (négation).
  3. Quantificateurs : * (0+), + (1+), ? (0 ou 1), {n}, {n,m}. Ajoutez ? pour rendre paresseux.
  4. Ancres : ^ (début), $ (fin), \b (frontière de mot).
  5. Groupes : (...) capture, (?:...) groupe sans capturer.
  6. Fonctions principales : search, match, findall, finditer, sub, split.
  7. re.compile() pour un motif réutilisé.
  8. Drapeaux : re.IGNORECASE, re.MULTILINE, re.DOTALL.
  9. regex101.com pour tester vos motifs.

← Retour à la présentation du niveauChapitre suivant : Itérateurs et générateurs →