Fichiers et formats de données
Lire et écrire du CSV, du JSON et du Parquet en Python, en lien avec les formats vus côté data engineering.
Introduction
Un script Python data passe une grande partie de son temps à lire et écrire des fichiers : CSV exportés d'un système tiers, JSON reçus d'une API, Parquet stocké dans un data lake. Les mécanismes de base reposent sur la bibliothèque standard et sur la gestion des chemins avec pathlib, en s'appuyant sur les notions de formats de fichiers du cours data engineer.
Context managers : with open(...)
Avant d'ouvrir un fichier, un mécanisme du langage mérite d'être compris : le context manager, introduit par le mot-clé with.
with open("donnees.txt") as fichier: contenu = fichier.read() print(contenu) # à la sortie du bloc with, le fichier est automatiquement fermé, # que le bloc se termine normalement ou par une exception
Le bloc with garantit que le fichier est fermé proprement en sortie du bloc, y compris si une exception survient à l'intérieur. C'est l'équivalent Python d'un try/finally qui fermerait systématiquement la ressource, mais condensé dans une syntaxe dédiée.
# Équivalent manuel, sans with, à ne pas faire en pratique fichier = open("donnees.txt") try: contenu = fichier.read() finally: fichier.close()
💡 Bon à savoir : le principe rejoint la gestion de ressources en PHP, où un fichier ouvert avec
fopen()doit être explicitement refermé avecfclose(), généralement dans un blocfinally, ou confié à un objet dont le destructeur s'en charge. Lewithde Python rend ce nettoyage automatique et impossible à oublier, pour tout objet qui implémente le protocole de context manager (les méthodes__enter__et__exit__).
Plusieurs fichiers peuvent être ouverts dans un seul bloc with :
with open("source.csv") as entree, open("resultat.csv", "w") as sortie: sortie.write(entree.read())
Lecture et écriture de CSV avec la stdlib
Le module csv fait partie de la bibliothèque standard et gère correctement les subtilités du format (guillemets, séparateurs contenus dans une valeur, encodage), plutôt qu'un simple découpage de chaque ligne par la virgule.
import csv # Lecture ligne par ligne, chaque ligne est une liste de chaînes with open("utilisateurs.csv", newline="", encoding="utf-8") as fichier: lecteur = csv.reader(fichier) en_tete = next(lecteur) # ["id", "nom", "email"] for ligne in lecteur: print(ligne) # ["1", "Alice", "alice@mail.com"]
csv.DictReader va plus loin en exposant chaque ligne comme un dictionnaire, en s'appuyant sur la première ligne comme en-tête, ce qui rend le code moins dépendant de l'ordre des colonnes :
with open("utilisateurs.csv", newline="", encoding="utf-8") as fichier: lecteur = csv.DictReader(fichier) for ligne in lecteur: print(ligne["nom"], ligne["email"])
L'écriture suit le même principe, avec csv.writer ou csv.DictWriter :
with open("export.csv", "w", newline="", encoding="utf-8") as fichier: ecrivain = csv.DictWriter(fichier, fieldnames=["id", "nom", "email"]) ecrivain.writeheader() ecrivain.writerow({"id": 1, "nom": "Alice", "email": "alice@mail.com"}) ecrivain.writerow({"id": 2, "nom": "Bob", "email": "bob@mail.com"})
💡 Bon à savoir : l'argument
newline=""à l'ouverture du fichier n'est pas une fantaisie, il évite un bug classique sous Windows où le modulecsvinsère des lignes vides supplémentaires si la traduction automatique des fins de ligne du système n'est pas désactivée.
Lecture et écriture de JSON avec la stdlib
Le module json convertit directement entre chaînes JSON et structures Python natives (dict, list, str, int, float, bool, None), sans étape de configuration.
import json # Lecture d'un fichier JSON with open("utilisateur.json", encoding="utf-8") as fichier: donnees = json.load(fichier) print(donnees["nom"]) # Écriture d'un fichier JSON utilisateur = {"id": 1, "nom": "Alice", "actif": True} with open("export.json", "w", encoding="utf-8") as fichier: json.dump(utilisateur, fichier, indent=2, ensure_ascii=False) # Conversion chaîne <-> objet, sans passer par un fichier texte = json.dumps(utilisateur) # dict -> str utilisateur_relu = json.loads(texte) # str -> dict
Le principe est le même qu'avec json_encode/json_decode en PHP : dump/dumps sérialisent (encodent), load/loads désérialisent (décodent). Le suffixe s (pour "string") distingue les fonctions qui travaillent directement sur une chaîne de celles qui travaillent sur un fichier ouvert.
| Opération | PHP | Python |
|---|---|---|
| Objet/dict vers chaîne JSON | json_encode($donnees) | json.dumps(donnees) |
| Objet/dict vers fichier JSON | file_put_contents($f, json_encode($donnees)) | json.dump(donnees, fichier) |
| Chaîne JSON vers dict | json_decode($texte, true) | json.loads(texte) |
| Fichier JSON vers dict | json_decode(file_get_contents($f), true) | json.load(fichier) |
💡 Bon à savoir : l'option
ensure_ascii=Falseévite que les caractères accentués soient échappés sous forme d'entités unicode (éau lieu deé) dans le fichier produit, un détail utile dès que le JSON exporté doit rester directement lisible.
Aperçu rapide avec pandas
Pour un usage data au-delà d'un simple script, pandas (voir la leçon Pandas : manipulation de données) simplifie nettement la lecture de fichiers tabulaires, avec une seule ligne pour charger un CSV complet dans une structure exploitable directement pour l'analyse.
import pandas as pd # Lecture d'un CSV en une ligne, avec inférence automatique des types de colonnes df = pd.read_csv("utilisateurs.csv") # Lecture d'un JSON en une ligne df = pd.read_json("utilisateurs.json") # Écriture df.to_csv("export.csv", index=False) df.to_json("export.json", orient="records")
Cette approche fait plus que le module csv de la stdlib : pandas infère automatiquement les types de colonnes, gère nativement les valeurs manquantes, et retourne une structure (DataFrame) directement prête pour du filtrage, de l'agrégation ou du calcul statistique. La stdlib (csv, json) reste pertinente pour des scripts simples ou des besoins de contrôle fin, pendant que pandas devient l'outil de choix dès qu'une véritable analyse de données commence.
Gestion des chemins avec pathlib
pathlib est le module moderne de gestion des chemins de fichiers, qui remplace l'ancienne approche par chaînes de caractères concaténées et fonctions du module os.path. Il représente un chemin comme un objet, avec des méthodes plutôt que des manipulations de chaînes.
from pathlib import Path dossier_donnees = Path("data") / "raw" fichier = dossier_donnees / "utilisateurs.csv" print(fichier) # data/raw/utilisateurs.csv print(fichier.exists()) # True ou False print(fichier.suffix) # .csv print(fichier.stem) # utilisateurs print(fichier.parent) # data/raw # créer un dossier, y compris les parents manquants, sans erreur s'il existe déjà dossier_donnees.mkdir(parents=True, exist_ok=True) # lister tous les fichiers CSV d'un dossier for chemin_csv in dossier_donnees.glob("*.csv"): print(chemin_csv) # lecture/écriture rapide, sans passer explicitement par open() contenu = fichier.read_text(encoding="utf-8") fichier.write_text("nouvelles données", encoding="utf-8")
L'opérateur / surchargé pour concaténer des segments de chemin (dossier_donnees / "utilisateurs.csv") est la première chose qui surprend en venant de PHP, où la construction d'un chemin se fait généralement par concaténation de chaînes avec DIRECTORY_SEPARATOR ou une fonction dédiée. pathlib gère automatiquement les différences entre systèmes d'exploitation (séparateur / ou \), ce qui évite les bugs de portabilité liés à des chemins codés en dur.
Parquet
Le format Parquet est présenté en détail dans la leçon Formats de fichiers et sérialisation du cours data engineer : format binaire orienté colonne, avec schéma embarqué, pensé pour la lecture sélective de colonnes et une forte compression. Ce qui suit se limite à son usage concret en Python.
La lecture et l'écriture de Parquet passent presque toujours par pandas, qui s'appuie en interne sur la librairie pyarrow (à installer comme dépendance) pour l'implémentation réelle du format.
poetry add pandas pyarrow
import pandas as pd # Écriture : un DataFrame vers un fichier Parquet df = pd.read_csv("utilisateurs.csv") df.to_parquet("utilisateurs.parquet", engine="pyarrow", compression="snappy") # Lecture : chargement complet df = pd.read_parquet("utilisateurs.parquet") # Lecture sélective : ne charge que certaines colonnes, # ce qui exploite directement l'avantage columnar de Parquet df = pd.read_parquet("utilisateurs.parquet", columns=["id", "email"])
Le paramètre columns de read_parquet illustre concrètement ce que "orienté colonne" signifie en pratique : demander deux colonnes sur dix ne lit physiquement que ces deux colonnes sur disque, sans jamais charger le reste du fichier en mémoire, contrairement à un CSV qui doit être parcouru ligne par ligne dans son intégralité même si une seule colonne est utile au final.
# Comparaison de taille sur un même jeu de données df.to_csv("export.csv", index=False) df.to_parquet("export.parquet", compression="snappy") import os print(os.path.getsize("export.csv")) # généralement plusieurs fois plus volumineux print(os.path.getsize("export.parquet")) # compressé, format binaire
💡 Bon à savoir : en dehors des échanges ponctuels ou de l'export vers un tableur, il n'y a généralement pas de raison de choisir CSV plutôt que Parquet pour stocker de la donnée tabulaire de façon durable dans un pipeline Python : le gain de place et de vitesse de lecture est immédiat, pour un coût de mise en œuvre quasi nul grâce à pandas.