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 mit Beispielcode und Erklärung.

8 Min Lesezeit 1.666 Wörter 4 FAQs
Jan-Tristan Rudat
Jan-Tristan Rudat Entwicklung und Konzeption
Geprüft am

JSON nach CSV umzuwandeln 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 drei Wege, die im Entwickleralltag zählen: Python mit Bordmitteln und mit pandas, Node.js mit und ohne Abhängigkeit, sowie jq auf der Kommandozeile. Alle Beispiele behandeln das Detail, das am häufigsten schiefgeht: das Quoting. Die Python- und Node.js-Beispiele haben wir mit einer kleinen Test-JSON ausgeführt. Für fertige Beispiele je Sprache gibt es zusätzlich den Bereich Umwandeln.

Das Datenmodell verstehen

CSV ist ein zweidimensionales Format: Zeilen und Spalten, sonst nichts. JSON ist ein Baum. Die Umwandlung 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, müssen Sie entscheiden: aufbrechen in mehrere Spalten, als JSON-Text in eine Zelle schreiben oder mehrere Zeilen erzeugen. Diese Entscheidung trifft kein Werkzeug für Sie, 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 Trennzeichen, 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 von Hand 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, schreibt zeilenweise und kümmert sich um das gesamte Quoting. DictWriter nimmt Ihnen 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 weg, die nicht in fieldnames stehen, statt einen ValueError zu werfen. Da das Beispiel alle Schlüssel vorab sammelt, ist das nur eine Absicherung. Fehlende Schlüssel füllt der DictWriter mit einem leeren Feld. Das Quoting läuft automatisch im Modus QUOTE_MINIMAL, also nur dort, wo es nötig ist. Wenn Ihr Ziel Excel mit deutscher Spracheinstellung ist, wollen Sie oft ein Semikolon statt Komma als Trenner und ein BOM für die Umlaute: dann csv.DictWriter(f, fieldnames=fieldnames, delimiter=";") und beim Öffnen encoding="utf-8-sig".

Für sehr große Dateien laden Sie nicht das ganze Array in den Speicher. Liegen die Daten als JSON Lines vor, also ein Objekt pro Zeile, lesen Sie zeilenweise:

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 wie das erste. Ein späteres Objekt mit einem zusätzlichen Schlüssel löst einen ValueError aus.

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-Pfad ab und kann Listen von Objekten zu mehreren Zeilen aufbrechen.

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, verschachtelte Pfade geben Sie 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 wiederholt werden. index=False verhindert, dass pandas seinen automatischen Zeilenindex als erste Spalte mitschreibt.

Wenn Sie nur abflachen, aber nichts aufbrechen wollen, lassen Sie record_path weg: pd.json_normalize(data) macht aus adresse.stadt eine Spalte und lässt die Liste bestellungen als Python-Liste in einer Spalte stehen, die Sie dann selbst behandeln. pandas ist mächtig, hat aber Kosten: Es ist eine schwere Abhängigkeit und hält den kompletten DataFrame im RAM. Für die gelegentliche Umwandlung verschachtelter Daten ist das ein guter Tausch, für Streaming-Pipelines über riesige Dateien nicht. Für deutsches Excel ergänzen Sie sep=";" und encoding="utf-8-sig" in to_csv.

JavaScript und Node.js ohne Abhängigkeit

In Node.js kommen Sie für flache Daten ohne Paket aus, müssen das Quoting aber selbst korrekt umsetzen. Genau hier scheitern naive Lösungen. Die folgende Funktion behandelt Komma, Anführungszeichen und Zeilenumbrüche nach RFC 4180. Als ES-Modul braucht die Datei die Endung .mjs oder "type": "module" in der package.json.

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 Texte „null“ oder „undefined“ in die Datei zu schreiben. Die Kopfzeile wird über ein Set aus allen vorkommenden Schlüsseln gesammelt, sodass auch uneinheitliche Objekte funktionieren. Als Zeilentrenner dient \r\n, wie es der CSV-Standard vorsieht. Für deutsches Excel ersetzen Sie das Komma durch ein Semikolon (im Regex und in den beiden join-Aufrufen) und setzen "" als BOM vor den Text. Verschachtelte Werte werden mit String(value) zu [object Object], serialisieren Sie sie vorher mit JSON.stringify.

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 kaputte 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 automatischem Punkt-Pfad 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, quoted_string: true setzt jeden Text in Anführungszeichen. Das gesamte Quoting übernimmt die Bibliothek. Für große Dateien nutzen Sie 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 Sie dieselbe Logik an vielen Stellen brauchen, konfigurierbare Trenner und Quoting-Modi wollen oder sich nicht selbst um Sonderfälle kümmern möchten.

