Zum Inhalt
wannacode

Daten in Dateien

Kurz gesagt: CSV ist eine Textdatei, in der jede Zeile ein Datensatz ist und die Werte durch Kommas getrennt sind; in Python teilt das Modul csv die Zeilen und liest die Spalten nach Namen.

Problem

Text, aber du brauchst Zahlen

Das Lager exportiert seine Produkte in eine Datei. Jede Zeile ist ein Produkt: Name, Preis und Bestand, durch Kommas getrennt. Die erste Zeile sagt, was in jeder Spalte steht:

products.csv
Name,Preis,Bestand
Brot,1.20,30
Milch,2.10,18
Kaffeebohnen,18.90,12

Die Datei lesen kannst du schon. Aber für das Programm ist die Zeile nur Text:

line = "Brot,1.20,30"
print(len(line))
print(line[5])
Ausgabe
12
1

Es weiß nicht, dass 1.20 ein Preis ist und 30 eine Anzahl. Um auszurechnen, was die Ware wert ist, musst du jede Zeile in etwas mit Feldern verwandeln: in ein Product-Objekt.

Die Idee

CSV: Zeile zu Objekt

So eine Datei heißt CSV (comma-separated values, „durch Kommas getrennte Werte“). Excel liest sie, Datenbanken lesen sie, jedes Programm liest sie. Eine Zeile wird in drei Schritten zum Objekt: split, jeden Teil in den passenden Typ umwandeln, der Konstruktor.

Am bequemsten ist das eine Methode in der Klasse selbst, die kein Objekt braucht, sondern eins erzeugt: @staticmethod.
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
@staticmethod
def from_csv(line):
f = line.split(",")
if len(f) != 3:
raise ValueError(f"3 Spalten erwartet, es sind {len(f)}")
return Product(f[0].strip(), float(f[1]), int(f[2]))
def to_csv(self):
return f"{self.name},{self.price},{self.stock}"
p = Product.from_csv("Kaffeebohnen,18.90,12")
print(f"{p.name}: {p.price:.2f} € x {p.stock} = {p.price * p.stock:.2f} €")
Ausgabe
Kaffeebohnen: 18.90 € x 12 = 226.80 €

from_csv ist die einzige Stelle, die weiß, wie die Zeile aussieht. Ändert sich das Format, änderst du nur sie. Die Prüfung der Spaltenanzahl wirft einen ValueError mit einer klaren Meldung, statt dass das Programm später mit einem Index außerhalb der Liste abbricht. float und int werfen selbst einen ValueError, wenn der Text keine Zahl ist, und entfernen selbst die Leerzeichen um die Zahl.

Was from_csv mit der Zeile macht, Schritt für Schritt, mit seinem Rumpf Zeile für Zeile ausgeschrieben:

line = "Kaffeebohnen,18.90,12"
f = line.split(",")
name = f[0]
price = float(f[1])
stock = int(f[2])
p = Product(name, price, stock)
print(f"{p.name}: {p.price} € x {p.stock}")
print(f"{p.price * p.stock:.2f} €")
Schritt für Schritt, in Worten
  1. Die Zeile aus der Datei ist ein String. Für das Programm ist die 18.90 darin nur vier Zeichen.
  2. split schneidet die Zeile an jedem Komma: eine Liste aus drei Strings.
  3. Der Name bleibt ein String.
  4. float macht aus dem String "18.90" die Zahl 18.9. Mit ihr kannst du jetzt rechnen.
  5. int macht aus "12" die ganze Zahl 12.
  6. Product(...) erzeugt ein neues Objekt, __init__ legt die drei Werte in seine Felder, und p zeigt darauf.
  7. p.price wird als 18.9 ausgegeben: Die Null am Ende stand nur im Text.
  8. Der Preis mal den Bestand: Jetzt ist es eine Rechnung mit Zahlen.

Sieh es dir an

Die ganze Datei

Du lädst alle Produkte in eine Liste und rechnest den Wert der Ware aus:
try:
with open("products.csv", "w", encoding="utf-8") as file:
file.write("Name,Preis,Bestand\nBrot,1.20,30\nMilch,2.10,18\nKaffeebohnen,18.90,12\n")
with open("products.csv", encoding="utf-8") as file:
lines = file.read().splitlines()
products = []
header = True
for line in lines:
if header:
header = False
continue
products.append(Product.from_csv(line))
value = 0
for p in products:
value += p.price * p.stock
print(f"{len(products)} Produkte, Warenwert {value:.2f} €")
except OSError as e:
print(f"Fehler beim Lesen von products.csv: {e}")
Ausgabe
3 Produkte, Warenwert 300.60 €
  • products beginnt als leere Liste, und append hängt jedes neue Produkt an ihr Ende. len(products) ist, wie viele es sind.
  • header ist nur im ersten Durchlauf True. Dann wird die Zeile mit continue übersprungen.

Mehr über Listen gibt es im Modul „Collections“.

Die Idee

Fehlerhafte Zeilen

Die Dateien schreiben Menschen und andere Programme. Früher oder später kommt eine Zeile ohne eine Spalte, mit einem Wort statt einer Zahl, oder eine leere Zeile:

products.csv
Name,Preis,Bestand
Brot,1.20,30
Milch,2.10
Kaffee,sieben,4
Tee,3.50,25

Ein Fehler soll nicht das Laden von allem anderen stoppen. Jede Zeile steht in ihrem eigenen try, und die Meldung nennt die Nummer der Zeile, damit jemand sie korrigieren kann:

try:
with open("products.csv", "w", encoding="utf-8") as file:
file.write("Name,Preis,Bestand\nBrot,1.20,30\nMilch,2.10\nKaffee,sieben,4\n\nTee,3.50,25\n")
with open("products.csv", encoding="utf-8") as file:
lines = file.read().splitlines()
products = []
row = 0
for line in lines:
row += 1
if row == 1 or not line.strip():
continue
try:
products.append(Product.from_csv(line))
except ValueError as e:
print(f"Zeile {row}: {e}")
print(f"Geladen: {len(products)}")
except OSError as e:
print(f"Fehler beim Lesen von products.csv: {e}")
Ausgabe
Zeile 3: 3 Spalten erwartet, es sind 2
Zeile 4: could not convert string to float: 'sieben'
Geladen: 2
  • Leere Zeilen werden übersprungen: line.strip() entfernt die Leerzeichen, und wenn nichts übrig bleibt, gibt not den Wert True.
  • Ein except ValueError fängt beide Fehler: den, den from_csv selbst wirft, und den von float("sieben"). Python hat keine eigene Art von Exception für eine ungültige Zahl. Was passiert ist, sagt die Meldung.
  • Das Programm sagt „Geladen: 2“ und macht mit ihnen weiter.

Probier es aus

Leere Spalten

In der ersten Zeile ist der Preis leer, in der zweiten der Bestand, und die dritte Zeile ist ganz leer:

Was gibt das Programm aus?

print(len("Kaffee,,4".split(",")))
print(len("Kaffee,7.50,".split(",")))
print(len("".split(",")))
Antwort

Die Idee

Objekte zu CSV

Die andere Richtung ist eine Methode, die die Zeile zusammenbaut:

def to_csv(self):
return f"{self.name},{self.price},{self.stock}"

Du schreibst die Kopfzeile und danach eine Zeile für jedes Objekt:

stock = [Product("Brot", 1.2, 30), Product("Milch", 2.1, 18)]
try:
with open("stock.csv", "w", encoding="utf-8") as out:
out.write("Name,Preis,Bestand\n")
for p in stock:
out.write(p.to_csv() + "\n")
with open("stock.csv", encoding="utf-8") as file:
print(file.read(), end="")
except OSError as e:
print(f"Fehler beim Schreiben von stock.csv: {e}")
Ausgabe
Name,Preis,Bestand
Brot,1.2,30
Milch,2.1,18

from_csv kann diese Datei wieder einlesen. Speichern und Lesen müssen dasselbe Format verwenden.

In Java folgt die Formatierung von Zahlen den Spracheinstellungen des Computers, und im Deutschen schreibt sie ein Komma, das die Spalten auseinanderschiebt. Der f-String in Python folgt ihnen nicht: {price:.2f} schreibt auf jedem Computer einen Punkt.

line = f"Brot,{1.2:.2f},30"
print(f"{line} -> {len(line.split(','))} Spalten")
Ausgabe
Brot,1.20,30 -> 3 Spalten

Die echte Falle ist ein Komma im Wert selbst: „Gouda, mittelalt“ wird zu zwei Spalten. Das Modul csv aus der Standardbibliothek kennt die Regel: So ein Wert kommt in Anführungszeichen. csv.writer setzt sie beim Speichern, und csv.reader entfernt sie beim Lesen und gibt jede Zeile als Liste:

import csv
with open("cheese.csv", "w", encoding="utf-8", newline="") as out:
writer = csv.writer(out, lineterminator="\n")
writer.writerow(["Name", "Preis", "Bestand"])
writer.writerow(["Gouda, mittelalt", 9.8, 5])
with open("cheese.csv", encoding="utf-8", newline="") as file:
print(file.read(), end="")
with open("cheese.csv", encoding="utf-8", newline="") as file:
for row in csv.reader(file):
print(row)
Ausgabe
Name,Preis,Bestand
"Gouda, mittelalt",9.8,5
['Name', 'Preis', 'Bestand']
['Gouda, mittelalt', '9.8', '5']
  • newline="" muss bei jeder Datei stehen, die du mit csv liest oder schreibst: Das Modul kümmert sich selbst um die Zeilenumbrüche.
  • lineterminator="\n" schreibt die Zeilen mit \n. Ohne es setzt csv.writer ein \r\n, wie unter Windows und im CSV-Standard.

Probier es aus

Finde den Fehler

Das Programm soll die drei Produkte aus products.csv laden, aber es bricht schon bei der ersten Zeile ab:

Fehler
ValueError: could not convert string to float: 'Preis'

Wähle die Zeile mit dem Fehler.

Die Idee

Spalten nach Namen

Ein anderes Programm exportiert dieselben Daten, aber mit den Spalten in einer anderen Reihenfolge und mit einer Spalte mehr. Schreibst du f[1] für den Preis, liest du bei einem neuen Format die falsche Spalte und merkst es nicht. Sicherer ist es, die Spalte über ihren Namen in der Kopfzeile zu finden:

def column(header, name):
for i in range(len(header)):
if header[i].strip().lower() == name.lower():
return i
raise ValueError(f"Keine Spalte {name}")
try:
with open("export.csv", "w", encoding="utf-8") as file:
file.write("Bestand,Name,Nummer,Preis\n30,Brot,1001,1.20\n18,Milch,1002,2.10\n")
name_col = -1
price_col = -1
with open("export.csv", encoding="utf-8") as file:
for line in file.read().splitlines():
f = line.split(",")
if name_col == -1:
name_col = column(f, "name")
price_col = column(f, "preis")
continue
print(f"{f[name_col]}: {f[price_col]} €")
except OSError as e:
print(e)
Ausgabe
Brot: 1.20 €
Milch: 2.10 €

Der erste Durchlauf findet nur, wo die Spalten sind. Danach wird jede Zeile über diese Indizes gelesen. Gibt es die Spalte nicht, wirft column sofort eine Exception, statt falsche Daten zu lesen.

Genauer, für Neugierige
  • Spalten nach Namen mit csv. csv.DictReader übernimmt die Suche nach Namen für dich: Die erste Zeile gibt die Namen, und jede weitere Zeile ist ein Dictionary (dict) vom Namen der Spalte zum Wert. Dictionaries kommen im Modul „Collections“.
