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.searchetre.fullmatch— la confusion classique du débutant Python. - Choisir entre
re.findalletre.finditer, et exploiter l’objet Match (group,groupdict). - Compiler un motif avec
re.compile, activer les flagsre.I,re.M,re.S,re.X. - Transformer du texte avec
re.subetre.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
reest 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")etlen(r"\bCMD"). Le premier vaut 4 (le\best 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
finditerci-dessus. Vous devez voir deux lignes. Si Python lève « no such group », vérifiez que vous avez bien écrit(?P<annee>...)avec leP— 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.subavecespace_millierssur « 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’étape —
analyser(journal)doit renvoyer une liste de deux dictionnaires complets. Si la liste est vide, vérifiez le modere.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
- Les regex en JavaScript — le même analyseur, côté navigateur et Node.
- Groupes, captures et alternation — la syntaxe
(?P<nom>...)en détail.
Pour aller plus loin
- 🔝 Retour au guide : Maîtriser les expressions régulières : le guide complet
- Documentation officielle du module re sur docs.python.org (source primaire).
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.