Die Kommandozeile mit jq

Für schnelle Umwandlungen und Shell-Pipelines ist jq praktisch. Der eingebaute @csv-Filter erledigt das Quoting korrekt, und -r gibt rohe Texte 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 in der Reihenfolge der Kopfzeile zieht. @csv formatiert jedes Array als CSV-Zeile inklusive Quoting. Die Spalten stammen dabei nur vom ersten Objekt. Schlüssel, die erst in späteren Objekten vorkommen, fehlen in der Ausgabe.

Wenn Sie nur bestimmte Felder wollen und die Überschriften fest kennen, wird es kürzer:

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

Für JSON Lines, also ein Objekt pro Zeile, braucht es keine Klammer um die Daten: jq -r '[.id, .name] | @csv' daten.jsonl liest jede Zeile als eigenen Wert. Da jq die Eingabe Wert für Wert verarbeitet, skaliert das auch bei großen JSON-Lines-Dateien. Bei einem einzigen riesigen Array parst jq standardmäßig alles auf einmal. Die Option --stream hilft dann, erfordert aber deutlich komplexere Filter.

Vergleich der Wege

Weg Stärke Schwäche
Python csv-Modul Keine Abhängigkeit, schreibt zeilenweise, korrektes Quoting Verschachtelung nur manuell
pandas json_normalize Flacht verschachtelte Daten und Listen automatisch ab Schwere Abhängigkeit, alles im RAM
Node.js ohne Paket Keine Abhängigkeiten, volle Kontrolle Quoting muss selbst stimmen, kein Streaming von Haus aus
Node.js mit Bibliothek Konfigurierbar, streamfähig, Sonderfälle abgedeckt Zusätzliche Abhängigkeit im Projekt
jq Kurze Befehle, ideal für Pipelines, schnell Kopfzeile und Spaltenreihenfolge von Hand, Lernkurve bei Verschachtelung

Welcher Weg wofür

Die Wahl folgt dem Kontext, nicht der Vorliebe. Für eine einmalige Umwandlung ist ein Browser-Konverter oder ein jq-Befehl 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 komplette 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: Überlassen Sie das Quoting einem Werkzeug, das die CSV-Regeln kennt, und bauen Sie 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 Ihre Datensätze flach sind und Sie keine Abhängigkeit ziehen wollen. 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 zeilenweise schreibende 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 Trennzeichen, Anführungszeichen oder Zeilenumbruch müssen in doppelte Anführungszeichen gesetzt werden, und enthaltene Anführungszeichen werden verdoppelt. Genau das nimmt Ihnen das csv-Modul, eine Bibliothek wie csv-stringify oder jq mit @csv ab. Manuelles Zusammensetzen von Texten 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, können Sie sie zeilenweise lesen und müssen nie das ganze Array in den Speicher laden.

Erzeugt jq automatisch eine Kopfzeile mit Spaltennamen?

Nein. jq gibt nur die Werte aus, die Sie explizit mit @csv formatieren. Die Kopfzeile müssen Sie selbst voranstellen, etwa indem Sie die Schlüssel des ersten Objekts ausgeben oder die Überschriften als feste Zeile davorsetzen. Das ist bewusst so, weil jq nicht rät, welche Felder Sie als Spalten wollen.

Anzeige

Quellen

Worauf dieser Ratgeber sich stützt

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