CSV : reader, DictReader et lignes invalides
Lire un CSV avec csv.reader ou csv.DictReader, régler newline, l'encodage et le séparateur, numéroter les lignes avec enumerate, sauter une ligne invalide avec continue et valider un champ avec re.search.
Mis à jour le
À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.
Pourquoi on voit ça
CSV veut dire « valeurs séparées par des virgules ». C’est le format des exports de tableur : une ligne par enregistrement, un séparateur entre les champs. Les inventaires de serveurs arrivent très souvent sous cette forme.
Un CSV est tapé ou exporté par des humains, donc il contient des erreurs : un nom mal écrit, un champ vide. Ton outil doit lire le fichier, repérer les lignes fausses, les signaler avec leur numéro et continuer avec les autres.
Dans toute la leçon, servers.csv contient :
name,env,port
web-1,prod,80
db-1,dev,5432
La première ligne est l’en-tête : elle donne le nom des colonnes.
csv.reader : chaque ligne est une liste
import csv
with open("servers.csv", newline="", encoding="utf-8") as f:
for row in csv.reader(f):
print(row)
['name', 'env', 'port']
['web-1', 'prod', '80']
['db-1', 'dev', '5432']
csv.reader découpe chaque ligne en liste de chaînes. Deux points à voir :
- L’en-tête est une ligne comme les autres. C’est à toi de la mettre de côté.
- Tout est du texte.
'80'est unstr:row[2] == 80vautFalse. Pour calculer, il fautint(row[2]).
On lit un champ par sa position : row[0] pour le nom.
csv.DictReader : chaque ligne est un dictionnaire
with open("servers.csv", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
print(row)
{'name': 'web-1', 'env': 'prod', 'port': '80'}
{'name': 'db-1', 'env': 'dev', 'port': '5432'}
DictReader utilise l’en-tête pour nommer les clés. La première ligne n’apparaît donc plus dans les données : on obtient deux dictionnaires, pas trois lignes.
csv.reader |
csv.DictReader |
|
|---|---|---|
| Une ligne est… | une list |
un dict |
| On lit le nom avec | row[0] |
row["name"] |
| L’en-tête | est la première ligne renvoyée | sert de clés, n’est pas renvoyé |
| Les valeurs | des str |
des str |
Trois réglages : newline, utf-8-sig, delimiter
newline=“”
Avec le module csv, on ouvre toujours le fichier avec newline="". Ça laisse csv gérer lui-même les fins de ligne. Sans ça, on risque des lignes vides en trop selon le système.
encoding=“utf-8-sig”
Certains tableurs ajoutent au tout début du fichier un caractère invisible, le BOM. Lu en utf-8, il se colle au premier nom de colonne :
{'\ufeffname': 'web-1', 'env': 'prod', 'port': '80'}
Résultat : row["name"] lève KeyError: 'name' alors que la colonne a l’air d’exister. Avec encoding="utf-8-sig", Python enlève ce caractère s’il est là, et lit normalement s’il n’y est pas.
delimiter
Un tableur réglé en français sépare souvent les champs par des points-virgules. Par défaut, csv cherche des virgules et ne coupe rien :
['web-1;prod;80']
Une liste d’un seul élément. Il faut annoncer le séparateur :
with open("export.csv", newline="", encoding="utf-8-sig") as f:
rows = list(csv.reader(f, delimiter=";"))
print(rows[1])
['web-1', 'prod', '80']
Parcourir les lignes de données
Sauter l’en-tête : rows[1:]
Avec csv.reader, on range tout dans une liste, puis on parcourt à partir de la position 1.
with open("servers.csv", newline="", encoding="utf-8") as f:
rows = list(csv.reader(f))
for row in rows[1:]:
print(row[0])
web-1
db-1
rows[0] est l’en-tête ; rows[1:] veut dire « tout sauf le premier ».
Numéroter : enumerate
Pour dire à quelle ligne se trouve une erreur, il faut un compteur. enumerate donne à chaque tour le numéro et l’élément.
for i, row in enumerate(rows[1:], start=2):
print(i, row[0])
2 web-1
3 db-1
Sans start, enumerate compte à partir de 0. Ici on part de 2 : l’en-tête est la ligne 1 du fichier, le premier serveur est donc à la ligne 2.
Associer en-tête et valeurs : zip
zip avance dans deux listes en même temps et donne des paires.
header = rows[0]
row = rows[1]
for k, v in zip(header, row):
print(k, "=", v)
print(dict(zip(header, row)))
name = web-1
env = prod
port = 80
{'name': 'web-1', 'env': 'prod', 'port': '80'}
dict(zip(header, row)) fabrique à la main ce que DictReader fait tout seul.
Erreur fatale ou ligne invalide ?
Ce sont deux situations différentes, avec deux réactions différentes.
| Erreur fatale | Ligne invalide | |
|---|---|---|
| Exemple | fichier absent ou illisible | un nom mal formé, un champ vide |
| Ce qui est touché | tout le traitement | une ligne |
| Réaction | message, puis sys.exit(1) |
message avec le numéro de ligne, puis continue |
continue abandonne le tour de boucle en cours et passe à la ligne suivante. Les autres serveurs sont traités normalement.
Valider un champ : re.search
Le module re vérifie qu’un texte suit un motif. re.search(motif, texte) renvoie un objet si le motif est trouvé, None sinon. On l’utilise donc directement dans un if.
Le motif r"^[a-z]+-\d+$" se lit : début (^), une ou plusieurs minuscules ([a-z]+), un tiret, un ou plusieurs chiffres (\d+), fin ($). web-1 correspond, cache_1 non.
Tout ensemble
parc.csv contient une ligne fausse, cache_1,prod,6379, en troisième ligne du fichier.
import csv
import re
import sys
try:
with open("parc.csv", newline="", encoding="utf-8-sig") as f:
rows = list(csv.reader(f))
except OSError as e:
print("Erreur fatale :", e)
sys.exit(1)
valid = 0
for i, row in enumerate(rows[1:], start=2):
if not re.search(r"^[a-z]+-\d+$", row[0]):
print(f"ligne {i} : nom invalide {row[0]}")
continue
valid = valid + 1
print(valid, "serveurs valides")
ligne 3 : nom invalide cache_1
2 serveurs valides
Le fichier s’est ouvert : pas d’erreur fatale. Une ligne est fausse : elle est signalée, sautée, et le script va jusqu’au bout.
À retenir
csv.reader: chaque ligne est une liste, en-tête compris.csv.DictReader: chaque ligne est un dict, l’en-tête sert de clés.- Toutes les valeurs d’un CSV sont des
str. Convertis avecint()avant de calculer. open(..., newline="", encoding="utf-8-sig"), etdelimiter=";"pour un export de tableur français.rows[1:]saute l’en-tête ;enumerate(rows[1:], start=2)donne les vrais numéros de ligne.- Fichier illisible : erreur fatale,
sys.exit(1). Ligne fausse : message,continue, et on poursuit. re.search(motif, texte)renvoieNonesi le texte ne correspond pas.
Entraînement
12 questions, dont 7 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.
Ta note
Cartes · 8
Réviser les cartes ici
Pourquoi