import csv
with open("export.csv", "w", encoding="utf-8") as file:
file.write("Bestand,Name,Nummer,Preis\n30,Brot,1001,1.20\n18,Milch,1002,2.10\n")
with open("export.csv", encoding="utf-8", newline="") as file:
for row in csv.DictReader(file):
print(f"{row['Name']}: {row['Preis']} €")
  • Ein anderes Trennzeichen. Excel auf einem Computer mit deutschen Einstellungen speichert CSV oft mit ;, genau weil das Komma das Dezimalzeichen ist. Dann liest du mit csv.reader(file, delimiter=";").
  • Serialisierung. Das Modul pickle schreibt ein ganzes Objekt in eine Datei. Die Datei ist binär: Ein Mensch kann sie nicht lesen, und eine pickle-Datei aus einer unbekannten Quelle solltest du nicht laden, weil das Laden Code ausführen kann. Textformate wie CSV und JSON (das Modul json) sind berechenbarer und werden deshalb öfter verwendet.

Übung

Daten, die lebendig werden

Löse die Aufgaben in deinem Editor. Das Programm legt die Eingabedatei zuerst selbst an.

1. CSV-Auswertung. Lege people.csv mit dem Inhalt unten an. Gib das Durchschnittsalter aus (Ganzzahldivision), wer am ältesten ist und wie viele Personen aus jeder Stadt kommen, in der Reihenfolge, in der die Städte vorkommen. Die Städte speicherst du in einer Liste, die Anzahlen in einer anderen, an denselben Stellen.

people.csv
Name,Alter,Stadt
Jonas,34,Berlin
Maria,28,Hamburg
Thomas,41,Berlin
Anna,25,München
Ausgabe
Durchschnittsalter: 32
Am ältesten: Thomas, 41
Personen aus Berlin: 2
Personen aus Hamburg: 1
Personen aus München: 1

2. Den Helden speichern. Eine Klasse Hero mit Name, Level, Leben und Gold. Die Methode save(path) speichert den Helden als Zeilen schlüssel=wert, und die statische Methode load(path) erzeugt aus so einer Datei einen neuen Hero. Erzeuge „Arin“ mit Level 6, 100 Leben und 120 Gold, erhöhe sein Level um 1, nimm ihm 15 Leben, gib ihm 220 Gold, speichere ihn, gib die Datei aus und lade ihn wieder.

Ausgabe
name=Arin
level=7
health=85
gold=340
Geladen: Arin, Level 7, Leben 85, Gold 340

3. Doppelter Text. Lege text.txt mit dem Text unten an. Teile ihn am Punkt (und den Leerzeichen danach) in Sätze und gib jeden Satz, der mehr als einmal vorkommt, einmal aus.

text.txt
Das ist Text. Das ist auch Text. Das ist Text.
Ausgabe
Doppelter Text: "Das ist Text."

Weitere Aufgaben

  • Das teuerste Produkt. Lade products.csv aus der Lektion und gib das Produkt mit dem höchsten Preis aus und wie viele Produkte einen Bestand unter 20 haben.
  • Korrigierte Zeilen. Lade die Datei mit den fehlerhaften Zeilen und schreibe die guten nach products-clean.csv und die fehlerhaften, mit der Nummer der Zeile und dem Grund, nach rejected.txt.
  • Umsatzbericht. Eine Datei sales.csv mit den Spalten Datum,Produkt,Anzahl,Preis. Die Zeilen sind nach Datum sortiert. Gib den Umsatz für jeden Tag mit einem f-String in Spalten aus.

Test

Teste dich

1. Was ist die erste Zeile der meisten CSV-Dateien?
2. from_csv wirft ValueError bei einer falschen Anzahl von Spalten, und float("sieben") wirft bei einer ungültigen Zahl. Wie viele except brauchst du, um beides zu fangen?
3. Was ist len("Kaffee,7.50,".split(","))?
4. Der Name des Produkts ist „Gouda, mittelalt“. Wie schreibst du ihn in eine CSV-Datei, ohne dass er zu zwei Spalten wird?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·