Intermédiaire

Fichiers, Erreurs et Outils

Lire et écrire des fichiers CSV et JSON, gérer les erreurs, utiliser pip et les environnements virtuels, maîtriser les regex

[05]Module 5

Fichiers, CSV et JSON

Lire et écrire des fichiers texte, exploiter des inventaires CSV et des réponses d'API JSON.

open / withcsvjsonencoding
5.1 Lire et écrire des fichiers texte

La fonction open() ouvre un fichier en mode lecture ("r"), écriture ("w", écrase) ou ajout ("a"). Le bloc with garantit la fermeture automatique du fichier, même en cas d'erreur — c'est la forme à toujours privilégier :

with open("equipements.txt", "w", encoding="utf-8") as f:
    f.write("R1\nSW1\nSW2\n")

with open("equipements.txt", "r", encoding="utf-8") as f:
    for ligne in f:
        print("Hote :", ligne.strip())
Pourquoi c'est important : Inventaires d'équipements, exports d'alertes, journaux : tout transite par des fichiers texte. Un script qui ne sait pas les lire et les écrire proprement ne sert à rien en production.
L'analogie qui aide : Le fichier, c'est un cahier. open() l'ouvre, with est le surveillant qui le referme tout seul même si tu pars en courant (erreur). Sans lui, le cahier reste ouvert et se corrompt.

Précisez toujours encoding="utf-8" : sous Windows, l'encodage par défaut diffère et les accents peuvent être corrompus. Les méthodes utiles : .read() (tout le contenu), .readlines() (liste des lignes), .strip() (retire espaces et retours ligne).

Démonstration pas à pas : Trace :
open("equipements.txt", "w") → fichier vidé ou créé.
f.write("R1\nSW1\nSW2\n") → 3 lignes écrites.
Relecture en mode "r" : 1re itération ligne = « R1\n » → strip() → « R1 » → affiche « Hote : R1 ». Idem pour SW1 puis SW2. Fin : fichier fermé automatiquement.
Pièges classiques : Ouvrir en "w" écrase le fichier existant sans prévenir : utilise "a" pour ajouter. Oublier encoding="utf-8" corrompt les accents sous Windows. Ouvrir sans with et oublier close(). Utiliser un chemin relatif en croyant être dans le bon dossier.
À vous de jouer : Prédis ce qui est affiché à la relecture, puis vérifie :
with open("t.txt", "w", encoding="utf-8") as f:
f.write("A\nB\n")
with open("t.txt", encoding="utf-8") as f:
for ligne in f:
print("X :", ligne.strip())
Voir la réponse

Sortie :
X : A
X : B

Explication : strip() retire le retour ligne, chaque tour affiche la ligne nettoyée préfixée de « X : ».

Exercice 5.1

À faire : écrivez dans « alertes.txt » les lignes « ALERTE 5.6.7.8 » et « OK 10.0.0.1 », relisez le fichier et affichez chaque ligne sans les retours ligne superflus.
5.2 Fichiers CSV : inventaires et exports

Le format CSV (valeurs séparées par des virgules) est le standard des inventaires et exports. Le module csv gère les guillemets et séparateurs. csv.DictReader lit chaque ligne comme un dictionnaire indexé par les en-têtes :

import csv

with open("parc.csv", "w", encoding="utf-8", newline="") as f:
    w = csv.DictWriter(f, fieldnames=["hostname", "ip", "os"])
    w.writeheader()
    w.writerow({"hostname": "R1", "ip": "10.0.0.1", "os": "IOS"})

with open("parc.csv", encoding="utf-8", newline="") as f:
    for eq in csv.DictReader(f):
        print(f"{eq['hostname']} -> {eq['ip']} ({eq['os']})")
Pourquoi c'est important : Les inventaires de parc vivent dans des tableurs exportés en CSV. Savoir les lire en dictionnaires et les réécrire, c'est brancher ton script directement sur la réalité du terrain.
L'analogie qui aide : Le CSV, c'est un tableau Excel mis à plat en texte : la première ligne donne les noms des colonnes, chaque suivante est une fiche. DictReader te rend chaque fiche déjà rangée par colonne.

Notez newline="" à l'ouverture : sans lui, des lignes vides parasites apparaissent sous Windows. Pour écrire rapidement sans dictionnaires, utilisez csv.writer et sa méthode writerow().

