Aller au contenu

14 - CSV

Vidéo : les fichiers CSV en Python (12 min 17)

Introduction

Beaucoup de données se présentent sous forme de table : une ligne par objet décrit, une colonne par caractéristique. Un relevé météo, la liste des élèves d’une classe ou les horaires d’une ligne de bus en sont des exemples.

Pour échanger ces tables entre logiciels, on utilise très souvent le format CSV (Comma Separated Values, « valeurs séparées par des virgules »). Un fichier CSV est un simple fichier texte :

  • chaque ligne du fichier est une ligne de la table, appelée enregistrement ;
  • sur une ligne, les valeurs sont séparées par un caractère choisi, le délimiteur : une virgule, ou un point-virgule.

Chaque colonne porte un nom, appelé descripteur. Un enregistrement donne une valeur à chaque descripteur : c’est un p-uplet nommé. Une table est donc une liste d’enregistrements qui partagent les mêmes descripteurs.

Un tableur (LibreOffice Calc, Excel) ouvre un fichier CSV et sait en enregistrer un ; en Python, le module csv permet de le lire et de l’écrire.

Structure d’un fichier CSV

Voici le fichier meteo.csv, qui donne pour quelques villes la température maximale de la journée, en degrés, et la hauteur de pluie tombée, en millimètres :

ville,région,température,pluie
Lyon,Auvergne-Rhône-Alpes,24,0
Brest,Bretagne,17,6
Nice,Provence-Alpes-Côte d'Azur,26,0
Lille,Hauts-de-France,15,12

La première ligne est l’en-tête : elle donne les descripteurs, c’est-à-dire le nom de chaque colonne. Chacune des lignes suivantes est un enregistrement, qui décrit une ville : pour Brest, le descripteur température prend la valeur 17. Le même fichier, présenté en tableau :

villerégiontempératurepluie
LyonAuvergne-Rhône-Alpes240
BrestBretagne176
NiceProvence-Alpes-Côte d’Azur260
LilleHauts-de-France1512

Deux précautions :

  • en France, les tableurs utilisent souvent le point-virgule comme délimiteur, parce que la virgule sert à écrire les nombres décimaux (12,5) ;
  • une valeur qui contient le délimiteur s’écrit entre guillemets, par exemple "Paris, 5e".

Lecture avec csv.reader()

La fonction csv.reader() lit le fichier ligne par ligne et donne chaque ligne sous forme de liste de valeurs :

import csv

with open('meteo.csv', encoding='utf-8') as fichier:
    lecteur = csv.reader(fichier, delimiter=',')
    for ligne in lecteur:
        print(ligne)
['ville', 'région', 'température', 'pluie']
['Lyon', 'Auvergne-Rhône-Alpes', '24', '0']
['Brest', 'Bretagne', '17', '6']
['Nice', "Provence-Alpes-Côte d'Azur", '26', '0']
['Lille', 'Hauts-de-France', '15', '12']

La première liste est l’en-tête. Pour la passer, on appelle une fois next(lecteur) avant la boucle : la boucle commence alors à la deuxième ligne. Dans chaque liste, ligne[0] est la ville, ligne[2] la température :

import csv

with open('meteo.csv', encoding='utf-8') as fichier:
    lecteur = csv.reader(fichier, delimiter=',')
    next(lecteur)  # passe la ligne d'en-tête
    for ligne in lecteur:
        print(f'À {ligne[0]}, il a fait {ligne[2]} degrés')
À Lyon, il a fait 24 degrés
À Brest, il a fait 17 degrés
À Nice, il a fait 26 degrés
À Lille, il a fait 15 degrés

Toutes les valeurs sont des chaînes

Dans les listes ci-dessus, la température est écrite '24', entre apostrophes : le module csv donne toujours des chaînes de caractères, même quand la valeur ressemble à un nombre. Pour calculer, il faut d’abord convertir avec int() ou float().

import csv

with open('meteo.csv', encoding='utf-8') as fichier:
    lecteur = csv.reader(fichier, delimiter=',')
    next(lecteur)
    total = 0
    nombre = 0
    for ligne in lecteur:
        total = total + int(ligne[2])
        nombre = nombre + 1
print(f'Température moyenne : {total / nombre} degrés')
Température moyenne : 20.5 degrés

Sans int(), l’instruction total + ligne[2] provoquerait une erreur, car on ne peut pas ajouter une chaîne à un entier.

Lecture avec csv.DictReader()

La fonction csv.DictReader() utilise l’en-tête : elle donne chaque ligne sous forme de dictionnaire, dont les clés sont les descripteurs, lus dans l’en-tête. L’en-tête n’apparaît donc plus comme une ligne de données.

import csv

with open('meteo.csv', encoding='utf-8') as fichier:
    lecteur = csv.DictReader(fichier, delimiter=',')
    for ligne in lecteur:
        print(f'{ligne["ville"]} ({ligne["région"]}) : {ligne["pluie"]} mm de pluie')
