json.csv.de

Ratgeber · Praxis & Code

JSON zu CSV in Python, JavaScript und der Kommandozeile

Drei Wege, JSON nach CSV zu bringen: Python für flache und verschachtelte Daten, Node.js mit korrektem Quoting und jq für die Pipeline. Jeweils lauffähig und erklärt.

7 Min Lesezeit 1.562 Wörter 4 FAQs
Jan-Tristan Rudat
Jan-Tristan RudatRedakteur
Geprüft am

JSON nach CSV zu konvertieren klingt nach einer Einzeiler-Aufgabe, und für saubere, flache Daten ist es das auch. Sobald die Daten verschachtelt sind, Sonderzeichen enthalten oder zu groß für den Arbeitsspeicher werden, trennt sich der robuste Code vom fragilen. Dieser Artikel zeigt die drei Wege, die im Entwickleralltag tatsächlich zählen: Python mit Bordmitteln und mit pandas, Node.js mit und ohne Abhängigkeit, sowie jq auf der Kommandozeile. Alle Beispiele sind lauffähig und behandeln das eine Detail, das am häufigsten schiefgeht: das Quoting.

Das Datenmodell verstehen

CSV ist ein zweidimensionales Format: Zeilen und Spalten, sonst nichts. JSON ist ein Baum. Die Konvertierung ist deshalb immer ein Plattmachen dieses Baums. Ein Array aus flachen Objekten bildet sich eins zu eins ab, jedes Objekt wird eine Zeile, jeder Schlüssel eine Spalte. Sobald ein Wert selbst ein Objekt oder eine Liste ist, musst du entscheiden: aufbrechen in mehrere Spalten, als JSON-String in eine Zelle serialisieren oder mehrere Zeilen erzeugen. Diese Entscheidung trifft kein Tool für dich, sie hängt davon ab, was die Zielsysteme erwarten.

Der zweite Stolperstein ist das CSV-Format selbst. Es gibt zwar RFC 4180, aber Werte mit Komma, Semikolon, Anführungszeichen oder Zeilenumbruch erzwingen Quoting. Die Regel ist simpel: betroffene Felder kommen in doppelte Anführungszeichen, und enthaltene Anführungszeichen werden durch Verdopplung escaped. Wer das manuell baut und diese Regel vergisst, produziert Dateien, die Excel oder ein Importer falsch oder gar nicht liest.

Python mit dem csv-Modul

Für flache Daten ist das csv-Modul aus der Standardbibliothek die richtige Wahl. Es ist immer da, streamt zeilenweise und kümmert sich um das gesamte Quoting. DictWriter nimmt dir die Spaltenzuordnung ab.

import csv
import json

with open("daten.json", encoding="utf-8") as f:
    records = json.load(f)

# Spalten aus allen Datensätzen sammeln, falls nicht jeder dieselben Keys hat
fieldnames = []
for row in records:
    for key in row:
        if key not in fieldnames:
            fieldnames.append(key)