Démonstration pas à pas : Trace : écriture → fichier avec en-tête hostname,ip,os + 1 ligne R1.
Relecture : 1re itération eq = {« hostname »: « R1 », « ip »: « 10.0.0.1 », « os »: « IOS »} → affiche « R1 -> 10.0.0.1 (IOS) ».
État final : une ligne traitée, aucune ligne vide parasite grâce à newline="".
Pièges classiques : Oublier newline="" crée des lignes vides sous Windows. Déclarer des fieldnames qui ne correspondent pas aux clés du writerow(). En France, Excel exporte parfois avec ; au lieu de , : il faut delimiter=";". Lire sans tenir compte de l'en-tête.
À vous de jouer : Sur un CSV de 3 lignes de données plus l'en-tête, combien de tours fait for eq in csv.DictReader(f) ? Vérifie en relisant la leçon.
Voir la réponse

Réponse : 3 tours.
Explication : DictReader consomme la première ligne comme en-têtes et ne la compte pas comme donnée.

Exercice 5.2

À faire : créez « soc.csv » avec les colonnes ip et tentatives (3 lignes dont 5.6.7.8 / 42), relisez-le avec DictReader et affichez les IP ayant plus de 10 tentatives.
5.3 JSON : le format des API

JSON est le format d'échange des API REST : il se mappe directement aux dictionnaires et listes Python. Le module json convertit dans les deux sens avec json.loads() (texte vers Python) et json.dumps() (Python vers texte) :

import json

reponse = '{"hostname": "R1", "interfaces": ["Gi0/0", "Gi0/1"], "up": true}'
donnees = json.loads(reponse)
print(donnees["hostname"], donnees["interfaces"][0])

print(json.dumps(donnees, indent=2, ensure_ascii=False))
Pourquoi c'est important : NetBox, les contrôleurs SDN, les plateformes SOC : tous parlent JSON. Si tu ne sais pas convertir une réponse JSON en dictionnaire, tu ne peux dialoguer avec aucune API moderne.
L'analogie qui aide : Le JSON, c'est le bon de commande standard que tous les magasins (API) acceptent : mêmes cases, mêmes formats, que tu sois en Python, en JavaScript ou en Go.

Correspondances à connaître : true/false/null deviennent True/False/None. Pour sauvegarder, utilisez json.dump(obj, f) directement dans un fichier ouvert en écriture, et json.load(f) pour relire.

Démonstration pas à pas : Trace : reponse = texte brut.
json.loads(reponse)true devient True, le texte devient dict : {« hostname »: « R1 », « interfaces »: [« Gi0/0 », « Gi0/1 »], « up »: True}.
donnees["interfaces"][0] → « Gi0/0 ».
json.dumps(..., indent=2) → re-sérialise en texte lisible avec indentation.
Pièges classiques : Le JSON strict exige des guillemets doubles : {'ip': 'x'} avec simples quotes échoue dans loads(). true/false/null (JSON) ne sont pas True/False/None (Python) : la conversion est automatique mais à connaître. Confondre dump/load (fichiers) et dumps/loads (chaînes). Oublier ensure_ascii=False pour garder les accents lisibles.
À vous de jouer : Prédis la sortie, puis vérifie :
import json
d = json.loads('{"ip": "9.9.9.9", "score": 85}')
print(f"{d['ip']} : score {d['score']}")
print(d["score"] + 1)
Voir la réponse

Sortie :
9.9.9.9 : score 85
86

Explication : loads() a produit un dict, la f-string l'affiche et le score entier 85 + 1 vaut 86.

Exercice 5.3

À faire : parsez la chaîne '{"ip": "9.9.9.9", "score": 85}' avec json.loads() et affichez « 9.9.9.9 : score 85 » avec une f-string.
[06]Module 6

Erreurs et exceptions

Anticiper les pannes : try/except, types d'erreurs courants, lever ses propres exceptions, journaliser.

try / exceptValueErrorraiselogging
6.1 Attraper les erreurs avec try / except

Un script d'automatisation rencontre des données sales, des fichiers absents, des équipements injoignables. Le bloc try/except capture l'erreur au lieu de planter. Le bloc finally s'exécute dans tous les cas (nettoyage) :

try:
    port = int("abc")
except ValueError:
    print("Port invalide : utilisation du port 22")
    port = 22
