Développement Web

Les regex en Python avec le module re

14 دقائق للقراءة

Python est sans doute le langage où l’on dégaine les regex le plus souvent : nettoyage de données, scripts d’administration, scraping, traitement de logs. Le module re est dans la bibliothèque standard — rien à installer. Dans ce tutoriel, on reconstruit l’analyseur de logs de Téranga Livraison en Python, et on découvre au passage les pièges propres au langage, à commencer par le plus sournois : les chaînes brutes.

📍 Article principal du parcours : Maîtriser les expressions régulières : le guide complet
Ce tutoriel fait partie du parcours « Expressions régulières ». Commencez par le guide pour la vue d’ensemble.

🎯 Ce que vous allez apprendre

  • Pourquoi écrire vos motifs en chaînes brutes r"..." et ce qui casse sans elles.
  • Distinguer re.match, re.search et re.fullmatch — la confusion classique du débutant Python.
  • Choisir entre re.findall et re.finditer, et exploiter l’objet Match (group, groupdict).
  • Compiler un motif avec re.compile, activer les flags re.I, re.M, re.S, re.X.
  • Transformer du texte avec re.sub et re.split, et reconstruire l’analyseur de logs.

🛠️ Ce que vous allez construire

Une fonction analyser(lignes) qui renvoie une liste de dictionnaires {"ip":..., "methode":..., "chemin":..., "statut":...} — la jumelle Python de l’analyseur JavaScript du parcours, prête à alimenter un script de statistiques.

Prérequis

  • Python installé (au moment d’écrire, la version 3.13 ou la toute récente 3.14). Le module re est inclus.
  • Avoir suivi Groupes, captures et alternation (notez la syntaxe (?P<nom>...) propre à Python).
  • Niveau : intermédiaire. ⏱️ ~40 minutes.

Étape 1 — Le module re et les chaînes brutes

Avant tout motif, une règle d’or : écrivez vos regex en chaîne brute, préfixée par r. Sans le r, Python interprète d’abord les séquences d’échappement de la chaîne avant que le moteur regex ne les voie — et \b, \d ou \1 peuvent être transformés ou provoquer un avertissement. La chaîne brute désactive cette interprétation : ce que vous écrivez est exactement ce que le moteur reçoit.

import re

# FRAGILE : \b est interprété par Python comme un caractère d'effacement
motif_faux = "\bCMD\b"

# CORRECT : la chaîne brute passe \b tel quel au moteur regex
motif = r"\bCMD\b"

Cette habitude vous épargnera des bugs incompréhensibles où un motif « pourtant correct » ne trouve rien. Prenez le réflexe : toute regex en Python s’écrit r"...". Les éditeurs et les linters vous le rappelleront, mais autant l’ancrer dès maintenant.

Point d’étape — Dans un interpréteur, comparez len("\bCMD") et len(r"\bCMD"). Le premier vaut 4 (le \b est devenu un seul caractère), le second 5. Cette différence d’un caractère, c’est tout le problème que la chaîne brute résout.

Étape 2 — match, search, fullmatch

Trois fonctions cherchent une correspondance, et les confondre est l’erreur n°1 du débutant Python. re.match n’essaie qu’au début de la chaîne. re.search cherche n’importe où. re.fullmatch exige que le motif couvre toute la chaîne. Chacune renvoie un objet Match en cas de succès, ou None.

texte = "ref CMD-2026-00428 livrée"

re.match(r"CMD-\d{4}-\d{5}", texte)      # None  : la chaîne ne commence pas par CMD
re.search(r"CMD-\d{4}-\d{5}", texte)     # Match : trouvée au milieu
re.fullmatch(r"CMD-\d{4}-\d{5}", "CMD-2026-00428")  # Match : couvre tout

La règle pratique : utilisez search pour « trouver quelque part », fullmatch pour valider qu’une saisie entière respecte un format (un code de suivi, un numéro), et réservez match aux cas où l’ancrage au début est voulu. Beaucoup de bugs « ma regex ne trouve rien » viennent d’un re.match employé là où il fallait re.search.

Étape 3 — findall, finditer et l’objet Match

Pour récupérer toutes les correspondances, deux fonctions. re.findall renvoie une liste de chaînes — mais attention, dès qu’il y a des groupes, elle renvoie une liste de tuples (un par groupe), ce qui surprend. re.finditer, lui, renvoie un itérateur d’objets Match complets, ce qui est presque toujours préférable car on garde l’accès aux groupes nommés.

