JSON et CSV
Lire et écrire des tableaux entre guillemets avec le module csv, pourquoi `newline=""` est nécessaire, conserver des données structurées avec json, et quels types changent pendant le trajet.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Depuis le chapitre 23, nous découpons les fichiers avec .split(","), et cela fonctionnait. Maintenant, un fichier où un champ contient une virgule à l'intérieur :
name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0with open("items.csv", "r", encoding="utf-8") as fh:
for line in fh:
print(line.strip().split(","))['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', '"large', ' sturdy"', '850.0']La dernière ligne est sortie avec quatre champs au lieu de trois, et les « guillemets » sont devenus partie des données.
Ces « guillemets » ne sont pas une erreur — c'est la règle du CSV, et ils existent précisément pour cette situation. L'erreur est dans notre code de lecture. Et écrire cette règle vous-même, c'est gérer des guillemets dans des guillemets et des sauts de ligne à l'intérieur des champs ; une petite tâche qui finit par devenir une grande.
Ce travail a déjà été fait.
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
for row in csv.reader(fh):
print(row)['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', 'large, sturdy', '850.0']Ce chapitre traite de deux formats — CSV pour les tableaux, JSON pour la structure — et Python fournit déjà un module pour chacun.
À la fin de ce chapitre vous pourrez
- Lire avec
csv.readeretcsv.DictReader, et écrire aveccsv.writer - Expliquer pourquoi on écrit
newline="" - Dire ce qui sépare
json.dumps/loadsdejson.dump/load - Dire quels types Python reviennent depuis JSON et lesquels ne reviennent pas
- Choisir entre CSV et JSON
- Lire un
JSONDecodeErroret trouver le problème
Prérequis : Exceptions.
Lire du CSV
csv.reader donne chaque ligne sous forme de liste. Mais alors il faut se souvenir des positions — c'était quoi déjà row[2] ?
csv.DictReader prend la première ligne comme en-têtes et transforme chaque ligne en dictionnaire :
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
for row in csv.DictReader(fh):
print(row["name"], "-", row["price"], type(row["price"]))pen - 15.0 <class 'str'>
bag - 850.0 <class 'str'>Deux choses.
L'accès se fait par nom, donc réordonner les colonnes ne casse pas le code. DictReader est presque toujours le meilleur choix.
Mais price reste du texte. Le module csv sépare les champs, il ne les convertit pas — rien dans un fichier CSV ne dit quelle colonne est un nombre. float() et int() sont à vous à appeler, exactement comme dans le chapitre 23.
Écrire du CSV
import csv
rows = [["pen", "blue ink", 15.0], ["bag", "large, sturdy", 850.0]]
with open("out.csv", "w", encoding="utf-8", newline="") as fh:
writer = csv.writer(fh)
writer.writerow(["name", "note", "price"])
writer.writerows(rows)name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0Remarquez que large, sturdy a été mis entre guillemets tout seul — et pas blue ink, parce que ce n'était pas nécessaire. Ce que nous ne pouvions pas lire nous-mêmes, nous n'avons pas eu à l'écrire non plus.
Pour une liste de dictionnaires, DictWriter :
import csv
rows = [
{"name": "pen", "price": 15.0},
{"name": "bag", "price": 850.0},
]
with open("out.csv", "w", encoding="utf-8", newline="") as fh:
writer = csv.DictWriter(fh, fieldnames=["name", "price"])
writer.writeheader()
writer.writerows(rows)name,price
pen,15.0
bag,850.0fieldnames fixe aussi l'ordre des colonnes, et sans writeheader() il n'y a pas de ligne d'en-têtes.
Pourquoinewline=""? Le modulecsvdécide lui-même de ce qui termine une ligne (\r\n). Si le fichier est ouvert normalement, sous Windows Python traduit les fins de ligne une seconde fois, ce qui laisse une ligne vide après chaque ligne du tableau. Sous Linux et macOS cette seconde traduction n'a pas lieu, donc on n'y voit aucune différence.newline=""désactive cette traduction, pour que le code se comporte partout de la même manière. Écrivez-le aussi à la lecture — ainsi les sauts de ligne à l'intérieur des champs entre guillemets sont lus correctement.
JSON — une structure qu'un tableau ne peut pas contenir
Le CSV est un tableau : des lignes et des colonnes, tout est à plat. Mais que se passe-t-il si une commande contient une liste d'articles, et que chaque article contient une liste d'étiquettes ? Un tableau ne peut pas contenir cela.
import json
order = {"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": True, "note": None}
print(json.dumps(order)){"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": true, "note": null}json.dumps transforme un objet Python en texte. La forme ressemble à celle de Python, avec deux différences : True est devenu true et None est devenu null. JSON est un langage à part, partagé entre de nombreux langages de programmation, il a donc sa propre orthographe.
Pour quelque chose qu'une personne lira :
import json
order = {"name": "pen", "tags": ["ink", "blue"]}
print(json.dumps(order, indent=2)){
"name": "pen",
"tags": [
"ink",
"blue"
]
}Et pour le relire :
import json
text = '{"name": "pen", "price": 15.0, "stocked": true, "note": null}'
order = json.loads(text)
print(order)
print(type(order["price"]), type(order["stocked"]), order["note"] is None){'name': 'pen', 'price': 15.0, 'stocked': True, 'note': None}
<class 'float'> <class 'bool'> TrueC'est là la plus grande différence avec le CSV. price est revenu en nombre et stocked en True, sans aucune conversion appelée. JSON porte les types ; le CSV non.
Les quatre noms sont faciles à ne pas mélanger : dumps/loads travaillent avec du texte, dump/load travaillent avec des fichiers. Le s final vient de string.
import json
from pathlib import Path
order = {"name": "pen", "price": 15.0}
Path("order.json").write_text(json.dumps(order, indent=2) + "\n", encoding="utf-8")
with open("order.json", "r", encoding="utf-8") as fh:
back = json.load(fh)
print(back){'name': 'pen', 'price': 15.0}Ce que JSON ne renvoie pas
Tous les objets Python ne peuvent pas entrer dans JSON, et tout ce qui entre ne ressort pas identique.
import json
data = {"pair": (1, 2), "numbers": {3, 4}}
print(json.dumps({"pair": data["pair"]}))
print(json.dumps(data)){"pair": [1, 2]}
TypeError: Object of type set is not JSON serializable(Avant la ligne d'erreur, Python imprime aussi quelques lignes de traceback ; dans ce chapitre, seule la dernière ligne est montrée.)
Un tuple devient silencieusement une liste. JSON n'a pas de tuple, donc l'écriture ne soulève aucune plainte — mais ce qui revient est une liste. La promesse du chapitre 14 qu'il ne changerait pas se perd en chemin.
Un ensemble n'entre pas du tout, et c'est le meilleur comportement — s'arrêter vaut mieux qu'avoir tort en silence. Envoyez sorted(...) sous forme de liste si vous en avez besoin.
Un autre point, plus délicat :
import json
data = {1: "pen", 2: "bag"}
text = json.dumps(data)
print(text)
print(json.loads(text)){"1": "pen", "2": "bag"}
{'1': 'pen', '2': 'bag'}Les clés JSON sont toujours des chaînes. Donnez-lui des clés numériques et elles deviennent silencieusement des chaînes, et reviennent en chaînes. Avant, data[1] marchait ; après, il faut data["1"].
La liste à retenir est courte : les dictionnaires, les listes, les chaînes, les nombres, True/False et None sont sûrs. Les tuples changent. Les ensembles, les dates et vos propres objets n'entrent pas.
JSON est strict
import json
print(json.loads("{'name': 'pen'}"))json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)En Python, les guillemets simples et doubles sont équivalents. En JSON, non — uniquement les « guillemets doubles », et pas de virgule après le dernier élément.
Le message donne une ligne et une colonne, donc on peut trouver l'endroit même dans un gros fichier. Et JSONDecodeError est en fait un ValueError — donc except ValueError l'attrape aussi, comme le dit la règle des familles du chapitre précédent.
Lequel, et quand
Choisissez le CSV quand les données sont un tableau à plat et qu'une personne l'ouvrira dans un tableur. Avec beaucoup de lignes, le fichier reste aussi plus petit.
Choisissez JSON quand il y a de la structure — des listes à l'intérieur, des dictionnaires à l'intérieur — ou quand les types doivent revenir intacts. Les fichiers de paramètres et les API web sont presque toujours en JSON.
Un exemple complet
items.csv :
name,note,price,quantity
pen,blue ink,15.0,3
bag,"large, sturdy",850.0,1
ink,,120.0,2
clip,broken,,4main.py :
"""Read a CSV of items, total it, and write the result as JSON."""
import csv
import json
from pathlib import Path
TAX_RATE = 0.15
def read_items(path):
"""Returns the usable rows and the complaints. csv handles the quoting."""
items = []
problems = []
with open(path, "r", encoding="utf-8", newline="") as fh:
for number, row in enumerate(csv.DictReader(fh), start=2):
try:
items.append({
"name": row["name"],
"note": row["note"],
"amount": round(
float(row["price"]) * int(row["quantity"]) * (1 + TAX_RATE), 2),
})
except ValueError as err:
problems.append(f"line {number}: {err}")
return items, problems
def main():
items, problems = read_items(Path("items.csv"))
summary = {
"items": items,
"total": round(sum(item["amount"] for item in items), 2),
"skipped": problems,
}
Path("summary.json").write_text(
json.dumps(summary, indent=2) + "\n", encoding="utf-8")
print(Path("summary.json").read_text(encoding="utf-8"), end="")
if __name__ == "__main__":
main(){
"items": [
{
"name": "pen",
"note": "blue ink",
"amount": 51.75
},
{
"name": "bag",
"note": "large, sturdy",
"amount": 977.5
},
{
"name": "ink",
"note": "",
"amount": 276.0
}
],
"total": 1305.25,
"skipped": [
"line 5: could not convert string to float: ''"
]
}Quatre points à retenir.
Le CSV entre et le JSON sort, et cela découle de ce qu'est chaque format. L'entrée est un tableau à plat, qu'un tableur peut aussi ouvrir. La sortie n'est pas à plat — une liste de choses, chacune un dictionnaire, avec à côté une seconde liste de plaintes. Cela n'aurait pas pu être écrit en CSV.
large, sturdy survit à tout le trajet. csv l'a lu correctement et json l'a écrit correctement. Le problème avec lequel ce chapitre a commencé est hors de vue.
La note vide et le price vide sont tous les deux du texte vide, et ils finissent différemment. Un champ CSV vide est "", pas None. note reste du texte et ne provoque rien ; float("") lève un ValueError, donc la ligne clip est sautée avec une plainte. Le CSV n'a pas de « manquant », seulement « vide » — et c'est à vous de décider ce que cela veut dire.
Le start=2 n'est pas arbitraire. DictReader consomme la première ligne comme en-têtes, donc la première ligne de données qu'il donne est la deuxième ligne du fichier. Un message qu'on vérifiera contre le fichier doit compter comme le fichier compte.
Quand ça casse
Une ligne vide après chaque ligne de mon CSV (sous Windows) newline="" n'a pas été passé. Passez-le à l'écriture.
KeyError: 'price' — mais la colonne est juste là L'en-tête ne correspond pas, dans l'orthographe ou dans les espaces. Imprimez reader.fieldnames pour voir ce que Python a réellement lu.
Un TypeError en faisant de l'arithmétique avec les nombres csv donne tout sous forme de texte. Il faut appeler float() ou int().
La première ligne est traitée comme des données Vous utilisez csv.reader, qui ne connaît rien aux en-têtes. Prenez DictReader, ou enlevez la première ligne vous-même.
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes Des guillemets simples, ou une virgule finale. JSON n'est pas Python.
TypeError: Object of type set is not JSON serializable Un ensemble, une date ou un objet à vous a été passé. Convertissez-le d'abord en liste ou en texte.
Mes tuples sont revenus sous forme de listes C'est normal — JSON n'a pas de tuple. Appelez tuple(...) vous-même ensuite si vous en avez besoin.
data[1] marchait, et après un aller-retour il lève un KeyError Les clés JSON sont toujours des chaînes. C'est data["1"] maintenant.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Un fichier à trois colonnes, découpé à la main. Combien de champs en sortent ?
# items.csv
# name,note,price
# pen,blue ink,15.0
# bag,"large, sturdy",850.0
with open("items.csv", "r", encoding="utf-8") as fh:
for line in fh:
print(len(line.strip().split(",")))- A3 3 4
- B3 3 3
- C3 3 5
- DUn `ValueError`
DictReader a lu la ligne. Que se passe-t-il à la seconde ligne ?
# items.csv
# name,price
# pen,15
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
row = next(csv.DictReader(fh))
print(row)
print(row["price"] + 1)- ALe dictionnaire s'imprime, puis `16`
- BLe dictionnaire s'imprime, puis `151`
- CLe dictionnaire s'imprime, puis un `TypeError` — `price` est encore du texte
- DUn `KeyError`
json.dumps reçoit un dictionnaire Python. Qu'est-ce qui s'imprime ?
import json
print(json.dumps({"name": "pen", "stocked": True, "note": None}))- A{'name': 'pen', 'stocked': True, 'note': None}
- B{"name": "pen", "stocked": True, "note": None}
- C{"name": "pen", "stocked": true}
- D{"name": "pen", "stocked": true, "note": null}
Répondre nécessite un compte
Connectez-vous pour vérifier vos réponses
Les questions sont ci-dessus, et les résoudre de tête est ce qui compte vraiment. Connectez-vous pour voir les réponses, les explications et les indices à trois niveaux.
À vous de jouer
Créez un books.csv avec les colonnes title,author,pages,year et au moins six lignes. Mettez une virgule dans au moins un titre (pour qu'il doive être mis entre guillemets), laissez pages vide dans une ligne, et mettez des lettres là où pages devrait se trouver dans une autre.
Écrivez ensuite un library.py qui :
- Lit le fichier avec
DictReaderen collectant une plainte avec un numéro de ligne pour chaque mauvaise ligne - Construit un résumé par auteur — combien de livres chacun, et combien de pages au total
- Écrit le tout dans
library.jsonavecindent=2 - Relit ce fichier et confirme que le total n'a pas changé
Puis cinq expériences :
- Utilisez
line.split(",")au lieu deDictReader. Que devient le titre qui contient la virgule ? - Enlevez
newline=""à l'écriture, puis ouvrez le fichier CSV et regardez. Sous Windows, vous verrez une ligne vide après chaque ligne ; sous Linux et macOS, vous ne verrez aucune différence — mais gardeznewline=""là aussi, pour que le code se comporte partout de la même manière. - Mettez un ensemble dans le résumé (l'ensemble des auteurs, par exemple) et essayez d'écrire le JSON. Lisez le message, puis corrigez-le.
- Ouvrez
library.jsonà la main, changez un guillemet double en guillemet simple, et essayez de le lire. Le message donne-t-il une ligne et une colonne ? - Utilisez l'année comme clé de dictionnaire —
{2020: [...]}— puis écrivez le dictionnaire et relisez-le. Avec quoi devez-vous maintenant chercher l'année ?
Les deux dernières montrent le point de ce chapitre : écrire dans un fichier, c'est traduire, et toute traduction change quelque chose. Savoir ce qui change vous permet de choisir le format ; ne pas le savoir, c'est le format qui choisit pour vous.
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Connectez-vous pour faire le quiz