print("Port retenu :", port)
Pourquoi c'est important : Un inventaire absent, une saisie « 8080x », un équipement injoignable : en automatisation, l'imprévu est la norme. Sans try/except, le premier grain de sable plante tout le script de nuit.
L'analogie qui aide : C'est l'airbag : en conduite normale il ne sert à rien, mais quand la valeur « abc » percute int(), il absorbe le choc et le script continue sa route avec le port 22.

Capturez des exceptions précises (ValueError, FileNotFoundError, KeyError), jamais un except: nu qui masque les bugs. La variable as e récupère le message d'erreur pour le journal :

try:
    with open("inventaire.csv", encoding="utf-8") as f:
        print(f.read())
except FileNotFoundError as e:
    print(f"Fichier introuvable : {e}")
Démonstration pas à pas : Trace :
int("abc") → lève ValueError, le reste du bloc try est sauté.
Python cherche un except ValueError → trouvé → affiche « Port invalide », port = 22.
État final : port vaut 22, le print("Port retenu :", port) après le bloc s'exécute normalement.
Pièges classiques : Un except: nu attrape tout, y compris les fautes de frappe que tu voulais voir : capture toujours précis (ValueError, FileNotFoundError). Mettre trop de code dans le try masque l'origine de l'erreur. Oublier as e quand tu veux journaliser le message. Croire que except répare la donnée : il ne fait que gérer l'échec.
À vous de jouer : Prédis la sortie, puis vérifie :
try:
port = int("8080x")
except ValueError:
print("Conversion impossible")
port = 0
print(port)
Voir la réponse

Sortie :
Conversion impossible
0

Explication : int("8080x") lève ValueError, le bloc except s'exécute et force port à 0.

Exercice 6.1

À faire : écrivez un bloc qui convertit saisie = "8080x" en entier ; en cas de ValueError, affichez « Conversion impossible » et affectez 0 à port.
6.2 Les erreurs à connaître et la levée d'exceptions
  • SyntaxError : faute de frappe, deux-points ou parenthèse oubliés — détectée avant l'exécution.
  • NameError : variable inexistante ou mal orthographiée.
  • TypeError : opération entre types incompatibles (« 3 » + 4).
  • ValueError : bon type mais valeur inexploitable (int("abc")).
  • IndexError / KeyError : index ou clé inexistants dans une liste ou un dict.
  • FileNotFoundError : fichier absent — fréquent en automatisation.

Avec raise, une fonction signale elle-même un problème à son appelant au lieu de retourner une valeur ambiguë :

def prefix_valide(p):
    if not 0 <= p <= 32:
        raise ValueError(f"Prefixe invalide : {p}")
    return True
Pourquoi c'est important : Savoir lire une erreur en 5 secondes au lieu de 30 minutes, c'est ce qui sépare un débutant d'un opérationnel. Et savoir lever ses propres erreurs, c'est refuser tôt un port ou un préfixe invalide plutôt que de corrompre la suite.
L'analogie qui aide : C'est le contrôle qualité à l'entrée de l'usine : chaque défaut a son tampon (TypeError, ValueError, KeyError...). Avec raise, tu deviens toi-même le contrôleur qui appose le tampon.
Démonstration pas à pas : Trace : prefix_valide(24) → test 0 <= 24 <= 32 True → renvoie True, rien n'est levé.
prefix_valide(33) → test False → raise ValueError("Prefixe invalide : 33") → l'appelant doit attraper, sinon le script s'arrête avec ce message.
Pièges classiques : Confondre TypeError (mauvais type : « 3 » + 4) et ValueError (bon type, mauvaise valeur : int("abc")). Lever sans message explicite. Attraper SyntaxError à l'exécution : elle est détectée avant, à l'import. Retourner -1 ou None au lieu de lever : l'appelant croit que c'est un résultat valide.
À vous de jouer : Prédis la sortie, puis vérifie :
def verifier_port(p):
if not 1 <= p <= 65535:
raise ValueError("mauvais port")
return True
try:
verifier_port(99999)
except ValueError:
print("refuse")
print(verifier_port(80))
Voir la réponse

Sortie :
refuse
True

Explication : 99999 déclenche le raise attrapé par le except, puis 80 est valide et renvoie True.

Exercice 6.2

À faire : écrivez verifier_port(p) qui lève ValueError si p n'est pas entre 1 et 65535, sinon retourne True. Testez avec 80 puis 99999 dans un try/except.
6.3 Journaliser avec logging et assert