texte = "CMD-2026-00428 puis CMD-2025-00031"

# findall avec groupes → liste de tuples (déroutant)
re.findall(r"CMD-(\d{4})-(\d{5})", texte)
# → [('2026', '00428'), ('2025', '00031')]

# finditer → objets Match, accès par nom
for m in re.finditer(r"CMD-(?P<annee>\d{4})-(?P<seq>\d{5})", texte):
    print(m.group("annee"), m.group("seq"))
# 2026 00428
# 2025 00031

L’objet Match est riche : m.group(0) renvoie la correspondance entière, m.group("annee") un groupe nommé, m.groupdict() tous les groupes nommés sous forme de dictionnaire, et m.start()/m.end() les positions. Pour transformer chaque correspondance en enregistrement structuré, m.groupdict() est l’allié idéal — on s’en sert dans l’analyseur final.

Point d’étape — Exécutez la boucle finditer ci-dessus. Vous devez voir deux lignes. Si Python lève « no such group », vérifiez que vous avez bien écrit (?P<annee>...) avec le P — la syntaxe JavaScript (?<annee>...) est refusée ici.

Étape 4 — re.compile et les flags

Quand un motif est réutilisé, re.compile le compile une fois en un objet Pattern dont on appelle ensuite les méthodes. C’est plus rapide et plus lisible. C’est aussi là qu’on passe les flags, qui modifient le comportement du moteur.

motif = re.compile(r"^cmd-\d{4}", re.IGNORECASE | re.MULTILINE)

re.I  / re.IGNORECASE  → ignore la casse
re.M  / re.MULTILINE   → ^ et $ s'ancrent à chaque ligne
re.S  / re.DOTALL      → le point . reconnaît aussi le saut de ligne
re.X  / re.VERBOSE     → autorise espaces et commentaires dans le motif
re.A  / re.ASCII       → \w \d \b se limitent à l'ASCII

Le flag re.VERBOSE mérite un mot : il transforme un motif illisible en quelque chose de documenté. Les espaces et retours à la ligne y sont ignorés (sauf échappés ou en classe), et tout ce qui suit un # est un commentaire. Idéal pour un motif complexe :

ref = re.compile(r"""
    CMD-            # préfixe fixe
    (?P<annee>\d{4})  # année sur 4 chiffres
    -
    (?P<seq>\d{5})    # numéro de séquence sur 5 chiffres
""", re.VERBOSE)

On combine plusieurs flags avec l’opérateur |, comme re.I | re.M. Vous pouvez aussi les activer en ligne dans le motif avec (?im) au début, ou de façon locale avec (?i:...) sur une portion seulement.

Étape 5 — re.sub et re.split

Transformer du texte se fait avec re.sub (substitution) et le découpage avec re.split. Comme en JavaScript, le remplacement de re.sub peut être une chaîne — avec \g<nom> pour réinjecter un groupe nommé — ou une fonction appelée pour chaque correspondance.

# Réinjecter un groupe nommé
re.sub(r"CMD-(?P<annee>\d{4})-(?P<seq>\d{5})",
       r"commande \g<seq> de \g<annee>",
       "CMD-2026-00428")
# → "commande 00428 de 2026"

# Fonction de remplacement : formater un montant en FCFA
def espace_milliers(m):
    return f"{int(m.group()):,}".replace(",", " ")

re.sub(r"\d+(?= FCFA)", espace_milliers, "Total 1250000 FCFA")
# → "Total 1 250 000 FCFA"

# Découper sur des séparateurs incohérents
re.split(r"\s*[;,]\s*", "Awa Diallo;  Dakar ,coursier")
# → ['Awa Diallo', 'Dakar', 'coursier']

La fonction de remplacement reçoit l’objet Match et renvoie la chaîne de substitution — ici on formate l’entier avec un séparateur de milliers via le format :, puis on remplace la virgule par une espace, à la française. Le lookahead (?= FCFA), vu dans le tutoriel sur les assertions, garantit qu’on ne touche qu’aux montants.

Point d’étape — Lancez le re.sub avec espace_milliers sur « Total 1250000 FCFA ». Vous devez lire « Total 1 250 000 FCFA ». Si vous obtenez une virgule au lieu d’une espace, c’est que le .replace(",", " ") manque.

Étape 6 — Assembler l’analyseur (et un bonus 3.11)

