# Daten einlesen in Python: CSV, JSON, Excel und Datenbank

URL: https://www.mikebild.dev/de/blog/python-daten-einlesen/

Ein Reader ist keine Transportfunktion. `csv.DictReader`, `json.load`, `pandas.read_excel` – sie alle machen aus fremden Bytes Python-Objekte, und in diesem Übergang entscheidet sich, ob ein `ä` ein `ä` bleibt, ob eine Pflichtspalte gefüllt ist und ob eine Eingabe später eine ganze Tabelle leert. Und die Quellen sind selten so aufgeräumt, wie man sie sich wünscht: eine CSV-Datei, die jemand aus einem Export gezogen hat. Eine JSON-Antwort von einer API. Eine Excel-Mappe mit drei Tabellenblättern und einer Kopfzeile, die erst in Zeile vier anfängt. Oder eine SQLite-Datei, die still im Projektordner liegt und darauf wartet, abgefragt zu werden.

Welchen Reader ich dafür wähle, ist die kleinere Entscheidung. Die größere ist, an welcher Stelle Vertrauen entsteht: Stimmt das Encoding, sind Pflichtfelder gefüllt, passen die Typen? Ein falsch dekodierter Umlaut oder eine ungeprüfte Eingabe kostet beim Einlesen selten etwas – die Rechnung kommt später, im Report an den Kunden, in der Datenbank, im Modell, das auf Müll trainiert wurde. Das Einlesen ist damit die Grenze, an der sich Datenqualität entscheidet.

Der Schritt ist unspektakulär, und trotzdem bleiben erstaunlich viele Projekte an ihm hängen. Schwer ist er nicht, nur unübersichtlich: Python hält für jedes Format einen eigenen Weg bereit, und welcher davon wann der richtige ist, verliert man schnell aus dem Blick. Dieser Artikel legt dafür einen kleinen Werkzeugkasten an: die Standardbibliothek für den direkten, zeilenweisen Zugriff, pandas für die tabellarische Analysearbeit, und eine parametrisierte Datenbankabfrage, die gar nicht erst in die häufigste Sicherheitsfalle führt. Die Werkzeuge sind schnell erklärt – interessanter ist die Haltung dahinter: Daten als Domäne zu behandeln, deren Qualität man aktiv verantwortet.

## Daten sind eine Domäne, kein Nebenschauplatz

Bevor es um Reader geht, lohnt ein Schritt zurück. Wird das Einlesen als Transport verstanden – Bytes rein, Objekte raus, weiter im Text –, ist das eine teure Sicht, denn sie verschiebt das Vertrauen an die falsche Stelle. Prüfe ich erst spät – im Report, in der Auswertung, im produktiven Endpunkt – dann prüfe ich Daten, die längst durch mein halbes System gewandert sind. Der Fehler ist dann nicht mehr eine Zeile in einer Datei, sondern ein Vorfall mit Namen: ein falscher Umsatz beim Kunden, ein Datensatz, der eine Auswertung kippt, eine Eingabe, die eine Tabelle leert.

Ich behandle das Einlesen deshalb als eigene kleine Domäne mit klarer Aufgabe: aus unbekannten Bytes vertrauenswürdige, typisierte Objekte machen – und alles zurückweisen, was das nicht erfüllt. Drei Fragen entscheiden dabei über das Produktrisiko, und alle drei fallen an der Systemgrenze an. Encoding: Ist ein Zeichen wirklich das gemeinte Zeichen? Ein `Ã¤` statt `ä` ist kein Schönheitsfehler. Es ist ein stiller Datenschaden, der sich durch jede spätere Suche, jeden Vergleich, jeden Export fortpflanzt. Vertrauen: Wem gehört die Quelle? Einen eigenen Export behandle ich anders als eine Nutzereingabe oder eine fremde API – bei allem von außen gilt die Annahme, dass es feindlich sein kann. Validierung: Sind die Felder da, und haben sie die erwartete Form?