En production, on ne débugue pas avec des print() dispersés : le module logging horodate les messages et les classe par niveau (DEBUG, INFO, WARNING, ERROR). L'instruction assert vérifie une hypothèse pendant le développement :

import logging

logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s [%(levelname)s] %(message)s")
logging.info("Debut du scan du reseau 10.0.0.0/24")
logging.warning("SW2 injoignable : tentative 2/3")

prefix = 24
assert 0 <= prefix <= 32, "Prefixe hors limites"
Pourquoi c'est important : Un scan qui tourne à 3 heures du matin ne peut pas afficher des print() que personne ne regarde. logging horodate chaque étape dans un fichier, et assert fait exploser tôt les hypothèses fausses pendant le développement.
L'analogie qui aide : Les print(), ce sont des post-it volants. Le logging, c'est la boîte noire de l'avion : chaque événement est daté, classé (INFO, WARNING, ERROR) et retrouvable après l'incident.

Règle pratique : assert pour les erreurs de programmation (jamais pour valider des entrées utilisateur), raise + try/except pour les erreurs d'exécution prévisibles, logging pour tracer le déroulé du script.

Démonstration pas à pas : Trace : basicConfig(level=INFO) → seuil à INFO.
logging.info("Debut du scan...") → affiché (INFO >= INFO).
logging.warning("SW2 injoignable...") → affiché (WARNING > INFO).
assert 0 <= 24 <= 32 → condition True → rien ne se passe, le script continue.
Pièges classiques : Appeler basicConfig() après un premier log : la configuration est ignorée silencieusement. Lancer avec python -O désactive tous les assert. Utiliser assert pour valider une saisie utilisateur (utilise raise). Laisser le niveau DEBUG en production : les journaux explosent.
À vous de jouer : Ce script plante-t-il ? Vérifie mentalement puis au terminal :
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Scan termine : 12 hotes")
logging.warning("1 hote sans reponse")
assert 24 <= 32
print("fin")
Voir la réponse

Réponse : non, il affiche les deux logs puis fin.
Explication : les deux messages passent le seuil INFO, et l'assertion 24 <= 32 est vraie donc silencieuse.

Exercice 6.3

À faire : configurez logging au niveau INFO, journalisez « Scan terminé : 12 hôtes » en INFO et « 1 hôte sans réponse » en WARNING.
[07]Module 7

Modules, pip et environnements virtuels

Réutiliser du code : imports, bibliothèque standard utile, pip, venv et structuration d'un projet.

importpipvenvstdlib
7.1 Importer des modules et explorer la stdlib

Un module est un fichier Python que l'on réutilise avec import. La bibliothèque standard couvre déjà 80 % des besoins d'automatisation, sans rien installer :

import os
import sys
from collections import Counter
from datetime import datetime