with open("daten.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
    writer.writeheader()
    writer.writerows(records)

Zwei Details sind wichtig. newline="" beim Öffnen verhindert, dass Python unter Windows zusätzliche Wagenrückläufe einfügt, was sonst zu doppelten Zeilenumbrüchen führt. extrasaction="ignore" lässt Felder durch, die nicht in fieldnames stehen, statt eine Exception zu werfen. Das Quoting läuft automatisch im Modus QUOTE_MINIMAL, also nur dort, wo es nötig ist. Wenn dein Ziel Excel in einer deutschen Locale ist, willst du oft Semikolon statt Komma als Trenner: dann csv.DictWriter(f, fieldnames=fieldnames, delimiter=";").

Für sehr große Dateien lädst du nicht das ganze Array in den Speicher. Liegen die Daten als JSON Lines vor, also ein Objekt pro Zeile, liest du streamend:

import csv
import json

with open("daten.jsonl", encoding="utf-8") as src, \
     open("daten.csv", "w", newline="", encoding="utf-8") as dst:
    writer = None
    for line in src:
        line = line.strip()
        if not line:
            continue
        row = json.loads(line)
        if writer is None:
            writer = csv.DictWriter(dst, fieldnames=list(row.keys()))
            writer.writeheader()
        writer.writerow(row)

Hier bleibt der Speicherbedarf konstant, egal ob die Datei hundert oder hundert Millionen Zeilen hat. Voraussetzung ist, dass alle Objekte dieselben Schlüssel haben, da die Header aus dem ersten Datensatz abgeleitet werden.

Python mit pandas und json_normalize

Sobald die Daten verschachtelt sind, wird das csv-Modul zur Fleißarbeit. pandas.json_normalize ist genau dafür gebaut. Es flacht verschachtelte Objekte zu Spalten mit Punkt-Notation ab und kann verschachtelte Listen zu mehreren Zeilen aufbrechen.

import json
import pandas as pd

data = [
    {
        "id": 1,
        "name": "Mara Stein",
        "adresse": {"stadt": "Hamburg", "plz": "20095"},
        "bestellungen": [
            {"artikel": "Kabel", "preis": 9.9},
            {"artikel": "Adapter", "preis": 14.5},
        ],
    }
]

df = pd.json_normalize(
    data,
    record_path="bestellungen",
    meta=["id", "name", ["adresse", "stadt"], ["adresse", "plz"]],
)
df.to_csv("bestellungen.csv", index=False, encoding="utf-8")

record_path zeigt auf die Liste, die zu einzelnen Zeilen werden soll. meta listet die Felder der übergeordneten Ebene, die in jeder erzeugten Zeile wiederholt werden sollen, verschachtelte Pfade gibt man als Liste an. Das Ergebnis ist eine Tabelle mit den Spalten artikel, preis, id, name, adresse.stadt, adresse.plz, in der jede Bestellung eine eigene Zeile bekommt und die Kundendaten dupliziert werden. index=False verhindert, dass pandas seinen automatischen Zeilenindex als erste Spalte mitschreibt.

Wenn du nur abflachen, aber nichts aufbrechen willst, lässt du record_path weg: pd.json_normalize(data) macht aus adresse.stadt eine Spalte und serialisiert die Liste bestellungen als Objektspalte, die du dann selbst behandelst. pandas ist mächtig, hat aber Kosten: es zieht eine schwere Abhängigkeit und hält den kompletten DataFrame im RAM. Für die gelegentliche Konvertierung verschachtelter Daten ist das ein guter Tausch, für Streaming-Pipelines über riesige Dateien nicht.

JavaScript und Node.js ohne Abhängigkeit

In Node.js kommt man für flache Daten ohne Paket aus, muss das Quoting aber selbst korrekt implementieren. Genau hier scheitern naive Lösungen. Die folgende Funktion behandelt Komma, Anführungszeichen und Zeilenumbrüche nach RFC 4180.

import { readFileSync, writeFileSync } from "node:fs";

function csvEscape(value) {
  if (value === null || value === undefined) return "";
  const str = String(value);
  if (/[",\n\r]/.test(str)) {
    return '"' + str.replace(/"/g, '""') + '"';
  }
  return str;
}

function toCsv(records) {
  if (records.length === 0) return "";
  const headers = [...new Set(records.flatMap((r) => Object.keys(r)))];
  const lines = [headers.map(csvEscape).join(",")];
  for (const record of records) {
    const row = headers.map((h) => csvEscape(record[h]));
    lines.push(row.join(","));
  }
  return lines.join("\r\n");
}

const data = JSON.parse(readFileSync("daten.json", "utf-8"));
writeFileSync("daten.csv", toCsv(data), "utf-8");

Der Kern ist csvEscape. Der reguläre Ausdruck /[",\n\r]/ prüft, ob ein Wert eines der kritischen Zeichen enthält. Trifft das zu, wird der Wert in Anführungszeichen gesetzt und jedes innenliegende Anführungszeichen durch "" ersetzt. null und undefined werden zu leeren Feldern, statt die Strings “null” oder “undefined” in die Datei zu schreiben. Die Header werden über ein Set aus allen vorkommenden Schlüsseln gesammelt, sodass auch heterogene Objekte funktionieren. Als Zeilentrenner dient \r\n, was dem CSV-Standard entspricht und Excel am zuverlässigsten zufriedenstellt.

Diese rund zwanzig Zeilen sind die Untergrenze für korrektes CSV. Wer sie unterbietet, etwa mit records.map(r => Object.values(r).join(",")), bekommt bei der ersten Adresse mit Komma oder dem ersten Freitextfeld mit Zeilenumbruch eine zerschossene Datei.

JavaScript mit Bibliothek

Sobald Verschachtelung, Streaming oder konfigurierbare Optionen ins Spiel kommen, lohnt eine Bibliothek. csv-stringify aus dem csv-Ökosystem ist ausgereift und streamt. Für verschachtelte Strukturen leistet json-2-csv mit automatischer Punkt-Notation gute Dienste.

import { stringify } from "csv-stringify/sync";
import { readFileSync, writeFileSync } from "node:fs";

const data = JSON.parse(readFileSync("daten.json", "utf-8"));

const output = stringify(data, {
  header: true,
  columns: ["id", "name", "email"],
  delimiter: ";",
  quoted_string: true,
});

writeFileSync("daten.csv", output, "utf-8");

header: true schreibt die Kopfzeile, columns legt Reihenfolge und Auswahl der Spalten fest und ignoriert alles andere, delimiter stellt auf Semikolon um. Das gesamte Quoting übernimmt die Bibliothek. Für große Dateien nutzt du statt der synchronen Variante die Stream-API: stringify als Transform-Stream zwischen einen JSON-Parser-Stream und einen Write-Stream gehängt, sodass nie alles gleichzeitig im Speicher liegt. Die Bibliotheksvariante ist die richtige Wahl, wenn du dieselbe Logik an vielen Stellen brauchst, konfigurierbare Trenner und Quoting-Modi willst oder dich nicht selbst um Edge Cases kümmern möchtest.

Die Kommandozeile mit jq

Für Ad-hoc-Konvertierungen und Shell-Pipelines ist jq unschlagbar. Der eingebaute @csv-Filter erledigt das Quoting korrekt, und -r gibt rohe Strings ohne umschließende JSON-Anführungszeichen aus.

jq -r '(.[0] | keys_unsorted) as $keys
       | $keys, (.[] | [.[ $keys[] ]])
       | @csv' daten.json > daten.csv

Schritt für Schritt: .[0] | keys_unsorted nimmt die Schlüssel des ersten Objekts in ihrer ursprünglichen Reihenfolge und bindet sie an $keys. Diese Schlüssel werden zuerst ausgegeben, das ist die Kopfzeile. Danach iteriert .[] über alle Objekte, und [.[ $keys[] ]] baut für jedes ein Array, das die Werte exakt in der Reihenfolge der Header zieht. @csv formatiert jedes Array als CSV-Zeile inklusive Quoting. Ohne den $keys-Trick würde jq die Werte in unbestimmter Reihenfolge ausgeben und keine Kopfzeile erzeugen.

Wenn du nur bestimmte Felder willst und die Header fest kennst, wird es kürzer und du musst nichts raten:

jq -r '["id","name","email"], (.[] | [.id, .name, .email]) | @csv' daten.json > daten.csv

Für JSON Lines, also ein Objekt pro Zeile, nutzt du -c im Eingangsstrom oder gibst jq die Datei direkt: jq -r '[.id, .name] | @csv' daten.jsonl. jq verarbeitet die Eingabe streamend pro Wert, weshalb es auch mit großen JSON-Lines-Dateien gut skaliert. Bei einem einzigen riesigen Array greift jq standardmäßig zum vollständigen Parsen, dann hilft die Option --stream, die aber deutlich komplexere Filter erfordert.

Vergleich der Wege

WegStärkeSchwäche
Python csv-ModulKeine Abhängigkeit, streamt, korrektes QuotingVerschachtelung nur manuell
pandas json_normalizeFlacht verschachtelte Daten und Listen automatisch abSchwere Abhängigkeit, alles im RAM
Node.js ohne PaketNull Abhängigkeiten, volle KontrolleQuoting muss selbst stimmen, kein Streaming out of the box
Node.js mit BibliothekKonfigurierbar, streamfähig, Edge Cases abgedecktZusätzliche Abhängigkeit im Projekt
jqEinzeiler, ideal für Pipelines, schnellHeader und Spaltenordnung manuell, Lernkurve bei verschachtelt

Welcher Weg wofür

Die Wahl folgt dem Kontext, nicht der Vorliebe. Für eine einmalige Konvertierung, bei der die Datei ohnehin schon im Browser liegt oder du keine Lust auf Setup hast, ist ein Browser-Konverter oder ein jq-Einzeiler am schnellsten. Für wiederkehrende Aufgaben in einer Datenpipeline gehört die Logik in Code: Python mit dem csv-Modul für flache Daten, pandas für verschachtelte Strukturen, in JavaScript-Projekten csv-stringify für Robustheit. Sobald die Dateien den Arbeitsspeicher sprengen, scheiden pandas und das naive Einlesen aus, dann zählen streamende Ansätze mit JSON Lines, ijson, stream-json oder jq. Und unabhängig vom Werkzeug gilt die eine Regel, die über kaputte und brauchbare Ausgaben entscheidet: Lass das Quoting von einem Tool erledigen, das die CSV-Regeln kennt, und bau es nicht mit String-Konkatenation nach.

FAQ

Häufige Fragen

Wann reicht das csv-Modul und wann brauche ich pandas?

Das csv-Modul reicht, solange deine Datensätze flach sind und du keine Abhängigkeit ziehen willst. Sobald JSON verschachtelte Objekte oder Listen enthält, die zu eigenen Spalten werden sollen, spart pandas mit json_normalize viel manuellen Code. Für sehr große Dateien ist das streamende csv-Modul speicherschonender als pandas, das den kompletten DataFrame im RAM hält.

Warum bricht meine selbstgebaute CSV-Ausgabe bei manchen Werten?

Fast immer fehlt das Quoting. Werte mit Komma, Anführungszeichen oder Zeilenumbruch müssen in doppelte Anführungszeichen gesetzt werden, und enthaltene Anführungszeichen werden verdoppelt. Genau das nimmt dir das csv-Modul, eine Bibliothek wie csv-stringify oder jq mit @csv ab. Manuelles Stringbasteln ohne diese Regel erzeugt kaputte Dateien.

Wie verarbeite ich eine JSON-Datei, die nicht in den Arbeitsspeicher passt?

Wenn die Datei ein Array aus vielen Objekten ist, hilft ein streamender Parser. In Python liest ijson Element für Element, in Node.js übernimmt stream-json diese Aufgabe. Liegt die Datei bereits als JSON Lines vor, also ein Objekt pro Zeile, kannst du sie zeilenweise lesen und musst nie das ganze Array materialisieren.

Erzeugt jq automatisch eine Kopfzeile mit Spaltennamen?

Nein. jq gibt nur die Werte aus, die du explizit mit @csv formatierst. Die Kopfzeile musst du selbst voranstellen, etwa indem du die Schlüssel des ersten Objekts ausgibst oder die Header als feste Zeile davorsetzt. Das ist bewusst so, weil jq nicht rät, welche Felder du als Spalten willst.

Anzeige

Quellen

Worauf dieser Ratgeber sich stützt

Veröffentlicht · zuletzt geprüft
Verantwortlich: Jan-Tristan Rudat
Anzeige
Anzeige
Anzeige
Anzeige