Abschnitt 19 · Lektion 2
Einlesen
Ein Export aus dem Online-Banking hat zwei Eigenheiten, die man erst bemerkt, wenn er auf dem Tisch
liegt. Für eine Datei, die man selbst geschrieben hat, reicht das Einlesen aus Abschnitt 12. Also
csv.DictReader mit Semikolon, die beiden Werkzeuge für Beträge und Daten und eine Buchung je
Zeile. Beide Eigenheiten stecken auch in der Mitgliederliste aus einem alten Vereinsprogramm, und an
der sind sie hier zuerst zu sehen.
Zwei Kodierungen, Byte für Byte
wort = "Müller"
print(wort.encode("utf-8"))
print(wort.encode("cp1252"))
print(len(wort.encode("utf-8")), "Bytes in UTF-8,", len(wort.encode("cp1252")), "in cp1252") In einer Datei stehen Bytes, also Zahlen, und die Kodierung (encoding) legt fest, welches Byte
welches Zeichen ist. Ein Byte ist eine Zahl von 0 bis 255. Hier sind die Bytes einmal zu sehen, denn
encode macht aus einem Text genau die Bytes, die in der Datei stünden.
Für A bis Z und die Ziffern sind sich die beiden Kodierungen einig. Beim ü nicht. UTF-8 schreibt es
mit zwei Bytes, \xc3\xbc, die alte Windows-Kodierung cp1252 (auch Windows-1252) mit einem,
\xfc. Dasselbe Wort ist in UTF-8 deshalb ein Byte länger.
UTF-8 ist heute die Regel, jede Datei in diesem Kurs ist so geschrieben. cp1252 stammt aus der Zeit, als Windows für Westeuropa eine eigene Tabelle hatte, und die gibt es immer noch: in älteren Programmen, in Excel unter „CSV (Trennzeichen-getrennt)“ und in den Exporten mancher Banken.
Eine Datei in der alten Kodierung
# Steht hier für das alte Vereinsprogramm. Es schreibt seine Mitgliederliste so,
# wie viele ältere Windows-Programme es tun: in cp1252, mit zwei Zeilen über der
# Tabelle.
from pathlib import Path
LISTE = """Mitgliederliste TSV Lindenau
Stand: 01.10.2026
Nachname;Vorname;Telefon;Status
Zimmermann;Ole;0176 5554443;aktiv
Krüger;Tim;0160 1112223;passiv
Schäfer;Paul;06221 44556;aktiv
Müller;Jürgen;06221 33221;aktiv
"""
def exportiere(pfad):
"""Schreibt die Mitgliederliste so, wie das alte Programm sie exportiert."""
Path(pfad).write_text(LISTE, encoding="cp1252") from pathlib import Path
import vereinsprogramm
vereinsprogramm.exportiere("mitglieder.csv")
text = Path("mitglieder.csv").read_text(encoding="utf-8")
print(text) vereinsprogramm.py spielt hier das alte Programm und schreibt die Liste in cp1252. Wenn man sie
als UTF-8 liest, bricht Python beim ersten Umlaut ab:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xfc in position 116: invalid start byte.
Übersetzt heißt das: An Stelle 116 der Datei, gezählt ab 0 wie bei einer Liste, steht das Byte
0xfc, und damit kann in UTF-8 kein Zeichen anfangen. Es ist das ü in Krüger, so wie cp1252 es
schreibt.
Das ist ein guter Fehler. Er kommt sofort, er nennt die Stelle, und man sieht ziemlich deutlich, dass die Datei nicht das ist, wofür man sie gehalten hat.
Erst UTF-8, dann cp1252
Mitgliederliste TSV Lindenau
Stand: 01.10.2026
Nachname;Vorname;Telefon;Status
Zimmermann;Ole;0176 5554443;aktiv
Krüger;Tim;0160 1112223;passiv
Schäfer;Paul;06221 44556;aktiv
Müller;Jürgen;06221 33221;aktiv # Steht hier für das alte Vereinsprogramm. Es schreibt seine Mitgliederliste so,
# wie viele ältere Windows-Programme es tun: in cp1252, mit zwei Zeilen über der
# Tabelle.
from pathlib import Path
LISTE = """Mitgliederliste TSV Lindenau
Stand: 01.10.2026
Nachname;Vorname;Telefon;Status
Zimmermann;Ole;0176 5554443;aktiv
Krüger;Tim;0160 1112223;passiv
Schäfer;Paul;06221 44556;aktiv
Müller;Jürgen;06221 33221;aktiv
"""
def exportiere(pfad):
"""Schreibt die Mitgliederliste so, wie das alte Programm sie exportiert."""
Path(pfad).write_text(LISTE, encoding="cp1252") from pathlib import Path
import vereinsprogramm
def lies_text(pfad):
"""Liest eine Textdatei, erst als UTF-8 und sonst als cp1252."""
try:
return Path(pfad).read_text(encoding="utf-8-sig"), "utf-8"
except UnicodeDecodeError:
return Path(pfad).read_text(encoding="cp1252"), "cp1252"
vereinsprogramm.exportiere("mitglieder-alt.csv")
for pfad in ["mitglieder-neu.csv", "mitglieder-alt.csv"]:
text, kodierung = lies_text(pfad)
print(pfad, kodierung, text.splitlines()[5]) Weil UTF-8 so streng ist, kann man es einfach versuchen. Wenn es gut geht, war die Datei UTF-8.
Kommt ein UnicodeDecodeError, liest lies_text dieselbe Datei noch einmal als cp1252. Das ist
dasselbe try und except wie in Lektion 9.2, nur steht im except diesmal kein Abbruch, sondern
der zweite Weg.
Zurück kommen der Text und die Kodierung, mit der es geklappt hat. Beide Listen werden richtig gelesen, auch Krüger mit ü, und zu jeder steht da, wie sie geschrieben war.
Im ersten Versuch steht utf-8-sig statt utf-8. Damit lassen sich dieselben Dateien lesen, und
zusätzlich fällt das BOM am Anfang weg, das Excel beim Speichern als „CSV UTF-8“ davorsetzt. Sonst
stünde es vor dem ersten Spaltennamen.
Andersherum geht es schief
Mitgliederliste TSV Lindenau
Stand: 01.10.2026
Nachname;Vorname;Telefon;Status
Zimmermann;Ole;0176 5554443;aktiv
Krüger;Tim;0160 1112223;passiv
Schäfer;Paul;06221 44556;aktiv
Müller;Jürgen;06221 33221;aktiv # Steht hier für das alte Vereinsprogramm. Es schreibt seine Mitgliederliste so,
# wie viele ältere Windows-Programme es tun: in cp1252, mit zwei Zeilen über der
# Tabelle.
from pathlib import Path
LISTE = """Mitgliederliste TSV Lindenau
Stand: 01.10.2026
Nachname;Vorname;Telefon;Status
Zimmermann;Ole;0176 5554443;aktiv
Krüger;Tim;0160 1112223;passiv
Schäfer;Paul;06221 44556;aktiv
Müller;Jürgen;06221 33221;aktiv
"""
def exportiere(pfad):
"""Schreibt die Mitgliederliste so, wie das alte Programm sie exportiert."""
Path(pfad).write_text(LISTE, encoding="cp1252") from pathlib import Path
import vereinsprogramm
def lies_text(pfad):
"""Liest eine Textdatei, erst als cp1252 und sonst als UTF-8."""
try:
return Path(pfad).read_text(encoding="cp1252"), "cp1252"
except UnicodeDecodeError:
return Path(pfad).read_text(encoding="utf-8-sig"), "utf-8"
vereinsprogramm.exportiere("mitglieder-alt.csv")
for pfad in ["mitglieder-neu.csv", "mitglieder-alt.csv"]:
text, kodierung = lies_text(pfad)
print(pfad, kodierung, text.splitlines()[5]) Dieselbe Funktion mit vertauschter Reihenfolge sieht genauso vernünftig aus. Für die neue Liste
kommt dabei aber Krüger heraus. Kein Fehler, kein Abbruch, nur Zeichensalat.
Die beiden Bytes, mit denen UTF-8 das ü schreibt, sind in cp1252 zwei einzelne Zeichen, Ã und ¼. Und
cp1252 hat für fast jedes Byte ein Zeichen. Damit liest es fast jede Datei, ohne sich zu beschweren,
und der except-Zweig mit UTF-8 kommt nie dran.
Die strenge Kodierung gehört deshalb nach vorn. Wenn ein Werkzeug einen Empfänger als
BÄCKEREI KRUME einliest, rechnet es danach fröhlich weiter. Nur passt die Regel für die Bäckerei
dann nicht mehr, und die Brötchen landen unter Sonstiges, ohne dass irgendwo ein Fehler steht.
Die Tabelle fängt weiter unten an
Mitgliederliste TSV Lindenau
Stand: 01.10.2026
Nachname;Vorname;Telefon;Status
Zimmermann;Ole;0176 5554443;aktiv
Krüger;Tim;0160 1112223;passiv
Schäfer;Paul;06221 44556;aktiv
Müller;Jürgen;06221 33221;aktiv import csv
with open("mitglieder-neu.csv", encoding="utf-8-sig", newline="") as datei:
for mitglied in csv.DictReader(datei, delimiter=";"):
print(mitglied)
print(mitglied["Nachname"], mitglied["Status"]) Die zweite Eigenheit steht oben in der Datei. Vor der Tabelle stehen der Name des Vereins und der Stand, dann kommt eine leere Zeile, und erst danach die Zeile mit den Spaltennamen.
csv.DictReader nimmt aber die erste Zeile als Spaltennamen, hier also „Mitgliederliste TSV
Lindenau“. Das ist dann der einzige Spaltenname, den die Dictionaries darunter kennen. Wer nach
Nachname fragt, bekommt KeyError: 'Nachname'.
Die erste Idee ist, die ersten drei Zeilen wegzuwerfen. Das hält genau bis zum nächsten Export, in dem eine Zeile mehr steht, etwa wer die Liste exportiert hat. Besser ist es, die Zeile mit den Spaltennamen zu suchen.
splitlines() macht aus dem Text eine Liste von Zeilen. Solange die erste davon nicht mit dem
ersten Spaltennamen anfängt, nimmt pop(0) sie weg (Lektion 5.2). Danach stehen die Spaltennamen
vorn, und csv.DictReader liest die Liste wie eine Datei, weil es ihm egal ist, woher seine Zeilen
kommen. Steht gar nichts darüber, wird auch nichts weggenommen.
Was die Aufgabe will
Der Export der Bank hat beide Eigenheiten. Über den Spaltennamen stehen drei Zeilen und eine leere,
und je nach Bank ist er in UTF-8 oder cp1252 geschrieben. Mit beidem muss lies_kontoauszug(pfad)
in einlesen.py zurechtkommen. Die Funktion gibt die Buchungen zurück und dazu die Kodierung, damit
das Werkzeug später sagen kann, wie die Datei geschrieben war.
Zum Mitnehmen
Eine Textdatei besteht aus Bytes, und welche Zeichen sie ergeben, hängt an der Kodierung. Mit UTF-8 gibt es einen UnicodeDecodeError, wenn die Bytes nicht passen, cp1252 liest dagegen fast alles und macht still Zeichensalat daraus. Deshalb kommt UTF-8 zuerst und cp1252 nur im Fehlerfall, und steht über den Spaltennamen noch etwas, wird ihre Zeile gesucht, statt Zeilen abzuzählen.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 5 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.