print(datetime.now().strftime("%Y-%m-%d %H:%M"))
print(Counter(["OK", "FAIL", "OK"]).most_common(1))
Pourquoi c'est important : Compter des logs, horodater un rapport, valider un réseau : tout existe déjà dans la bibliothèque standard. La connaître, c'est éviter de réinventer des fonctions bugguées pendant des heures.
L'analogie qui aide : C'est le rayon d'outils déjà payés dans l'atelier : Counter la caisse de comptage, datetime l'horloge pointeuse, ipaddress le gabarit de mesure. Tu les empruntes avec import.
  • os / sys / pathlib : système, chemins, arguments en ligne de commande.
  • re : expressions régulières (module 8).
  • csv / json : données (module 5).
  • collections.Counter : comptages — parfait pour les logs.
  • datetime : horodatage des rapports.
  • socket / ipaddress : réseau (validation d'IP, résolution).
Démonstration pas à pas : Trace :
Counter(["OK", "FAIL", "OK"]) → {« OK »: 2, « FAIL »: 1}.
.most_common(1) → [(« OK », 2)] : le plus fréquent d'abord.
datetime.now().strftime("%Y-%m-%d %H:%M") → ex. « 2026-09-09 14:30 » selon l'heure actuelle.
Pièges classiques : Nommer ton fichier re.py ou csv.py : il masque le vrai module standard et tout casse. Faire from x import * qui écrase tes noms sans prévenir. Importer un paquet tiers sans l'avoir installé (ModuleNotFoundError). Tourner en rond avec des imports circulaires entre tes propres fichiers.
À vous de jouer : Prédis la sortie, puis vérifie :
from collections import Counter
print(Counter("abac").most_common(1))
Voir la réponse

Sortie : [('a', 2)].
Explication : « a » apparaît 2 fois, « b » et « c » une fois. Le plus fréquent est donc « a » avec 2.

Exercice 7.1

À faire : avec le module ipaddress, vérifiez que « 192.168.1.0/24 » est un réseau valide et affichez son nombre d'adresses.
7.2 pip et environnements virtuels (venv)

pip installe des paquets tiers depuis PyPI. Un environnement virtuel (venv) isole les dépendances de chaque projet pour éviter les conflits de versions :

python -m venv venv
# Windows :
venv\Scripts\activate
# Linux / macOS :
source venv/bin/activate
pip install requests netmiko
pip freeze > requirements.txt
Pourquoi c'est important : requests version 2 et version 3 sur la même machine, et c'est le script de supervision qui casse un soir de garde. Un venv par projet isole chaque installation et fige les versions dans requirements.txt.
L'analogie qui aide : C'est une caisse à outils par chantier : les outils du chantier réseau ne se mélangent pas avec ceux du chantier SOC. venv fabrique la caisse, activate l'ouvre, pip freeze en fait l'inventaire.

Le fichier requirements.txt fige les versions et permet de réinstaller à l'identique avec pip install -r requirements.txt. Règle d'or : un projet = un venv, et on ne versionne jamais le dossier venv/ (ajoutez-le au .gitignore).

Démonstration pas à pas : Déroulé :
python -m venv venv → crée le dossier isolé. État : Python local au projet.
venv\Scripts\activate → l'invite affiche (venv). État : pip installe dedans, plus en global.
pip install requests netmiko → paquets isolés.
pip freeze > requirements.txt → inventaire figé, réinstallable avec -r.
Pièges classiques : Travailler sans activer le venv : tu installes en global sans le voir. Versionner le dossier venv/ dans Git : il pèse lourd et dépend de la machine, mets-le dans .gitignore. Faire pip install --upgrade la veille d'une mise en production. Oublier de régénérer requirements.txt après un ajout.
À vous de jouer : Quelle commande fige les versions pour réinstaller à l'identique sur une autre machine ? Réponse à retrouver dans la leçon, puis à tester.
Voir la réponse

Réponse : pip freeze > requirements.txt puis pip install -r requirements.txt sur l'autre machine.
Explication : le premier fige, le second réinstalle exactement les mêmes versions.

Exercice 7.2

À faire : sur votre poste, créez un dossier « lab-python », un venv dedans, activez-le, puis listez les paquets installés avec pip list.
7.3 Structurer un projet et lire sys.argv

Un projet propre sépare le code réutilisable (modules) du point d'entrée. Le bloc if __name__ == "__main__": permet d'importer les fonctions sans exécuter le script, et sys.argv lit les arguments de la ligne de commande :

# fichier utils_reseau.py
def resumer(ip, tentatives, seuil=10):
    """Retourne le niveau d'alerte pour une IP."""
    return "critique" if tentatives > seuil else "normal"

if __name__ == "__main__":
    import sys
    ip = sys.argv[1] if len(sys.argv) > 1 else "10.0.0.1"
    print(resumer(ip, 42))
Pourquoi c'est important : Un script écrit en vrac ne se réutilise pas : impossible de l'importer sans qu'il se lance, impossible de changer l'IP sans éditer le code. Le bloc main + sys.argv en fait un vrai outil en ligne de commande.
L'analogie qui aide : C'est un restaurant : la cuisine (les fonctions importables) tourne indépendamment, le comptoir (if __name__ == "__main__") ne sert que les clients qui entrent par la porte (exécution directe), avec leur commande (sys.argv).

Utilisation : python utils_reseau.py 5.6.7.8. Pour les projets plus gros, découpez en plusieurs fichiers (parse.py, api.py, rapport.py) importés par un main.py.

Démonstration pas à pas : Trace de python utils_reseau.py 5.6.7.8 :
sys.argv vaut [« utils_reseau.py », « 5.6.7.8 »], longueur 2 → ip = « 5.6.7.8 ».
resumer("5.6.7.8", 42) → 42 > 10 → renvoie « critique ».
Sans argument : sys.argv = [« utils_reseau.py »] → ip défaut « 10.0.0.1 » → même appel → « critique » aussi (42 > 10).
Pièges classiques : Lire sys.argv[1] sans vérifier len(sys.argv) lève IndexError quand on oublie l'argument. Oublier le test __name__ == "__main__" : l'import exécute le script. Laisser des seuils et des chemins en dur au lieu d'arguments ou de constantes. Découper trop tôt en 10 fichiers pour 50 lignes.
À vous de jouer : Prédis la sortie de python utils_reseau.py sans argument, puis vérifie en relisant le code : que vaut ip et qu'affiche le script ?
Voir la réponse

Réponse : ip vaut « 10.0.0.1 » (valeur par défaut) et le script affiche critique.
Explication : len(sys.argv) vaut 1 donc on prend le défaut, et resumer("10.0.0.1", 42) renvoie « critique » car 42 > 10.

Exercice 7.3

À faire : créez « ports.py » contenant est_bien_connu(p) (True si p < 1024) et un bloc main qui teste le port passé en argument.
[08]Module 8

Expressions régulières (regex)

Chercher et extraire des motifs dans du texte : IP, logs, configurations — avec le module re.

re.searchGroupesfindallCas réseau / SOC
8.1 Bases : search, match et métacaractères

Une regex décrit un motif à chercher. re.search() trouve le motif n'importe où dans la chaîne, re.match() seulement au début. Les métacaractères essentiels : \d (chiffre), \w (lettre/chiffre/underscore), \s (espace), . (n'importe quel caractère), + (1 ou plus), * (0 ou plus), ? (optionnel) :