Reconstruisons l’analyseur complet. On compile le motif de découpe en mode VERBOSE pour qu’il reste lisible, on parcourt les lignes avec search, et on convertit chaque Match en dictionnaire via groupdict().

import re

LIGNE = re.compile(r"""
    ^(?P<ip>\d{1,3}(?:\.\d{1,3}){3})   # adresse IP en tête
    .*\s"                              # tout jusqu'au premier guillemet
    (?P<methode>[A-Z]+)\s              # méthode HTTP
    (?P<chemin>/\S*)\s                 # chemin demandé
    HTTP/[\d.]+"\s
    (?P<statut>\d{3})                  # code de statut
""", re.VERBOSE)

def analyser(lignes):
    resultats = []
    for ligne in lignes:
        m = LIGNE.search(ligne)
        if m:
            resultats.append(m.groupdict())
    return resultats

journal = [
    '196.46.21.7 - - [28/May/2026:08:14:55 +0000] "GET /commande/428 HTTP/1.1" 200',
    '41.82.13.9 - - [28/May/2026:08:15:02 +0000] "POST /paiement HTTP/1.1" 500',
]
print(analyser(journal))
# [{'ip': '196.46.21.7', 'methode': 'GET', 'chemin': '/commande/428', 'statut': '200'},
#  {'ip': '41.82.13.9', 'methode': 'POST', 'chemin': '/paiement', 'statut': '500'}]

Bonus pour les versions modernes : depuis Python 3.11, le module re accepte enfin les quantificateurs possessifs (\d++, \d*+) et les groupes atomiques (?>...). Avant 3.11, il fallait passer par la bibliothèque tierce regex. Ces constructions empêchent le moteur de revenir en arrière une fois un segment consommé, ce qui élimine certaines explosions de temps de calcul sur des motifs mal formés (le fameux « catastrophic backtracking »). Pour un analyseur de logs simple, vous n’en aurez pas besoin tous les jours, mais c’est bon à connaître pour durcir un motif sensible.

Point d’étapeanalyser(journal) doit renvoyer une liste de deux dictionnaires complets. Si la liste est vide, vérifiez le mode re.VERBOSE : sans lui, les espaces et retours à la ligne de votre motif seraient pris au pied de la lettre et rien ne correspondrait.

Étape 7 — Anonymiser des données sensibles

Voici une tâche que tout script Python finit par rencontrer : partager un extrait de logs ou un export sans divulguer les coordonnées des clients. Téranga Livraison veut envoyer un échantillon à un prestataire, mais doit d’abord masquer les e-mails et les numéros de téléphone. re.sub avec une fonction de remplacement est exactement l’outil : on garde juste assez d’information pour reconnaître un enregistrement, on cache le reste.

import re

def anonymiser(texte):
    # Masquer l'e-mail en gardant la première lettre et le domaine
    texte = re.sub(r"(\w)[\w.+-]*(@[\w.-]+)",
                   r"\1***\2", texte)
    # Masquer un numéro en ne gardant que les deux derniers chiffres
    texte = re.sub(r"\+?\d[\d\s]{6,}(\d{2})",
                   lambda m: "+*** ** ** " + m.group(1), texte)
    return texte

ligne = "Awa Diallo, awa.diallo@example.sn, +221 77 123 45 67"
print(anonymiser(ligne))
# → "Awa Diallo, a***@example.sn, +*** ** ** 67"

Le premier re.sub capture la première lettre de l’adresse et son domaine, et remplace le reste par *** grâce aux rétroréférences \1 et \2. Le second emploie une fonction qui ne conserve que les deux derniers chiffres du numéro. Le résultat reste lisible — on voit qu’il y a un e-mail et un téléphone — sans exposer la donnée réelle. C’est une opération qu’on automatise une fois et qu’on réutilise sur chaque export, et elle illustre la puissance combinée des groupes, des rétroréférences et des fonctions de remplacement vues dans le parcours.

Un mot de prudence : un masquage par regex protège contre une divulgation accidentelle, pas contre un adversaire déterminé. Pour de vraies exigences de confidentialité, on supprime carrément le champ plutôt que de le masquer partiellement. Mais pour partager un échantillon de débogage entre collègues, cette approche est rapide, lisible et largement suffisante.

