Cours de Python / Lire et écrire des CSV avec DictReader et DictWriter

Le format CSV, pour Comma-Separated Values, enregistre des lignes et des champs sous forme de texte. Utilisez le module csv pour interpréter séparateurs et guillemets, puis validez les données : DictReader fournit normalement des chaînes, même pour les montants.

Lire un CSV sans découper naïvement les virgules

Un champ entouré de guillemets peut contenir une virgule, un guillemet échappé ou un saut de ligne. split(",") ne sait pas interpréter ces règles et peut produire de fausses colonnes. Dans l’exercice, lunch, team est un seul nom, pas deux champs distincts.

DictReader utilise par défaut la première ligne comme noms des colonnes ; DictWriter reçoit explicitement leur liste et leur ordre. Ouvrez les fichiers avec newline="" afin de laisser csv gérer les fins de ligne. L’encodage utf-8-sig accepte un texte UTF-8 avec ou sans marqueur BOM, pour Byte Order Mark, sans ajouter ce marqueur au premier nom de colonne.

Les détails de ces classes et des dialectes figurent dans la documentation officielle du module csv. La leçon sur les fichiers texte, with et UTF-8 explique l’ouverture, la fermeture et le remplacement d’un fichier.

Écrire un nom entre guillemets et calculer le total

Python 3 et sa bibliothèque standard suffisent. Utilisez un dossier réservé aux essais : le programme remplace expenses-demo.csv à chaque lancement. Enregistrez le bloc complet ci-dessous dans csv_demo.py, en UTF-8 ; ne nommez pas votre script csv.py, ce qui pourrait masquer le module standard.

Dans un terminal ouvert dans ce dossier avec l’environnement virtuel du projet activé, lancez python csv_demo.py. Sur macOS ou Ubuntu hors environnement virtuel, utilisez python3 csv_demo.py si votre interpréteur porte ce nom.

import csv
from pathlib import Path

path = Path("expenses-demo.csv")
with path.open("w", encoding="utf-8", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "amount"])
    writer.writeheader()
    writer.writerows([{ "name": "lunch, team", "amount": 8000}, {"name": "bus", "amount": 2500}])
total = 0
with path.open(encoding="utf-8-sig", newline="") as f:
    reader = csv.DictReader(f)
    if reader.fieldnames != ["name", "amount"]:
        raise ValueError("unexpected header")
    for row in reader:
        total += int(row["amount"])
        print(row["name"])
print("total:", total)

Le résultat attendu est :

lunch, team
bus
total: 10500

Le writer écrit l’en-tête puis deux enregistrements. Il entoure automatiquement lunch, team de guillemets parce que le nom contient une virgule. Le reader restitue ce nom dans un seul champ et la conversion des montants avec int() permet l’addition 8000 + 2500 = 10500.

Ouvrez le fichier dans un éditeur de texte pour observer le champ délimité par des guillemets. Relisez-le aussi avec csv : chaque enregistrement doit contenir exactement deux champs et le premier nom doit être identique à la chaîne d’origine. Les guillemets de structure ne font pas partie du nom restitué.

Définir la validation des montants et des colonnes

Le test reader.fieldnames != ["name", "amount"] impose ici les deux noms et leur ordre exact. Un en-tête cost à la place d’amount, des colonnes inversées ou un nom répété ne correspondent pas à ce contrat. Le message unexpected header signifie « en-tête inattendu ». Ce contrôle ne constitue toutefois pas à lui seul une validation de toutes les lignes.

Avec DictReader, des champs supplémentaires peuvent être placés sous la clé None et des champs manquants peuvent recevoir None par défaut. Une ligne peut donc avoir une structure inattendue même après un bon en-tête. Pour une importation réelle, vérifiez le nombre et la présence des champs ainsi que les règles métier avant de les agréger ; décidez si une erreur arrête le traitement ou produit un enregistrement rejeté avec son motif.

int() ne traite pas indifféremment une chaîne vide, "8,000" ou "8000 KRW" comme le montant 8000. Définissez le format accepté avant une conversion ; supprimer arbitrairement tous les caractères non numériques pourrait aussi transformer une donnée incorrecte en nombre apparemment valide. Le programme d’introduction suppose ici des montants entiers convertibles et ne les contrôle pas comme une application complète.

Pour signaler une position, enumerate(reader, start=2) peut numéroter les enregistrements logiques après l’en-tête. Ce compteur ne correspond pas nécessairement à la ligne physique du fichier lorsqu’un champ contient un saut de ligne. La propriété reader.line_num indique le nombre de lignes physiques consommées, pas le numéro initial de chaque enregistrement. Précisez la convention utilisée dans un rapport d’erreurs.

Vérifier les relances et les exports vers un tableur

  1. Relancez deux fois le programme initial. Le CSV doit toujours contenir un en-tête et deux enregistrements, sans accumulation. Vérifiez à la relecture que lunch, team reste un seul champ.
  2. Dans le test d’en-tête uniquement, remplacez la liste attendue ["name", "amount"] par ["name", "cost"], sans modifier le writer. Le programme s’arrête sur ValueError: unexpected header avant d’additionner les lignes. Restaurez la liste attendue.
  3. Remplacez seulement le montant entier 8000 écrit par le writer par la chaîne "8,000". Le CSV peut enregistrer correctement ce champ, mais la conversion int échoue avec une ValueError. Restaurez l’entier 8000 et retrouvez les trois lignes de référence.

Modifier manuellement le CSV puis relancer tout ce script ne permet pas de tester cette modification : sa première partie recrée le fichier. Pour tester des fichiers externes, séparez leur lecture de la génération des données d’exercice. Cette distinction évite de croire qu’un contrôle a traité un fichier qui a en réalité été remplacé avant la lecture.

CSV ne fixe pas les types des cellules ni les règles de calcul d’un tableur. Des valeurs extérieures commençant par =, par exemple, peuvent être interprétées comme des formules dans un tableur, même si leur écriture CSV est syntaxiquement correcte. Pour un export destiné à être partagé, définissez le traitement texte adapté au logiciel destinataire ; les seuls guillemets CSV ne garantissent pas l’absence d’interprétation de formule. La fiche OWASP sur l’injection de formules dans les CSV explique cette différence et les limites des protections selon les destinataires.