JSON en CSV
Tabellen met aanhalingstekens lezen en schrijven met de csv-module, waarom `newline=""` nodig is, gestructureerde data bewaren met json, en welke typen onderweg veranderen.
Hoe wil je leren?
Het probleem dat we oplossen
Sinds hoofdstuk 23 splitsen we bestanden met .split(","), en dat werkte. Nu een bestand waarin één veld een komma binnenin bevat:
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']De laatste rij kwam eruit met vier velden in plaats van drie, en de aanhalingstekens werden onderdeel van de data.
De aanhalingstekens zijn geen fout — het is de regel van CSV, en die bestaat precies voor deze situatie. De fout zit in onze leescode. En die regel zelf uitschrijven betekent omgaan met aanhalingstekens binnen aanhalingstekens en met regelafbrekingen binnen velden: een klein klusje dat steeds groter wordt.
Het werk is al gedaan.
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']Dit hoofdstuk gaat over twee formaten — CSV voor tabellen, JSON voor structuur — en voor elk ervan zit er een module in Python.
Aan het einde van dit hoofdstuk kun je
- Lezen met
csv.readerencsv.DictReader, en schrijven metcsv.writer - Uitleggen waarom
newline=""wordt geschreven - Uitleggen wat
json.dumps/loadsonderscheidt vanjson.dump/load - Aangeven welke Python-typen terugkomen uit JSON en welke niet
- Kiezen tussen CSV en JSON
- Een
JSONDecodeErrorlezen en het probleem vinden
Voorkennis: Exceptions — afhandelen wat breekt.
CSV lezen
csv.reader geeft elke rij als lijst. Maar dan moet je onthouden welke positie wat is — wat was row[2] ook alweer?
csv.DictReader behandelt de eerste rij als kopregel en maakt van elke rij een dictionary:
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'>Twee dingen.
Toegang gaat via de naam, dus het omwisselen van kolommen breekt de code niet. DictReader is bijna altijd de betere keuze.
Maar price is nog steeds tekst. De csv-module scheidt velden, maar zet ze niet om — niets in een CSV-bestand zegt welke kolom een getal is. float() en int() moet je zelf aanroepen, precies zoals in hoofdstuk 23.
CSV schrijven
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.0Let op: alleen large, sturdy kreeg aanhalingstekens — blue ink niet, omdat dat niet nodig was. Wat wij zelf niet konden lezen, hoefden we ook niet zelf te schrijven.
Voor een lijst van dictionaries, 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 legt ook de volgorde van de kolommen vast, en zonder writeheader() is er geen kopregel.
Waaromnewline=""? Decsv-module bepaalt zelf wat een rij afsluit (\r\n). Als het bestand gewoon wordt geopend, zet Python op Windows de regeleindes nog een keer om, en dan staat er na elke rij een lege regel. Op Linux en macOS gebeurt die tweede omzetting niet, dus daar zie je geen verschil.newline=""zet die omzetting uit, zodat de code overal hetzelfde werkt. Schrijf het ook bij het lezen — dan worden regelafbrekingen binnen aanhalingstekens goed gelezen.
JSON — structuur die een tabel niet kan bevatten
CSV is een tabel: rijen en kolommen, alles plat. Maar wat als een bestelling een lijst van regels bevat, en elke regel een lijst van tags? Een tabel kan dat niet bevatten.
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 zet een Python-object om in tekst. De vorm lijkt op die van Python, met twee verschillen: True werd true en None werd null. JSON is een aparte taal die door veel programmeertalen wordt gedeeld, dus het heeft zijn eigen spelling.
Voor iets wat een mens gaat lezen:
import json
order = {"name": "pen", "tags": ["ink", "blue"]}
print(json.dumps(order, indent=2)){
"name": "pen",
"tags": [
"ink",
"blue"
]
}En terug:
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'> TrueDit is het grootste verschil met CSV. price kwam terug als getal en stocked kwam terug als True, zonder dat er ook maar één conversie werd aangeroepen. JSON bewaart de typen; CSV doet dat niet.
De vier namen zijn makkelijk uit elkaar te houden: dumps/loads werken met tekst, dump/load werken met bestanden. De achterliggende s staat voor 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}Wat JSON niet teruggeeft
Niet elk Python-object kan in JSON, en niet alles wat erin gaat komt hetzelfde terug.
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(Vóór de foutregel print Python ook enkele traceback-regels; in dit hoofdstuk wordt alleen de laatste regel getoond.)
Een tuple wordt stilletjes een lijst. JSON kent geen tuple, dus bij het schrijven komt er geen melding — maar wat terugkomt is een lijst. De belofte uit hoofdstuk 14 dat dit niet zou veranderen, gaat onderweg verloren.
Een set gaat helemaal niet mee, en dat is het betere gedrag — stoppen is beter dan stilletjes fout zijn. Geef sorted(...) als lijst mee als je het nodig hebt.
Nog één, die harder bijt:
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'}JSON-sleutels zijn altijd strings. Geef er numerieke sleutels in en ze worden stilletjes strings, en komen als strings terug. Dus data[1] werkte eerst, en na de reis is data["1"] nodig.
De lijst om te onthouden is kort: dictionaries, lijsten, strings, getallen, True/False en None zijn veilig. Tuples veranderen. Sets, datums en eigen objecten gaan niet mee.
JSON is streng
import json
print(json.loads("{'name': 'pen'}"))json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)In Python zijn enkele en dubbele aanhalingstekens gelijk. In JSON zijn ze dat niet — alleen dubbele aanhalingstekens, en geen komma na het laatste item.
Het bericht geeft een regel en een kolom, dus de plek is zelfs in een groot bestand te vinden. En JSONDecodeError is eigenlijk een ValueError — dus except ValueError vangt hem ook, zoals de familieregel uit het vorige hoofdstuk zegt.
Welke, wanneer
Kies CSV als de data een platte tabel is en een mens het in een spreadsheet gaat openen. Bij veel rijen blijft het bestand ook kleiner.
Kies JSON als er structuur is — lijsten binnenin, dictionaries binnenin — of als de typen intact moeten terugkomen. Instellingenbestanden en web-API's zijn bijna altijd JSON.
Een compleet voorbeeld
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: ''"
]
}Vier dingen die het bekijken waard zijn.
CSV gaat erin en JSON gaat eruit, en dat volgt uit wat elk formaat is. De invoer is een platte tabel, die ook een spreadsheet kan openen. De uitvoer is niet plat — een lijst van dingen, elk een dictionary, met daarnaast een tweede lijst met klachten. Dat had niet als CSV geschreven kunnen worden.
large, sturdy blijft de hele reis intact. csv las het goed en json schreef het goed. Het probleem waarmee dit hoofdstuk begon is nergens meer te zien.
De lege note en de lege price zijn allebei lege tekst, en ze eindigen anders. Een leeg CSV-veld is "", niet None. note blijft tekst en veroorzaakt niets; float("") geeft een ValueError, dus de clip-rij wordt overgeslagen met een klacht. CSV kent geen "ontbrekend", alleen "leeg" — en wat dat betekent, moet jij beslissen.
De start=2 is niet willekeurig. DictReader gebruikt de eerste rij als kopregel, dus de eerste datarij die hij teruggeeft is de tweede regel van het bestand. Een bericht dat iemand tegen het bestand gaat controleren moet tellen zoals het bestand telt.
Wanneer het breekt
Een lege regel na elke rij van mijn CSV (op Windows) newline="" is niet meegegeven. Geef het mee bij het schrijven.
KeyError: 'price' — maar de kolom staat er gewoon De kopregel komt niet overeen, in spelling of in witruimte. Print reader.fieldnames om te zien wat Python werkelijk heeft gelezen.
Een TypeError bij het rekenen met de getallen csv geeft alles als tekst. float() of int() moet worden aangeroepen.
De eerste rij wordt als data behandeld Je gebruikt csv.reader, die niets van kopregels weet. Neem DictReader, of haal de eerste rij zelf weg.
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes Enkele aanhalingstekens, of een komma aan het eind. JSON is geen Python.
TypeError: Object of type set is not JSON serializable Er is een set, een datum of een eigen object meegegeven. Zet het eerst om naar een lijst of tekst.
Mijn tuples kwamen terug als lijsten Zoals het hoort — JSON kent geen tuple. Roep daarna zelf tuple(...) aan als je er een nodig hebt.
data[1] werkte, en na een reis heen en terug geeft het KeyError JSON-sleutels zijn altijd strings. Het is nu data["1"].
Stap 4 van 6 — Voorspellen
Test je begrip
Een bestand met drie kolommen, met de hand gesplitst. Hoeveel velden komen eruit?
# 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
- DEen `ValueError`
DictReader heeft de rij gelezen. Wat gebeurt er op de tweede regel?
# 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)- ADe dictionary wordt geprint, daarna `16`
- BDe dictionary wordt geprint, daarna `151`
- CDe dictionary wordt geprint, daarna een `TypeError` — `price` is nog steeds tekst
- DEen `KeyError`
Aan json.dumps wordt een Python-dictionary gegeven. Wat wordt geprint?
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}
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
Maak een books.csv met de kolommen title,author,pages,year en minstens zes rijen. Zet een komma in minstens één titel (dus die moet tussen aanhalingstekens), laat pages leeg in één rij, en zet letters waar pages hoort in een andere.
Schrijf daarna library.py dat:
- Het bestand leest met
DictReader, en voor elke slechte rij een klacht met een regelnummer verzamelt - Een overzicht per auteur maakt — hoeveel boeken elk heeft, en hoeveel pagina's dat in totaal zijn
- Het hele resultaat schrijft naar
library.jsonmetindent=2 - Dat bestand weer inleest en controleert dat het totaal niet is veranderd
Dan vijf experimenten:
- Gebruik
line.split(",")in plaats vanDictReader. Wat gebeurt er met de titel met de komma? - Verwijder
newline=""bij het schrijven, open daarna het CSV-bestand en kijk. Op Windows zie je een lege regel na elke rij; op Linux en macOS zie je geen verschil — maar zetnewline=""daar ook, zodat de code overal hetzelfde werkt. - Zet een set in het overzicht (bijvoorbeeld de set van auteurs) en probeer de JSON te schrijven. Lees de melding en repareer het daarna.
- Open
library.jsonmet de hand, verander één dubbele aanhalingsteken in een enkele, en probeer het in te lezen. Geeft de melding een regel en een kolom? - Gebruik het jaar als dictionary-sleutel —
{2020: [...]}— schrijf het weg en lees het terug. Waarmee moet je het nu opzoeken?
Die laatste twee maken het punt van dit hoofdstuk: naar een bestand schrijven is vertalen, en elke vertaling verandert iets. Weten wat er verandert laat je het formaat kiezen; het niet weten betekent dat het formaat voor je kiest.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken