Zum Inhalt springen

9. Dateien und Datenformate

Zu Zen-Modus wechseln

Alle bisherigen Programme hatten ein Gedächtnisproblem: Mit dem Programmende waren sämtliche Daten verloren. Reale Anwendungen müssen Daten dauerhaft speichern (Persistenz) und, mindestens ebenso wichtig, mit anderen Programmen austauschen: Eine Schnittsoftware exportiert eine Liste von Markern, ein Python-Skript wertet sie aus; ein eigenes Programm erzeugt eine Tabelle, die in einer Tabellenkalkulation weiterverarbeitet wird.

Dieses Kapitel behandelt beides: den grundlegenden Umgang mit Dateien in Python und die zwei wichtigsten textbasierten Datenformate, CSV für tabellarische und JSON für strukturierte Daten. Der Lehrplan spricht hier von Daten-Schnittstellen, und dieser Begriff trifft den Kern: Das Dateiformat ist der Vertrag zwischen zwei Programmen, die einander sonst nicht kennen. Hält sich der Erzeuger an das Format, kann jeder Verarbeiter die Daten lesen, unabhängig von Programmiersprache und Betriebssystem.

Der Zugriff auf Dateien läuft über die eingebaute Funktion open(), und zwar grundsätzlich in der with-Form:

with open("notes.txt", "w", encoding="utf-8") as file:
file.write("Color grading for scene 4 still missing.\n")
file.write("Check audio sync in the interview part.\n")
with open("notes.txt", encoding="utf-8") as file:
content = file.read()
print(content)

Die Bestandteile:

  • Der zweite Parameter ist der Modus: "r" lesen (Standard), "w" schreiben (Achtung: überschreibt eine bestehende Datei ersatzlos), "a" anhängen.
  • with macht die Datei zu einem Kontextmanager: Beim Verlassen des Blocks wird sie garantiert geschlossen, auch wenn mittendrin eine Exception auftritt. Offene Dateien sind eine klassische Quelle schleichender Fehler (gesperrte Dateien, nicht geschriebene Pufferinhalte); die with-Form schließt diese Fehlerquelle vollständig. open() ohne with hat in neuem Code nichts verloren.
  • encoding="utf-8" legt die Zeichenkodierung fest, also die Übersetzung zwischen Text und Bytes.

Große Dateien liest man nicht mit read() am Stück, sondern zeilenweise; die Datei selbst ist iterierbar:

with open("render.log", encoding="utf-8") as file:
for line in file:
if "ERROR" in line:
print(line.strip())

strip() entfernt den Zeilenumbruch am Ende jeder gelesenen Zeile, ein Detail, das andernfalls regelmäßig doppelte Leerzeilen in der Ausgabe erzeugt.

Fehlt die Datei, löst open() einen FileNotFoundError aus; bei fehlenden Rechten kommt ein PermissionError. Beide sind Subklassen von OSError und werden mit den Mitteln aus Kapitel 7 behandelt, und zwar dort, wo eine sinnvolle Reaktion möglich ist:

try:
with open("notes.txt", encoding="utf-8") as file:
content = file.read()
except FileNotFoundError:
content = "" # a missing notes file simply means: no notes yet

CSV (Comma-Separated Values) ist das einfachste verbreitete Datenformat: eine Textdatei, eine Zeile pro Datensatz, Felder durch Kommas getrennt, üblicherweise mit einer Kopfzeile:

filename,duration_seconds,fps
intro.mp4,90,25
interview.mp4,845,25
outro.mp4,45,50

CSV ist die Lingua franca tabellarischer Daten: Tabellenkalkulationen, Datenbanken, Kamerasysteme und Webdienste können es exportieren und importieren. Wer Daten “für Excel” bereitstellen oder von dort übernehmen soll, landet fast immer bei CSV.

Der Reflex, CSV mit line.split(",") zu zerlegen, scheitert an den Rändern des Formats: Felder können Kommas enthalten und stehen dann in Anführungszeichen ("Meier, Anna"), Anführungszeichen im Feld werden verdoppelt, Felder können Zeilenumbrüche enthalten. Ein handgeschriebener Parser zerlegt solche Dateien falsch, und zwar erst dann, wenn die ersten echten Daten eintreffen. Python bringt deshalb das Modul csv mit, das alle diese Fälle korrekt behandelt.

Am angenehmsten ist die Arbeit mit DictReader und DictWriter, die jede Zeile als Dictionary mit den Spaltennamen aus der Kopfzeile behandeln:

import csv
# reading: every row becomes a dictionary
with open("clips.csv", encoding="utf-8", newline="") as file:
reader = csv.DictReader(file)
clips = list(reader)
print(clips[0])
# {'filename': 'intro.mp4', 'duration_seconds': '90', 'fps': '25'}
# writing
clips.append({"filename": "credits.mp4", "duration_seconds": "30", "fps": "25"})
with open("clips.csv", "w", encoding="utf-8", newline="") as file:
writer = csv.DictWriter(file, fieldnames=["filename", "duration_seconds", "fps"])
writer.writeheader()
writer.writerows(clips)

Zwei Fallstricke verdienen einen eigenen Hinweis:

  • Alles ist String. CSV kennt keine Datentypen; duration_seconds kommt als '90' an, nicht als 90. Wer damit rechnen will, wandelt explizit um: int(clip["duration_seconds"]). Vergessene Umwandlungen fallen erst beim Rechnen oder Sortieren auf (Strings sortieren "9" > "845").
  • newline="" gehört bei CSV-Dateien zu jedem open(). Ohne diesen Parameter entstehen unter Windows Leerzeilen zwischen den Datensätzen; das ist eine dokumentierte Eigenheit des Zusammenspiels von csv-Modul und Zeilenende-Übersetzung.

Ein dritter Stolperstein kommt aus der Praxis im deutschsprachigen Raum: Tabellenkalkulationen mit deutschen Ländereinstellungen exportieren häufig mit Semikolon statt Komma als Trennzeichen (weil das Komma als Dezimaltrennzeichen belegt ist). Das csv-Modul verarbeitet das mit dem Parameter delimiter=";". Wer fremde CSV-Dateien einliest, wirft also zuerst einen Blick in die Datei: Trennzeichen und Kopfzeile sind Teil des Vertrags, und der Vertrag steht nirgendwo sonst.

CSV bildet flache Tabellen ab. Sobald Daten verschachtelt sind (ein Projekt enthält Clips, jeder Clip hat Marker), braucht es ein Format, das Hierarchien darstellen kann. Der Standard dafür ist JSON (JavaScript Object Notation): das Austauschformat des Webs, von praktisch jeder Programmiersprache unterstützt und auch als Konfigurations- und Projektdateiformat weit verbreitet.

{
"project": "image film",
"fps": 25,
"clips": [
{
"filename": "intro.mp4",
"duration_seconds": 90,
"markers": [12.5, 47.0, 80.2]
},
{
"filename": "interview.mp4",
"duration_seconds": 845,
"markers": []
}
]
}

JSON kennt sechs Werttypen, und sie entsprechen fast eins zu eins den Python-Grundtypen:

JSONPython
object {...}dict
array [...]list
stringstr
numberint / float
true / falseTrue / False
nullNone

Anders als CSV ist JSON also typisiert: Die 90 im Beispiel ist eine Zahl, kein String. Zu beachten sind die Unterschiede in der Schreibweise: JSON verlangt doppelte Anführungszeichen um Schlüssel und Strings, schreibt true klein und kennt keine Kommentare und kein Komma nach dem letzten Element.

import json
# reading: the whole file becomes nested dicts and lists
with open("project.json", encoding="utf-8") as file:
project = json.load(file)
print(project["clips"][0]["markers"]) # [12.5, 47.0, 80.2]
# modifying and writing back
project["clips"][0]["markers"].append(85.0)
with open("project.json", "w", encoding="utf-8") as file:
json.dump(project, file, indent=2, ensure_ascii=False)

json.load() liefert die komplette Struktur als verschachtelte Dictionaries und Listen; der Zugriff funktioniert mit den bekannten Mitteln, ohne neue Konzepte. Beim Schreiben sorgt indent=2 für eingerückte, menschenlesbare Ausgabe, und ensure_ascii=False erhält Umlaute als Umlaute statt als ä-Sequenzen.

json.dump() kann mit eigenen Klassen nichts anfangen; es kennt nur die Typen der Tabelle oben. Das Standardmuster ist ein Methodenpaar, das zwischen Objekt und Dictionary übersetzt:

class Clip:
def __init__(self, filename, duration_seconds, markers=None):
self.filename = filename
self.duration_seconds = duration_seconds
self.markers = markers if markers is not None else []
def to_dict(self):
return {
"filename": self.filename,
"duration_seconds": self.duration_seconds,
"markers": self.markers,
}
@classmethod
def from_dict(cls, data):
return cls(data["filename"], data["duration_seconds"], data["markers"])
clips = [Clip("intro.mp4", 90), Clip("outro.mp4", 45, [3.0])]
with open("clips.json", "w", encoding="utf-8") as file:
json.dump([clip.to_dict() for clip in clips], file, indent=2)
with open("clips.json", encoding="utf-8") as file:
loaded = [Clip.from_dict(entry) for entry in json.load(file)]

