JSON und CSV
CSV-Tabellen mit dem csv-Modul lesen und schreiben, warum `newline=""` nötig ist, strukturierte Daten mit json speichern, und welche Typen sich auf der Reise verändern.
Wie möchtest du lernen?
Das Problem, das wir lösen
Seit Kapitel 23 haben wir Dateien mit .split(",") zerlegt, und es funktionierte. Jetzt eine Datei, in der ein Feld ein Komma enthält:
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']Die letzte Zeile ergab vier statt drei Felder, und die Anführungszeichen wurden Teil der Daten.
Die Anführungszeichen sind kein Fehler — das ist die CSV-Regel, und sie existiert genau dafür. Der Fehler steckt in unserem Lesecode. Und diese Regel selbst zu schreiben bedeutet, Anführungszeichen in Anführungszeichen und Zeilenumbrüche in Feldern zu behandeln — eine kleine Aufgabe, die stetig zu einer großen wird.
Die Aufgabe ist bereits erledigt.
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']Dieses Kapitel handelt von zwei Formaten — CSV für Tabellen, JSON für Struktur — und Python bringt für jedes ein eigenes Modul mit.
Am Ende dieses Kapitels kannst du
- Mit
csv.readerundcsv.DictReaderlesen und mitcsv.writerschreiben - Sagen, warum
newline=""geschrieben wird - Sagen, was
json.dumps/loadsvonjson.dump/loadunterscheidet - Sagen, welche Python-Typen aus JSON zurückkommen und welche nicht
- Zwischen CSV und JSON entscheiden
- Einen
JSONDecodeErrorlesen und das Problem finden
Voraussetzungen: Exceptions.
CSV lesen
csv.reader gibt jede Zeile als Liste zurück. Aber dann muss man sich die Positionen merken — was war noch gleich row[2]?
csv.DictReader behandelt die erste Zeile als Überschriften und macht aus jeder Zeile ein 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'>Zwei Dinge.
Der Zugriff erfolgt über den Namen, also bricht ein Umsortieren der Spalten den Code nicht. DictReader ist fast immer die bessere Wahl.
Aber price ist immer noch Text. Das csv-Modul trennt Felder, es wandelt sie nicht um — nichts in einer CSV-Datei sagt, welche Spalte eine Zahl ist. float() und int() musst du selbst aufrufen, genau wie in Kapitel 23.
CSV schreiben
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.0Beachte, dass large, sturdy allein in Anführungszeichen gesetzt wurde — und blue ink nicht, weil es nicht nötig war. Was wir beim Lesen selbst nicht konnten, mussten wir beim Schreiben auch nicht selbst machen.
Für eine Liste von Dictionaries gibt es 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 auch die Spaltenreihenfolge fest, und ohne writeheader() gibt es keine Überschriftenzeile.
Warumnewline=""? Dascsv-Modul entscheidet selbst, was eine Zeile beendet (\r\n). Wird die Datei normal geöffnet, übersetzt Python unter Windows die Zeilenenden ein zweites Mal und lässt nach jeder Zeile eine Leerzeile stehen. Auf Linux und macOS passiert diese zweite Übersetzung nicht, dort siehst du also keinen Unterschied.newline=""schaltet diese Übersetzung ab, damit sich der Code überall gleich verhält. Setze es auch beim Lesen ein — dann werden Zeilenumbrüche innerhalb von Feldern in Anführungszeichen richtig gelesen.
JSON — Struktur, die keine Tabelle halten kann
CSV ist eine Tabelle: Zeilen und Spalten, alles flach. Aber was, wenn eine Bestellung eine Liste von Positionen enthält und jede Position eine Liste von Tags? Das kann eine Tabelle nicht halten.
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 verwandelt ein Python-Objekt in Text. Die Form sieht aus wie die von Python, mit zwei Unterschieden: True wurde zu true und None wurde zu null. JSON ist eine eigene Sprache, die viele Programmiersprachen teilen, also hat sie ihre eigene Schreibweise.
Für etwas, das ein Mensch lesen soll:
import json
order = {"name": "pen", "tags": ["ink", "blue"]}
print(json.dumps(order, indent=2)){
"name": "pen",
"tags": [
"ink",
"blue"
]
}Und wieder einlesen:
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'> TrueDas ist der größte Unterschied zu CSV. price kam als Zahl zurück und stocked kam als True zurück, ohne dass irgendwo eine Umwandlung aufgerufen wurde. JSON trägt die Typen; CSV nicht.
Die vier Namen lassen sich leicht auseinanderhalten: dumps/loads arbeiten mit Text, dump/load arbeiten mit Dateien. Das angehängte s steht für 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}Was JSON nicht zurückgibt
Nicht jedes Python-Objekt kann in JSON, und nicht alles, was hineingeht, kommt unverändert zurück.
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(Vor der Fehlerzeile druckt Python auch einige Traceback-Zeilen; in diesem Kapitel wird nur die letzte Zeile gezeigt.)
Ein Tupel wird stillschweigend zur Liste. JSON hat kein Tupel, also beschwert sich das Schreiben nicht — aber was zurückkommt, ist eine Liste. Das Versprechen aus Kapitel 14, dass es sich nicht verändert, geht auf dem Weg verloren.
Ein Set geht überhaupt nicht, und das ist das bessere Verhalten — stoppen ist besser als stillschweigend falsch zu sein. Übergebe sorted(...) als Liste, wenn du es brauchst.
Ein weiteres Problem, das härter trifft:
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-Schlüssel sind immer Strings. Gibst du numerische Schlüssel, werden sie stillschweigend zu Strings und kommen als Strings zurück. Also funktionierte vorher data[1], danach wird data["1"] gebraucht.
Die Liste zum Merken ist kurz: Dictionaries, Listen, Strings, Zahlen, True/False und None sind sicher. Tupel verändern sich. Sets, Datumswerte und eigene Objekte gehen nicht.
JSON ist 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 sind einfache und doppelte Anführungszeichen gleichwertig. In JSON nicht — nur doppelte Anführungszeichen, und kein Komma nach dem letzten Element.
Die Meldung nennt eine Zeile und eine Spalte, sodass man die Stelle auch in einer großen Datei findet. Und JSONDecodeError ist eigentlich ein ValueError — also fängt ihn except ValueError mit, wie es die Familienregel des vorigen Kapitels sagt.
Was wann
Nimm CSV, wenn die Daten eine flache Tabelle sind und jemand sie in einer Tabellenkalkulation öffnen soll. Bei vielen Zeilen bleibt die Datei auch kleiner.
Nimm JSON, wenn Struktur da ist — Listen darin, Dictionaries darin — oder wenn die Typen unverändert zurückkommen müssen. Einstellungsdateien und Web-APIs sind fast immer JSON.
Ein vollständiges Beispiel
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 Dinge, die sich lohnen anzusehen.
Rein kommt CSV, raus geht JSON, und das folgt aus dem, was jedes Format ist. Die Eingabe ist eine flache Tabelle, die auch eine Tabellenkalkulation öffnen kann. Die Ausgabe ist nicht flach — eine Liste von Dingen, jedes ein Dictionary, mit einer zweiten Liste von Beschwerden daneben. Das hätte man nicht als CSV schreiben können.
large, sturdy überlebt die ganze Reise. csv hat es richtig gelesen und json hat es richtig geschrieben. Das Problem, mit dem dieses Kapitel begonnen hat, ist nirgends mehr zu sehen.
Das leere note und der leere price sind beides leerer Text, und sie verhalten sich trotzdem unterschiedlich. Ein leeres CSV-Feld ist "", nicht None. note bleibt Text und macht keine Probleme; float("") löst einen ValueError aus, also wird die clip-Zeile mit einer Beschwerde übersprungen. CSV hat kein „fehlend“, nur „leer“ — und was das bedeutet, entscheidest du.
Das start=2 ist nicht willkürlich. DictReader verbraucht die erste Zeile als Überschriften, also ist die erste Datenzeile, die es liefert, die zweite Zeile der Datei. Eine Meldung, die jemand mit der Datei abgleicht, muss so zählen wie die Datei zählt.
Wenn es schiefgeht
Eine Leerzeile nach jeder Zeile meiner CSV-Datei (unter Windows) newline="" wurde nicht übergeben. Übergib es beim Schreiben.
KeyError: 'price' — aber die Spalte ist doch da Die Überschrift stimmt nicht, in der Schreibweise oder bei den Leerzeichen. Drucke reader.fieldnames, um zu sehen, was Python wirklich gelesen hat.
Ein TypeError bei der Rechnung mit den Zahlen csv gibt alles als Text. float() oder int() muss aufgerufen werden.
Die erste Zeile wird als Daten behandelt Du benutzt csv.reader, der nichts über Überschriften weiß. Nimm DictReader, oder entferne die erste Zeile selbst.
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes Einfache Anführungszeichen oder ein überzähliges Komma. JSON ist nicht Python.
TypeError: Object of type set is not JSON serializable Ein Set, ein Datum oder ein eigenes Objekt wurde übergeben. Wandle es zuerst in eine Liste oder in Text um.
Meine Tupel kamen als Listen zurück Wie es sein sollte — JSON hat kein Tupel. Rufe danach selbst tuple(...) auf, wenn du eines brauchst.
data[1] funktionierte, und nach der Hin- und Rückreise löst es einen KeyError aus JSON-Schlüssel sind immer Strings. Jetzt ist es data["1"].
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Eine Datei mit drei Spalten, von Hand geteilt. Wie viele Felder kommen heraus?
# 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
- DEin `ValueError`
DictReader hat die Zeile gelesen. Was passiert in der zweiten Zeile?
# 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)- ADas Dictionary wird gedruckt, dann `16`
- BDas Dictionary wird gedruckt, dann `151`
- CDas Dictionary wird gedruckt, dann ein `TypeError` — `price` ist immer noch Text
- DEin `KeyError`
json.dumps bekommt ein Python-Dictionary. Was wird gedruckt?
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}
Zum Antworten brauchst du ein Konto
Melde dich an, um deine Antworten zu prüfen
Die Fragen stehen oben, und sie im Kopf durchzuarbeiten ist der wichtige Teil. Melde dich an, um die Antworten, die Erklärungen und die dreistufigen Hinweise zu sehen.
Jetzt bist du dran
Erstelle eine books.csv mit den Spalten title,author,pages,year und mindestens sechs Zeilen. Pack in mindestens einen Titel ein Komma (damit er in Anführungszeichen gesetzt werden muss), lass in einer Zeile pages leer und schreib in einer anderen Buchstaben dorthin, wo pages hingehört.
Schreib dann ein library.py, das:
- Die Datei mit
DictReaderliest und für jede schlechte Zeile eine Beschwerde mit der Zeilennummer sammelt - Eine Zusammenfassung nach Autor baut — wie viele Bücher jeder hat, und wie viele Seiten insgesamt
- Das Ganze mit
indent=2nachlibrary.jsonschreibt - Diese Datei wieder liest und bestätigt, dass die Summe unverändert ist
Dann fünf Experimente:
- Benutze
line.split(",")stattDictReader. Was passiert mit dem Titel, der ein Komma enthält? - Entferne
newline=""beim Schreiben und öffne dann die CSV-Datei und sieh nach. Unter Windows siehst du eine Leerzeile nach jeder Zeile; auf Linux und macOS siehst du keinen Unterschied — behaltenewline=""aber auch dort bei, damit sich der Code überall gleich verhält. - Pack ein Set in die Zusammenfassung (zum Beispiel das Set der Autoren) und versuche, das JSON zu schreiben. Lies dir die Meldung durch und behebe dann den Fehler.
- Öffne
library.jsonvon Hand, ändere ein doppeltes Anführungszeichen in ein einfaches und versuche es zu lesen. Nennt die Meldung eine Zeile und eine Spalte? - Benutze das Jahr als Dictionary-Key —
{2020: [...]}—, schreibe es dann und lies es zurück. Womit musst du es jetzt nachschlagen?
Diese letzten beiden machen den Punkt dieses Kapitels: In eine Datei zu schreiben heißt zu übersetzen, und jede Übersetzung verändert etwas. Zu wissen, was sich verändert, lässt dich das Format wählen; es nicht zu wissen, heißt, dass das Format für dich wählt.
Schritt 6 von 6
Herausforderung — das Kapitel-Quiz
Zehn Fragen von leicht bis schwer. Die letzten sind absichtlich knifflig.
Melde dich an, um das Quiz zu machen