Point d’étape — Passez la ligne d’exemple dans anonymiser. Vous devez obtenir « a***@example.sn » et un numéro réduit à ses deux derniers chiffres. Si l’e-mail n’est pas masqué, vérifiez la classe [\w.+-]* qui doit avaler le reste de l’identifiant avant l’arobase.

🐞 Pièges fréquents

Symptôme Cause probable Correctif
Le motif ne trouve rien, sans erreur re.match employé là où il fallait re.search Utiliser re.search pour chercher n’importe où
SyntaxWarning: invalid escape sequence Motif écrit sans le préfixe r Toujours r"..." pour une regex
findall renvoie des tuples inattendus Le motif contient des groupes capturants Utiliser finditer, ou des groupes non capturants (?:...)
Le motif VERBOSE ne correspond plus Espaces du motif ignorés en mode re.X Échapper l’espace voulue (\ ) ou la mettre en classe [ ]

🌍 Adaptation au contexte ouest-africain

Python est parfait pour les scripts d’administration sur un petit VPS : analyser les logs Nginx d’une boutique, extraire les numéros mobile money d’un export, nettoyer un fichier client mal formaté. Le module re étant dans la bibliothèque standard, aucun téléchargement n’est nécessaire — un atout réel quand la bande passante coûte cher. Un script de quelques lignes, lancé en tâche planifiée la nuit, peut résumer les erreurs de la journée et vous épargner des heures de lecture manuelle.

✅ Récapitulatif

En Python, on écrit toujours ses motifs en chaîne brute r"...". On choisit re.search (n’importe où), re.match (au début) ou re.fullmatch (tout). Pour toutes les correspondances avec leurs groupes, re.finditer bat re.findall. re.compile compile un motif réutilisé et reçoit les flags re.I/M/S/X ; re.VERBOSE rend lisibles les gros motifs. re.sub et re.split transforment et découpent. Votre fonction analyser() renvoie une liste de dictionnaires propres.

🧾 Aide-mémoire

Élément Rôle
r"..." Chaîne brute : obligatoire pour les regex
re.search / match / fullmatch N’importe où / au début / toute la chaîne
re.findall / finditer Liste de résultats / itérateur d’objets Match
m.group("nom") / m.groupdict() Lire un groupe nommé / tous en dict
re.compile(p, re.I | re.M) Compiler avec flags
re.sub(p, repl, s) / re.split(p, s) Substituer / découper
(?P<nom>...) / (?P=nom) Groupe nommé / sa rétroréférence

💪 À vous de jouer

Écrivez une fonction erreurs_serveur(lignes) qui renvoie la liste des adresses IP ayant provoqué une erreur 5xx, sans doublon.

Voir une solution
import re

def erreurs_serveur(lignes):
    motif = re.compile(r'^(?P<ip>\d{1,3}(?:\.\d{1,3}){3}).*"\s5\d{2}\s*$')
    ips = set()
    for ligne in lignes:
        m = motif.search(ligne)
        if m:
            ips.add(m.group("ip"))
    return sorted(ips)

On capture l’IP en tête et on n’accepte la ligne que si elle se termine par un statut 5xx. Le set() élimine les doublons ; sorted() rend la sortie stable.

Tutoriels frères

Pour aller plus loin

FAQ

Q : Faut-il toujours re.compile ?
R : Non. Pour un usage ponctuel, les fonctions de module (re.search, etc.) suffisent : elles compilent et mettent en cache le motif en interne. re.compile brille quand vous réutilisez le même motif dans une boucle ou à travers un module — le code y gagne en clarté autant qu’en vitesse.

Q : Pourquoi findall me renvoie des tuples ?
R : Parce que votre motif contient plusieurs groupes capturants ; findall renvoie alors un tuple par correspondance. Si vous voulez la liste des correspondances entières, retirez les groupes ou rendez-les non capturants avec (?:...). Pour garder les groupes nommés, passez à finditer.

Q : Le module re suffit-il, ou faut-il regex ?
R : re couvre l’immense majorité des besoins. La bibliothèque tierce regex (sur PyPI) ajoute des fonctionnalités avancées — lookbehind de longueur variable, correspondance approximative — utiles dans des cas pointus. Depuis Python 3.11, l’écart s’est réduit avec l’arrivée des quantificateurs possessifs et des groupes atomiques dans re.

Mots-clés : regex Python, module re, re.search, re.findall, re.finditer, groupes nommés Python, (?P<nom>), re.VERBOSE, chaîne brute, re.sub.

مشاركة