Lyon (Auvergne-Rhône-Alpes) : 0 mm de pluie
Brest (Bretagne) : 6 mm de pluie
Nice (Provence-Alpes-Côte d'Azur) : 0 mm de pluie
Lille (Hauts-de-France) : 12 mm de pluie

Écrire ligne["pluie"] est plus clair que ligne[3], et le programme reste juste si quelqu’un change l’ordre des colonnes dans le fichier.

Écriture avec csv.writer()

Pour créer un fichier CSV, on ouvre un fichier en écriture et on lui associe un csv.writer(). La méthode writerow() écrit une ligne à partir d’une liste :

import csv

releves = [
    ['ville', 'région', 'température', 'pluie'],
    ['Rennes', 'Bretagne', 18, 4],
    ['Toulouse', 'Occitanie', 27, 0],
]

with open('releves.csv', 'w', encoding='utf-8', newline='') as fichier:
    ecrivain = csv.writer(fichier, delimiter=',')
    for ligne in releves:
        ecrivain.writerow(ligne)

Le fichier releves.csv contient alors :

ville,région,température,pluie
Rennes,Bretagne,18,4
Toulouse,Occitanie,27,0

Les nombres sont convertis en texte à l’écriture. Le paramètre newline='' évite, sous Windows, une ligne vide après chaque enregistrement.

Le mode 'w' remplace le contenu du fichier. Pour ajouter une ligne à la fin d’un fichier existant, on l’ouvre en mode 'a' (append, « ajouter ») :

with open('releves.csv', 'a', encoding='utf-8', newline='') as fichier:
    ecrivain = csv.writer(fichier, delimiter=',')
    ecrivain.writerow(['Strasbourg', 'Grand Est', 21, 2])

releves.csv compte alors trois villes : Rennes, Toulouse et Strasbourg.

Écriture avec csv.DictWriter()

csv.DictWriter() écrit des dictionnaires. Le paramètre fieldnames lui donne la liste des colonnes ; la méthode writeheader() écrit la ligne d’en-tête, puis writerow() écrit un dictionnaire par ligne :

import csv

releves = [
    {'ville': 'Rennes', 'température': 18},
    {'ville': 'Toulouse', 'température': 27},
]

with open('temperatures.csv', 'w', encoding='utf-8', newline='') as fichier:
    ecrivain = csv.DictWriter(fichier, fieldnames=['ville', 'température'], delimiter=',')
    ecrivain.writeheader()        # ligne d'en-tête
    for ligne in releves:
        ecrivain.writerow(ligne)  # un dictionnaire par ligne
ville,température
Rennes,18
Toulouse,27

Comparaison : reader() vs DictReader()

Pour la ligne de Brest, les deux fonctions donnent :

# csv.reader() : une liste, lue par indice
['Brest', 'Bretagne', '17', '6']
# ligne[2] vaut '17'

# csv.DictReader() : un dictionnaire, lu par nom de colonne
{'ville': 'Brest', 'région': 'Bretagne', 'température': '17', 'pluie': '6'}
# ligne['température'] vaut '17'

csv.reader() suffit pour un petit fichier dont on connaît l’ordre des colonnes. csv.DictReader() est préférable dès qu’il y a plusieurs colonnes : le code se lit mieux et ne dépend pas de leur ordre. Dans les deux cas, les valeurs restent des chaînes.

Charger un CSV dans une liste de dictionnaires

On charge souvent tout un fichier en mémoire, sous forme d’une liste de dictionnaires : chaque dictionnaire est une ligne de la table. On peut ensuite la parcourir autant de fois qu’on veut, la filtrer ou la trier.

import csv

def charger_csv(nom_fichier):
    """Renvoie le contenu du fichier CSV sous forme de liste de dictionnaires."""
    with open(nom_fichier, encoding='utf-8') as fichier:
        lecteur = csv.DictReader(fichier, delimiter=',')
        return list(lecteur)

table = charger_csv('meteo.csv')
print(len(table))
print(table[1])
4
{'ville': 'Brest', 'région': 'Bretagne', 'température': '17', 'pluie': '6'}

Pour garder, par exemple, les villes où il n’a pas plu :

seches = [ligne['ville'] for ligne in table if int(ligne['pluie']) == 0]
print(seches)
['Lyon', 'Nice']

Rechercher avec plusieurs critères

Un critère de recherche est une expression booléenne : on combine plusieurs conditions avec les opérateurs and, or et not.

  • Avec and, une ligne est gardée si toutes les conditions sont vraies : ici, les villes à plus de 25 degrés et sans pluie.
  • Avec or, une ligne est gardée si au moins une condition est vraie : ici, les villes à moins de 16 degrés ou à plus de 5 mm de pluie.
ensoleillees = [ligne['ville'] for ligne in table
                if int(ligne['température']) > 25
                and int(ligne['pluie']) == 0]
print(ensoleillees)

fraiches_ou_pluvieuses = [ligne['ville'] for ligne in table
                          if int(ligne['température']) < 16
                          or int(ligne['pluie']) > 5]
print(fraiches_ou_pluvieuses)
['Nice']
['Brest', 'Lille']

Lyon est sèche, mais à 24 degrés seulement : elle ne vérifie pas le premier critère. Brest, à 17 degrés, est gardée par le second critère grâce à ses 6 mm de pluie ; Lille vérifie les deux conditions.

Doublons et cohérence d’une table

Avant d’exploiter une table, on vérifie qu’elle ne contient pas d’erreur de saisie. Pour l’exemple, deux enregistrements fautifs sont ajoutés à la table de départ :

releves = table + [
    {'ville': 'Brest', 'région': 'Bretagne', 'température': '19', 'pluie': '2'},
    {'ville': 'Rennes', 'région': 'Bretagne', 'température': '18', 'pluie': '-4'},
]

Les doublons. Un doublon est un enregistrement présent deux fois. La fonction doublons() parcourt la table et garde dans la liste vus chaque valeur déjà rencontrée pour le descripteur choisi ; une valeur qui s’y trouve déjà est un doublon :

def doublons(table, descripteur):
    vus = []
    repetes = []
    for ligne in table:
        valeur = ligne[descripteur]
        if valeur in vus:
            repetes.append(valeur)
        else:
            vus.append(valeur)
    return repetes

print(doublons(releves, 'ville'))
['Brest']

La cohérence. Chaque descripteur a un domaine de valeurs : l’ensemble des valeurs qu’il peut prendre. Une hauteur de pluie n’est jamais négative, et une température ne dépasse pas 50 degrés. Une valeur hors de son domaine signale une erreur :

incoherentes = [ligne['ville'] for ligne in releves
                if int(ligne['pluie']) < 0
                or int(ligne['température']) > 50]
print(incoherentes)
['Rennes']

Il faut corriger ces enregistrements avant de calculer une moyenne ou de trier la table.

Trier une table suivant une colonne

La fonction sorted() trie une liste. Pour trier une table, c’est-à-dire une liste de dictionnaires, suivant une colonne, on lui donne avec le paramètre key une fonction qui, appliquée à une ligne, renvoie la valeur à comparer :

eleves = [
    {"nom": "Alice", "note": 15},
    {"nom": "Bob", "note": 12},
    {"nom": "Chloé", "note": 17},
]

def note(ligne):
    return ligne["note"]

par_note = sorted(eleves, key=note)
print([e["nom"] for e in par_note])      # ['Bob', 'Alice', 'Chloé']

decroissant = sorted(eleves, key=note, reverse=True)
print([e["nom"] for e in decroissant])   # ['Chloé', 'Alice', 'Bob']

On écrit le nom de la fonction sans parenthèses : c’est sorted() qui l’appelle sur chaque ligne. Le paramètre reverse=True inverse l’ordre. Le même paramètre key existe pour max() et min() : max(eleves, key=note) renvoie la ligne de la meilleure note.

Fusionner deux tables

Fusionner deux tables, c’est construire une nouvelle table qui combine leurs données. Les deux tables doivent avoir un descripteur commun, qui sert de clé de fusion : deux lignes sont réunies quand elles ont la même valeur pour ce descripteur. Ce descripteur doit avoir le même domaine de valeurs dans les deux tables : les mêmes noms de villes, écrits de la même façon ('LYON' ne correspond pas à 'Lyon').

Voici le fichier departements.csv, qui donne le département de quelques villes :

ville,département
Lyon,Rhône
Brest,Finistère
Lille,Nord
Marseille,Bouches-du-Rhône

Pour chaque ligne de la première table, la fonction fusionner() parcourt toute la seconde table. Quand les deux lignes ont la même valeur pour la clé, elle crée une nouvelle ligne, qui reçoit les colonnes de la première ligne, puis celles de la seconde :

def fusionner(table1, table2, cle):
    resultat = []
    for ligne1 in table1:
        for ligne2 in table2:
            if ligne1[cle] == ligne2[cle]:
                nouvelle_ligne = {}
                for k in ligne1:
                    nouvelle_ligne[k] = ligne1[k]
                for k in ligne2:
                    nouvelle_ligne[k] = ligne2[k]
                resultat.append(nouvelle_ligne)
    return resultat

meteo = charger_csv('meteo.csv')
departements = charger_csv('departements.csv')
for ligne in fusionner(meteo, departements, 'ville'):
    print(f'{ligne["ville"]} ({ligne["département"]}) : {ligne["température"]} degrés')
Lyon (Rhône) : 24 degrés
Brest (Finistère) : 17 degrés
Lille (Nord) : 15 degrés

La table fusionnée ne contient que les villes présentes dans les deux tables : Nice n’a pas de ligne dans departements.csv, et Marseille n’en a pas dans meteo.csv.

Résumé des fonctions du module csv

FonctionRôleType renvoyé par ligne
csv.reader()Lecture ligne par ligneliste de chaînes
csv.DictReader()Lecture avec en-têtedictionnaire
csv.writer()Écriture ligne par ligne—
csv.DictWriter()Écriture avec en-tête—