(@classmethod markiert eine Methode, die auf der Klasse statt auf einem Objekt aufgerufen wird und hier als benannter Konstruktor dient.) Damit ist der Kreis zur Objektorientierung geschlossen: Das Programm arbeitet intern mit Objekten samt Methoden und Kapselung; an der Daten-Schnittstelle werden sie in das neutrale Austauschformat übersetzt und zurück.

Robustheit: Wenn die Datei nicht hält, was sie verspricht

Abschnitt betitelt „Robustheit: Wenn die Datei nicht hält, was sie verspricht“

Daten-Schnittstellen haben eine unangenehme Eigenschaft: Die andere Seite des Vertrags ist nicht unter eigener Kontrolle. Dateien fehlen, sind halb geschrieben, von Hand editiert oder schlicht kaputt. Ein Programm, das fremde Dateien einliest, behandelt deshalb drei Fehlerklassen (die Werkzeuge dazu liefert Kapitel 7):

def load_project(path):
"""Load a project file, raising ProjectFileError with a clear message."""
try:
with open(path, encoding="utf-8") as file:
data = json.load(file)
except FileNotFoundError:
raise ProjectFileError(f"project file not found: {path}")
except json.JSONDecodeError as error:
raise ProjectFileError(f"not valid JSON: {path} (line {error.lineno})")
if "clips" not in data:
raise ProjectFileError(f"missing 'clips' entry in: {path}")
return data
  1. Die Datei fehlt (FileNotFoundError): je nach Kontext ein normaler Fall (noch kein Speicherstand) oder ein Fehler.
  2. Die Datei ist kein gültiges JSON (json.JSONDecodeError): Die Exception nennt sogar die Zeile des Syntaxfehlers, und eine gute Fehlermeldung reicht diese Information weiter.
  3. Das JSON ist gültig, aber inhaltlich falsch: Der Vertrag umfasst mehr als die Syntax; erwartete Schlüssel und Wertebereiche müssen geprüft werden, bevor das Programm damit rechnet.

Die Funktion übersetzt alle drei Fälle in eine eigene, fachliche Exception (Kapitel 7) mit präziser Meldung. Der Aufrufer behandelt damit einen einzigen Fehlertyp, und die Fehlermeldungen taugen für Endbenutzer. Genau diese Sorgfalt an der Datengrenze unterscheidet ein robustes Programm von einem, das bei der ersten unerwarteten Datei mit einem Traceback abstürzt; im Abschlussprojekt ist sie Teil der Anforderungen.

KriteriumCSVJSON
Datenformflache Tabellebeliebig verschachtelt
Datentypenkeine (alles String)Zahlen, Strings, Bool, null
ZielgruppeTabellenkalkulation, DatenimportProgramme, Web-APIs, Konfiguration
Lesbarkeit für Laienhoch (in Excel öffnen)mittel
Standardisierungschwach (Trennzeichen, Encoding variieren)streng

Faustregel: Tabellen für Menschen und Tabellenkalkulationen als CSV, strukturierte Daten zwischen Programmen als JSON. Häufig braucht ein Programm beides, etwa JSON als internes Speicherformat und CSV als Exportfunktion für Auswertungen.

Lernergebnisse: Was Sie nach diesem Kapitel können sollten

Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“

Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:

  • Anwenden: Textdateien mit open() im with-Block lesen und schreiben, mit korrektem Modus und UTF-8-Encoding.
  • Anwenden: CSV-Dateien mit dem csv-Modul (DictReader/DictWriter) verarbeiten und die typischen Fallen (Stringtypen, Trennzeichen, newline) vermeiden.
  • Anwenden: JSON-Dateien mit json.load/json.dump verarbeiten und die Typabbildung zwischen JSON und Python nennen.
  • Entwerfen: eigene Objektstrukturen über to_dict/from_dict in Austauschformate überführen und zurück.
  • Erklären: Dateiformate als Daten-Schnittstellen (Verträge zwischen Programmen) einordnen und die Wahl zwischen CSV und JSON begründen.
  • Anwenden: Datei- und Formatfehler mit Exceptions robust behandeln und in verständliche Fehlermeldungen übersetzen.
  • Aufgabe 17 - Notenverwaltung mit CSV und JSON
  • Aufgabe 18 - JSON-Werkstatt: Die Mediendatenbank