Die Abwägung liegt bei der Validierung, denn Prüfung kostet Zeit. Je weiter die Daten reisen und je mehr Menschen am Ende von ihnen abhängen, desto früher und strenger prüfe ich. Ein Wegwerf-Skript für eine einmalige Analyse darf großzügig sein. Ein Reader, der eine Kundendatenbank oder einen wiederkehrenden Report speist, prüft an der Grenze und weist Fehlerhaftes sichtbar zurück, statt es klaglos durchzureichen. Der Gewinn ist konkret: Ein Problem taucht dort auf, wo es entsteht, mit Datei und Zeile – statt drei Schichten später als rätselhaftes Fehlverhalten, das man rückwärts durch ein halbes System zurückverfolgen muss.

## Die zwei Lager: Standardbibliothek oder pandas

Bevor wir Code schreiben, lohnt eine grobe Einordnung. Python bringt für die gängigsten Formate bereits alles mit, was du brauchst – `csv`, `json`, `xml.etree` und `sqlite3` sind Teil der Standardbibliothek, ohne dass du etwas installieren müsstest. Diese Module arbeiten nah an den Rohdaten: Du bekommst Listen, Dictionaries, einzelne Zeilen. Das ist ideal, wenn du eine Datei zeilenweise verarbeiten willst, wenig Speicher zur Verfügung hast oder schlicht keine externe Abhängigkeit in dein Projekt holen möchtest.

pandas gehört ins andere Lager. Es ist eine zusätzliche Bibliothek, und es lohnt sich immer dann, wenn deine Daten tabellarisch sind und du sie analysieren, filtern oder aggregieren willst. Statt für jedes Format ein eigenes mentales Modell zu pflegen, landest du in pandas immer im selben `DataFrame` – egal, ob die Quelle eine CSV, eine Excel-Mappe, ein JSON-Dokument oder eine SQL-Abfrage war. `read_csv`, `read_excel`, `read_json`, `read_sql`: ein Reader je Format, ein gemeinsames Ziel. Dazu kommen Typinferenz und ein reichhaltiger Satz an Operationen.

Der folgende Überblick zeigt den Weg von der Quelle zur Zielstruktur:

```mermaid
flowchart LR
  CSV[CSV] --> STD[Standardbibliothek<br/>csv, json, etree, sqlite3]
  JSON[JSON] --> STD
  XML[XML] --> STD
  DB[(SQLite/DB)] --> STD
  CSV --> PD[pandas<br/>read_csv, read_excel, read_sql]
  XLSX[Excel .xlsx] --> PD
  DB --> PD
  STD --> DICTS[dicts / Listen]
  PD --> DF[DataFrame]
```

Die Einordnung, nach der ich entscheide, ist kurz: Zeilenweise, klein, ohne Zusatzabhängigkeit – Standardbibliothek. Tabellarisch, analytisch, mit Filtern und Aggregaten – pandas. Und Excel im modernen `.xlsx`-Format bekommst du ohnehin nur über pandas (mit einer Hilfsbibliothek) sinnvoll gelesen, denn die Standardbibliothek kann das nicht. Entscheidend ist nicht das bequemste API, sondern das Datenvolumen und die Arbeit, die danach mit den Daten passieren soll.

## CSV mit der Standardbibliothek: der Klassiker

CSV ist das Format, das dir am häufigsten begegnet, und gleichzeitig das mit den meisten kleinen Tücken. Die Standardbibliothek bietet zwei Wege: `csv.reader` liefert pro Zeile eine Liste, `csv.DictReader` liefert pro Zeile ein Dictionary und nutzt dafür die Kopfzeile automatisch als Schlüssel. In der Praxis greife ich fast immer zu `DictReader`, weil der Zugriff über sprechende Spaltennamen den Code lesbarer macht.

```python
import csv

with open("customers.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)          # header row becomes dict keys
    rows = [row for row in reader]      # each row is a dict

for row in rows[:3]:
    print(row["name"], row["email"])
```

Zwei Details an dieser `open`-Zeile sind wichtiger, als sie aussehen. Erstens `newline=""`: Das `csv`-Modul kümmert sich selbst um Zeilenumbrüche innerhalb von Feldern, und wenn du die Datei nicht mit leerem `newline` öffnest, kann es unter bestimmten Bedingungen zu zerrissenen Zeilen kommen. Das steht so in der Dokumentation, und es ist eine dieser Regeln, die man einmal lernt und dann nie wieder vergisst – meist nach dem ersten kaputten Datensatz.

Zweitens `encoding="utf-8"`. Dazu gleich mehr, denn hier steckt der Fallstrick, der in Projekten die meiste Verwirrung stiftet – kaum ein Datenfehler taucht so zuverlässig auf.

Ein schöner Nebeneffekt von `DictReader`: Weil er die Datei als Iterator durchläuft, kannst du ihn auch für große Dateien nutzen, ohne alles auf einmal in den Speicher zu laden. Statt die Liste per Comprehension zu materialisieren, verarbeitest du dann Zeile für Zeile in der Schleife und behältst nur, was du wirklich brauchst.

## JSON: von der Datei zum Python-Objekt

JSON ist in Python angenehm unkompliziert, weil sich die Struktur fast eins zu eins auf Dictionaries und Listen abbildet. `json.load` liest aus einem geöffneten Datei-Objekt, `json.loads` aus einer Zeichenkette. Das Ergebnis ist ein ganz normales Python-Objekt, mit dem du sofort weiterarbeiten kannst.

```python
import json

with open("config.json", encoding="utf-8") as f:
    data = json.load(f)         # dict or list, depending on the document

print(data["version"])
for item in data["items"]:
    print(item["id"], item["title"])
```

Für das Zurückschreiben gibt es die Gegenstücke `json.dump` und `json.dumps`. Beim Schreiben lohnt sich meist `ensure_ascii=False`, damit Umlaute als echte Zeichen und nicht als `\u`-Escapes in der Datei stehen, sowie `indent=2` für eine lesbare Formatierung. Beim Lesen brauchst du diese Optionen nicht – da ist JSON erfreulich anspruchslos.

## Excel: hier führt der Weg über pandas

Sobald echte Tabellen ins Spiel kommen – mehrere Blätter, Zahlenformate, Kopfzeilen – wird pandas zum Mittel der Wahl. `read_excel` liest ein `.xlsx` in einen DataFrame, und du kannst gezielt ein Tabellenblatt über `sheet_name` auswählen.

```python
import pandas as pd

df = pd.read_excel("report.xlsx", sheet_name="Sales")  # openpyxl engine by default
print(df.head())
print(df["revenue"].sum())
```

Ein Punkt, über den viele beim ersten Mal stolpern: `read_excel` braucht für `.xlsx` eine zusätzliche Bibliothek namens `openpyxl`. Seit pandas 1.2.0 (Ende 2020) ist `openpyxl` der Standard-Engine für dieses Format – die ältere `xlrd`-Bibliothek unterstützt moderne `.xlsx`-Dateien nicht mehr und ist nur noch für die alten `.xls`-Dateien zuständig. Wenn du also eine Fehlermeldung über eine fehlende Engine bekommst, ist die Lösung fast immer ein `pip install openpyxl`.

Gibst du `sheet_name=None` an, liefert pandas dir ein Dictionary aller Tabellenblätter als DataFrames zurück – praktisch, wenn du eine Mappe komplett einlesen willst. Und wenn deine Kopfzeile nicht in der ersten Zeile steht, hilft der `header`-Parameter, die richtige Zeile als Spaltennamen zu setzen.

## Datenbanken: sqlite3 und der wichtigste Sicherheitshinweis

SQLite ist mein Standardwerkzeug für Datenbankzugriff in kleinen Projekten und Prototypen, weil es ohne Server auskommt und das Modul `sqlite3` bereits in der Standardbibliothek steckt. Der Ablauf ist immer gleich: Verbindung öffnen, einen Cursor holen, eine Abfrage ausführen, das Ergebnis abholen, Verbindung schließen.

```python
import sqlite3

conn = sqlite3.connect("shop.db")
cur = conn.cursor()

category = "books"
cur.execute(
    "SELECT name, price FROM products WHERE category = ?",
    (category,),
)
result = cur.fetchall()

for name, price in result:
    print(name, price)

conn.close()
```

Der entscheidende Teil steht in der `execute`-Zeile: das Fragezeichen als Platzhalter und der Wert als separates Tupel im zweiten Argument. Das ist keine Stilfrage, sondern eine Sicherheitsfrage. Wer stattdessen den Wert direkt in den SQL-String hineinbaut – per Konkatenation oder f-String – öffnet die Tür für SQL-Injection. Ein manipuliertes Eingabefeld kann dann die Abfrage umschreiben, Daten auslesen oder ganze Tabellen löschen.

Der Vergleich macht den Unterschied deutlich:

```python
# unsafe: never do this
cur.execute(f"SELECT name, price FROM products WHERE category = '{category}'")

# safe: the driver handles quoting and escaping
cur.execute(
    "SELECT name, price FROM products WHERE category = ?",
    (category,),
)
```

Der parametrisierte Weg hat noch einen zweiten Vorteil neben der Sicherheit: Der Datenbanktreiber übernimmt das Quoting und Escaping für dich. Du musst dir keine Gedanken um Anführungszeichen im Wert machen. Ein wichtiges Detail dabei: Der Platzhalter `?` steht **ohne** umschließende Anführungszeichen im SQL-Text – das Quoting macht der Treiber, nicht du.

Wenn du mehr Komfort willst oder zwischen verschiedenen Datenbanken wechseln möchtest, kommt SQLAlchemy ins Spiel. Mit `create_engine("sqlite:///shop.db")` baust du eine Verbindung auf, und `pandas.read_sql(query, con)` liest das Ergebnis direkt in einen DataFrame. Damit schließt sich der Kreis zwischen Datenbank und Analyse: Die Abfrage kommt aus SQL, das Ergebnis liegt sofort im vertrauten pandas-Modell.

## Und XML? Nicht vergessen, aber selten gebraucht

XML begegnet dir seltener als früher, taucht aber in Konfigurationen, Exporten und älteren Schnittstellen weiterhin auf. Die Standardbibliothek deckt das mit `xml.etree.ElementTree` ab. Du parst die Quelle zu einem Baum, holst dir die Wurzel und navigierst über `find`, `findall` und `iter` zu den Elementen, die dich interessieren.

```python
import xml.etree.ElementTree as ET

tree = ET.parse("catalog.xml")
root = tree.getroot()

for product in root.findall("product"):
    name = product.find("name").text
    price = product.get("price")        # read an attribute
    print(name, price)
```

Für einfache Strukturen reicht das vollkommen. Bei komplexeren XML-Dokumenten mit Namespaces oder tiefer Verschachtelung wird es schnell fummelig – dann greife ich gern zu spezialisierten Bibliotheken. Für den Alltag deckt `ElementTree` aber die meisten Fälle ab.

## Welcher Reader für welches Format?

Damit du die Übersicht behältst, hier die Zuordnung als kompakte Liste:

- Für CSV nimmst du `csv.DictReader` aus der Standardbibliothek zum zeilenweisen Lesen und `pandas.read_csv`, sobald es analytisch wird.
- Für JSON reichen `json.load` und `json.loads` aus der Standardbibliothek; `pandas.read_json` hilft, wenn das JSON tabellarisch ist.
- Für Excel im `.xlsx`-Format führt der Weg über `pandas.read_excel` mit installiertem `openpyxl` – die Standardbibliothek kann dieses Format nicht.
- Für Datenbanken deckt `sqlite3` den direkten, parametrisierten Zugriff ab; `pandas.read_sql` oder SQLAlchemy kommen dazu, wenn Analyse oder Portabilität gefragt sind.
- Für XML genügt `xml.etree.ElementTree` aus der Standardbibliothek bei überschaubaren Dokumenten.

## Drei Fallstricke, die immer wieder zuschlagen

In der Praxis sind es selten die Reader selbst, die Probleme machen, sondern drei wiederkehrende Themen rund ums Einlesen.

### Encoding

Warum Encoding ein Produktrisiko ist, stand schon oben – hier der praktische Teil. Der Standard-Zeichensatz beim Öffnen einer Datei ist plattformabhängig, unter Windows oft `cp1252` statt UTF-8. Erscheinen Umlaute als `Ã¤` oder `Ã¶`, oder kommt ein `UnicodeDecodeError`, ist fast immer das Encoding schuld, und die Lösung ist, es explizit anzugeben: `encoding="utf-8"`. Für Dateien, die aus Excel exportiert wurden, brauchst du oft `encoding="utf-8-sig"`, weil Excel eine sogenannte Byte Order Mark an den Anfang setzt, die das schlichte `utf-8` als Sonderzeichen missdeutet.

### Große Dateien komplett im Speicher

Es ist verlockend, eine CSV einfach mit `pandas.read_csv("big.csv")` komplett zu laden – bis die Datei mehrere Gigabyte hat und der Speicher überläuft. pandas bietet dafür `chunksize`: Statt eines DataFrames bekommst du dann einen Iterator, über den du die Datei in Blöcken abarbeitest.

```python
import pandas as pd

total = 0
for chunk in pd.read_csv("big.csv", chunksize=100_000):
    total += chunk["amount"].sum()     # process one block at a time

print(total)
```

In der Standardbibliothek erreichst du dasselbe, indem du über `DictReader` Zeile für Zeile iterierst, statt alles in eine Liste zu materialisieren. Ein Hinweis noch: `read_excel` kennt kein natives Chunking – bei sehr großen Tabellen lohnt es sich, die Daten vorher nach CSV oder in eine Datenbank zu überführen.

### SQL-Injection

Wichtig genug für die Wiederholung: Baue niemals Benutzereingaben per String-Konkatenation oder f-String in eine SQL-Abfrage ein. Nutze immer Platzhalter und übergib die Werte separat. Das kostet keine zusätzliche Zeile Code und erspart dir eine ganze Klasse von Sicherheitslücken.

Der folgende Entscheidungsbaum fasst zusammen, wie ich in der Praxis den Weg wähle:

```mermaid
flowchart TD
  A{Tabellarische<br/>Analyse nötig?} -->|ja| B[pandas]
  A -->|nein| C{Zeilenweise,<br/>klein, ohne Abhängigkeit?}
  C -->|ja| D[Standardbibliothek]
  C -->|nein| B
  B --> E{Sehr große Datei?}
  E -->|ja| F[chunksize / Streaming]
  E -->|nein| G[direkt einlesen]
```

## Fazit

Die Werkzeugwahl ist schnell getroffen: Für zeilenweisen, direkten Zugriff ohne Zusatzabhängigkeit reicht die Standardbibliothek mit `csv`, `json`, `xml.etree` und `sqlite3`; sobald es tabellarisch und analytisch wird oder Excel im Spiel ist, zahlt sich pandas mit seinem einheitlichen DataFrame-Modell aus. Der Unterschied zwischen einem Skript, das läuft, und einem, dem man vertraut, entsteht nicht bei den Readern, sondern bei der Haltung: Encoding, Vertrauen und Validierung gehören an die Systemgrenze.

Wer diese Grenze bewusst zieht, verändert die Ökonomie des Projekts. Fehler tauchen früh und benannt auf, statt spät und rätselhaft – das verkürzt die Debugging-Zyklen und nimmt Reibung aus der Weiterarbeit. Die Daten, die durchkommen, sind die, denen man trauen kann, was die Qualität aller späteren Schritte anhebt. Und die drei Reflexe dazu kosten fast nichts: Encoding immer explizit setzen, damit Umlaute nicht zerfallen. Bei großen Dateien früh an `chunksize` oder zeilenweises Streamen denken, statt den Speicher vollzuladen. Datenbankabfragen ausnahmslos mit Platzhaltern, nie mit zusammengebautem SQL. Wichtig ist auch die klare Grenze nach oben: Nicht jedes Skript braucht volle Schema-Validierung, und wo Aufwand und Risiko nicht zusammenpassen, ist Großzügigkeit die richtige Wahl. Wer das entscheidet statt es dem Zufall zu überlassen, kommt schnell und sicher ans Ziel – und kann sich dem eigentlich Interessanten widmen: dem, was in den Daten steckt.

## Weiterführende Quellen

- Foliensatz zu diesem Thema: https://supabase.mikebild.dev/storage/v1/object/public/slides/introduction-python.html
- Repository `introduction-python` – Beispiele zum Datenzugriff aus meinen Schulungen (`csv`, `json`, `sqlite3`, pandas).
- csv – Python-Dokumentation: https://docs.python.org/3/library/csv.html
- json – Python-Dokumentation: https://docs.python.org/3/library/json.html
- sqlite3 – Python-Dokumentation: https://docs.python.org/3/library/sqlite3.html
- xml.etree.ElementTree – Python-Dokumentation: https://docs.python.org/3/library/xml.etree.elementtree.html
- pandas IO-Guide: https://pandas.pydata.org/docs/user_guide/io.html
- pandas 1.2.0 – openpyxl als Default-Engine: https://pandas.pydata.org/docs/whatsnew/v1.2.0.html
- SQLAlchemy-Dokumentation: https://docs.sqlalchemy.org/en/14/