import re

ligne = "Interface Gi0/1 is up, line protocol is up"
m = re.search(r"Gi\d+/\d+", ligne)
if m:
    print("Interface trouvee :", m.group(0))
Pourquoi c'est important : Extraire une interface d'un show, une IP d'un log, un VLAN d'une config : sans regex, tu écris des split() fragiles qui cassent au premier espace en trop. Avec, une ligne suffit.
L'analogie qui aide : La regex, c'est un pochoir : \d ne laisse passer que les chiffres, + dit « un ou plusieurs », et re.search() promène le pochoir sur toute la ligne jusqu'à trouver le motif.

Préfixez toujours les motifs par r (raw string) pour que les antislashs ne soient pas interprétés par Python. Testez vos motifs sur de petits exemples avant de les lancer sur des méga-fichiers de logs.

Démonstration pas à pas : Trace : ligne = « Interface Gi0/1 is up... ».
Motif r"Gi\d+/\d+" : « Gi » littéral, \d+ = un ou plusieurs chiffres, « / », \d+.
re.search() balaie : trouve « Gi0/1 » à la position 10 → objet match. m.group(0) → « Gi0/1 ». État : « Interface trouvee : Gi0/1 » affiché.
Pièges classiques : Oublier le préfixe r : "\d" devient un caractère d'échappement au lieu d'un motif chiffre. Utiliser re.match() qui n'ancre qu'au début au lieu de search(). Appeler m.group() sans vérifier if mAttributeError quand rien n'est trouvé. Laisser un . qui matche n'importe quoi au lieu de \. pour un point réel.
À vous de jouer : Prédis la sortie, puis vérifie :
import re
print(re.findall(r"VLAN\d+", "VLAN10 actif, VLAN20 inactif"))
Voir la réponse

Sortie : ['VLAN10', 'VLAN20'].
Explication : findall() renvoie toutes les occurrences non chevauchantes du motif « VLAN » suivi de chiffres.

Exercice 8.1

À faire : dans « VLAN10 actif, VLAN20 inactif », extrayez tous les motifs « VLAN » suivis de chiffres avec re.findall() (attendu : ['VLAN10', 'VLAN20']).
8.2 Groupes de capture et motifs réseau

Les parenthèses capturent des morceaux du motif, récupérables avec m.group(1), m.group(2)... Les groupes nommés (?P<nom>...) rendent le code lisible. Voici le motif IPv4 de référence :

import re

MOTIF_IP = r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
log = "DROP SRC=203.0.113.7 DST=10.0.0.1"
m = re.search(r"SRC=" + MOTIF_IP, log)
if m:
    print("Source bloquee :", m.group(1))
Pourquoi c'est important : Un log pare-feu brut ne sert à rien tant que tu n'as pas isolé la source et la destination. Les groupes de capture extraient exactement ces morceaux pour alimenter le comptage et le croisement d'IOC.
L'analogie qui aide : Ce sont des cases à remplir sur le pochoir : chaque parenthèse est une case, group(1) lit la case 1, et le groupe nommé (?P<dst>...) écrit carrément le nom sur la case.

