Lire et écrire des fichiers CSV et JSON, gérer les erreurs, utiliser pip et les environnements virtuels, maîtriser les regex
Lire et écrire des fichiers texte, exploiter des inventaires CSV et des réponses d'API JSON.
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())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).
open("equipements.txt", "w") → fichier vidé ou créé.f.write("R1\nSW1\nSW2\n") → 3 lignes écrites."r" : 1re itération ligne = « R1\n » → strip() → « R1 » → affiche « Hote : R1 ». Idem pour SW1 puis SW2. Fin : fichier fermé automatiquement."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.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())Sortie :X : A
X : B
Explication : strip() retire le retour ligne, chaque tour affiche la ligne nettoyée préfixée de « X : ».
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']})")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().
newline="".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.for eq in csv.DictReader(f) ? Vérifie en relisant la leçon.Réponse : 3 tours.
Explication : DictReader consomme la première ligne comme en-têtes et ne la compte pas comme donnée.
DictReader et affichez les IP ayant plus de 10 tentatives.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))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.
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.{'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.import json
d = json.loads('{"ip": "9.9.9.9", "score": 85}')
print(f"{d['ip']} : score {d['score']}")
print(d["score"] + 1)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.
'{"ip": "9.9.9.9", "score": 85}' avec json.loads() et affichez « 9.9.9.9 : score 85 » avec une f-string.Anticiper les pannes : try/except, types d'erreurs courants, lever ses propres exceptions, journaliser.
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)try/except, le premier grain de sable plante tout le script de nuit.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}")
int("abc") → lève ValueError, le reste du bloc try est sauté.except ValueError → trouvé → affiche « Port invalide », port = 22.print("Port retenu :", port) après le bloc s'exécute normalement.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.try:
port = int("8080x")
except ValueError:
print("Conversion impossible")
port = 0
print(port)Sortie :Conversion impossible
0
Explication : int("8080x") lève ValueError, le bloc except s'exécute et force port à 0.
saisie = "8080x" en entier ; en cas de ValueError, affichez « Conversion impossible » et affectez 0 à port.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 TrueTypeError, ValueError, KeyError...). Avec raise, tu deviens toi-même le contrôleur qui appose le tampon.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.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.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))Sortie :refuse
True
Explication : 99999 déclenche le raise attrapé par le except, puis 80 est valide et renvoie True.
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.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"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.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.
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.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.import logging
logging.basicConfig(level=logging.INFO)
logging.info("Scan termine : 12 hotes")
logging.warning("1 hote sans reponse")
assert 24 <= 32
print("fin")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.
logging au niveau INFO, journalisez « Scan terminé : 12 hôtes » en INFO et « 1 hôte sans réponse » en WARNING.Réutiliser du code : imports, bibliothèque standard utile, pip, venv et structuration d'un projet.
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))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).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.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.from collections import Counter
print(Counter("abac").most_common(1))Sortie : [('a', 2)].
Explication : « a » apparaît 2 fois, « b » et « c » une fois. Le plus fréquent est donc « a » avec 2.
ipaddress, vérifiez que « 192.168.1.0/24 » est un réseau valide et affichez son nombre d'adresses.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.txtrequests 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.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).
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.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.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.
pip list.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))main + sys.argv en fait un vrai outil en ligne de commande.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.
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 ».sys.argv = [« utils_reseau.py »] → ip défaut « 10.0.0.1 » → même appel → « critique » aussi (42 > 10).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.python utils_reseau.py sans argument, puis vérifie en relisant le code : que vaut ip et qu'affiche le script ?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.
est_bien_connu(p) (True si p < 1024) et un bloc main qui teste le port passé en argument.Chercher et extraire des motifs dans du texte : IP, logs, configurations — avec le module re.
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))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.\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.
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é.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 m → AttributeError quand rien n'est trouvé. Laisser un . qui matche n'importe quoi au lieu de \. pour un point réel.import re
print(re.findall(r"VLAN\d+", "VLAN10 actif, VLAN20 inactif"))Sortie : ['VLAN10', 'VLAN20'].
Explication : findall() renvoie toutes les occurrences non chevauchantes du motif « VLAN » suivi de chiffres.
re.findall() (attendu : ['VLAN10', 'VLAN20']).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))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.
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 ».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.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"))Sortie : 10.0.0.1.
Explication : le groupe nommé « dst » capture tous les non-espaces après « DST= », soit l'adresse de destination.
(?P<dst>...) et affichez-la.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))sub() et findall() font en trois lignes ce qui prendrait une journée à la main.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.
findall(r"switchport access vlan (\d+)", conf) balaie tout le texte → [« 10 », « 20 »]. État affiché : « VLAN configures : ['10', '20'] ».sub(motif_ip, "[IP MASQUEE]", log) → « Connexion de [IP MASQUEE] refusee ».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.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)))Réponse : 3.
Explication : le motif trouve trois adresses distinctes, findall() renvoie une liste de 3 éléments.
findall() le nombre d'IP dans : « De 10.0.0.1 vers 10.0.0.2 via 10.0.0.254 » (attendu : 3).