Autres motifs utiles : MAC r"([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}", interface r"(Gi|Fa|Eth)\d+/\d+", code HTTP r"\b([1-5]\d{2})\b". Combinez findall() + set pour lister les valeurs uniques d'un fichier.

Démonstration pas à pas : Trace : log = « DROP SRC=203.0.113.7 DST=10.0.0.1 ».
Motif r"SRC=(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" : « SRC= » puis 4 groupes de 1 à 3 chiffres séparés par des points échappés.
search() trouve « SRC=203.0.113.7 » → group(1) = « 203.0.113.7 ». État affiché : « Source bloquee : 203.0.113.7 ».
Pièges classiques : Confondre group(0) (tout le match) et group(1) (1re parenthèse). Le motif IPv4 naïf accepte « 999.999.999.999 » : ajoute une validation ipaddress derrière. Un .* glouton qui avale trop de texte entre deux champs. Oublier d'échapper les points de l'IP.
À vous de jouer : Prédis la sortie, puis vérifie :
import re
log = "DROP SRC=203.0.113.7 DST=10.0.0.1"
m = re.search(r"DST=(?P<dst>\S+)", log)
print(m.group("dst"))
Voir la réponse

Sortie : 10.0.0.1.
Explication : le groupe nommé « dst » capture tous les non-espaces après « DST= », soit l'adresse de destination.

Exercice 8.2

À faire : extrayez l'adresse de destination (DST=...) du log ci-dessus avec un groupe nommé (?P<dst>...) et affichez-la.
8.3 Substitution et parsing de configurations

re.sub() remplace les occurrences d'un motif — idéal pour anonymiser des logs ou normaliser des configs. Avec re.MULTILINE, ^ et $ s'appliquent à chaque ligne, ce qui permet de parser des blocs de configuration :

import re

conf = "interface Fa0/1\n switchport access vlan 10\ninterface Fa0/2\n switchport access vlan 20\n"
vlans = re.findall(r"switchport access vlan (\d+)", conf)
print("VLAN configures :", vlans)

log = "Connexion de 203.0.113.7 refusee"
print(re.sub(r"\d+\.\d+\.\d+\.\d+", "[IP MASQUEE]", log))
Pourquoi c'est important : Anonymiser 2 Go de logs avant de les transmettre, extraire tous les VLAN d'une config, normaliser des noms : sub() et findall() font en trois lignes ce qui prendrait une journée à la main.
L'analogie qui aide : C'est le correcteur et l'aspirateur : sub() corrige chaque occurrence trouvée (l'IP devient « [IP MASQUEE] »), findall() aspire toutes les occurrences dans un sac (la liste des VLAN).

Pour les gros volumes, compilez le motif une fois avec re.compile() puis réutilisez l'objet : c'est plus rapide et plus lisible dans les boucles de parsing.

Démonstration pas à pas : Trace : config avec vlan 10 puis vlan 20 → findall(r"switchport access vlan (\d+)", conf) balaie tout le texte → [« 10 », « 20 »]. État affiché : « VLAN configures : ['10', '20'] ».
Log « Connexion de 203.0.113.7 refusee » → sub(motif_ip, "[IP MASQUEE]", log) → « Connexion de [IP MASQUEE] refusee ».
Pièges classiques : Oublier re.MULTILINE : ^ et $ ne s'appliquent qu'au début/fin du texte entier, pas de chaque ligne. Recompiler le motif dans une boucle sur un million de lignes : compile une fois avec re.compile(). sub() sans count remplace tout, y compris ce que tu voulais garder. Point non échappé dans le motif IP.
À vous de jouer : Prédis le résultat, puis vérifie :
import re
t = "De 10.0.0.1 vers 10.0.0.2 via 10.0.0.254"
print(len(re.findall(r"\d+\.\d+\.\d+\.\d+", t)))
Voir la réponse

Réponse : 3.
Explication : le motif trouve trois adresses distinctes, findall() renvoie une liste de 3 éléments.

Exercice 8.3

À faire : compilez le motif IPv4, puis comptez avec findall() le nombre d'IP dans : « De 10.0.0.1 vers 10.0.0.2 via 10.0.0.254 » (attendu : 3).