Zum Inhalt

CSV schreiben & einlesen

Für CSV gibt es in Skripten zwei Zugänge mit leicht unterschiedlichem Umfang:

  • das Objekt csv — immer da; mit csv.parse zum schnellen Einlesen ganzer Dateien;
  • das Modul CSVService über modules.use("CSVService") — mit dem ExcelParser und Windows-Zeilenenden als Vorgabe; für übernommene Skripte.

Beim Schreiben verstehen beide Printer dieselben Einstellungen.

csv modules.use("CSVService")
Ganze CSV einlesen (parse) ja –
Ganze CSV schreiben (stringify) ja –
Zeilenweise einlesen (StandardParser) ja ja, zusätzlich ExcelParser
Parser-Quellen Text, fs-Datei, Bytes, Dateianlage dieselben
Parser-Optionen encoding, separator, quoteChar, escapeChar, skipEmptyLines (+ header, trim, skip, strict bei parse) dieselben ohne die parse-eigenen
Schreiben (StandardPrinter) ja ja
Printer-Optionen separator, encoding, lineSeparator, quote, quoteChar, escapeChar, bom, nullValue, escapeFormulae, decimalSep, sepLine, header dieselben ohne header
Zeilenende ohne Angabe wie das Server-Betriebssystem (meist \n) \r\n (Windows)
CSV-Text per p.toString() / String(p) ja ja

Faustregel

Einlesen mit csv.parse, schreiben mit csv.stringify — beides einzeilig und zueinander passend. StandardParser/StandardPrinter brauchst du, wenn du Zeile für Zeile entscheiden willst. CSVService ist für übernommene Skripte da; neue Skripte brauchen es nicht.

Für eine Datei, die ein deutsches Excel direkt richtig öffnet: { separator: ";", bom: true, lineSeparator: "WINDOWS" }. Zum Einlesen musst du dafür nichts angeben — das Trennzeichen wird erkannt.

Einlesen

Die Lese-Beispiele dieser Seite arbeiten mit einer kleinen Übungsdatei. Einmal anlegen, dann sind sie der Reihe nach lauffähig:

var vorbereiten = new csv.StandardPrinter({ separator: ";" });
vorbereiten.println(["Name", "Stadt"])
           .println(["Anna Berg", "Hagen"])
           .println(["Bo Nilsson", "Malmö"]);
fs["/beispiele/csv-service/leute.csv"] = vorbereiten;

Die ganze Datei auf einmal — csv.parse

// Array von Zeilen-Arrays
var rows = csv.parse("a,b\n1,2\n3,4");        // [["a","b"],["1","2"],["3","4"]]
log.info(rows[1][0]);                          // "1"

// Mit Kopfzeile → Array von Objekten
var leute = csv.parse(fs["/beispiele/csv-service/leute.csv"],
                      { header: true, separator: ";" });
for (var i = 0; i < leute.length; i++) {
  log.info(leute[i].Name + " aus " + leute[i].Stadt);
}

Quelle: Text, fs-Datei, Dateianlage (ctx.attachments[i]) oder Bytes.

Beim Zählen keine forEach-Schleife nehmen

Das Ergebnis von csv.parse ist eine Liste vom Server, kein JS-Array — auch wenn Array.isArray dafür true sagt. Bei einem Namen wirkt sich das aus: forEach gibt es auf einer Server-Liste auch in Java, und die Java-Fassung übergibt nur das Element, keinen Index.

// FALSCH: i ist undefined, die Meldung lautet "Zeile NaN"
zeilen.forEach(function (z, i) { log.info("Zeile " + (i + 2)); });

// RICHTIG: indizierte Schleife
for (var i = 0; i < zeilen.length; i++) {
  log.info("Zeile " + (i + 2) + ": " + zeilen[i].Name);
}

map, filter, reduce und slice sind davon nicht betroffen — die gibt es in Java nicht, also gewinnt dort die JS-Fassung samt Index. Zweiter Name mit demselben Problem: sort liefert auf einer Server-Liste null statt der Liste, zeilen.sort(f).slice(0, 3) bricht also ab. Erst filter(...) oder slice(...) aufrufen — ab dann ist es ein echtes JS-Array und alles verhält sich wie gewohnt.

Dasselbe gilt für db.query(...) und für Detail-Tabellen aus dem Formular.

Option Bedeutung Standard
header erste Zeile als Spaltennamen → Objekte statt Arrays false
separator Trennzeichen, genau ein Zeichen — oder "auto" erkannt
encoding Zeichensatz bei Datei, Bytes und Anlage, z. B. "ISO-8859-1"; "auto" probiert UTF-8 und fällt bei ungültigen Bytes auf Windows-1252 zurück UTF-8
trim Leerzeichen um die Werte entfernen false
skip so viele Zeilen am Anfang überspringen 0
skipEmptyLines Leerzeilen überspringen false
strict Zeilen mit abweichender Spaltenzahl melden statt stillschweigend auffüllen bzw. abschneiden false
quoteChar Anführungszeichen "
escapeChar Maskierzeichen; ohne Angabe gilt die Verdopplung ("") –

Das Trennzeichen wird erkannt

Ohne separator prüft der Parser ;, ,, Tabulator und | und nimmt das, das über die ersten Zeilen durchgehend dieselbe Spaltenzahl ergibt. Ein Freitext mit vielen Kommata in einer Semikolon-Datei kippt die Erkennung also nicht — gleichmäßig schlägt häufig.

Das ist der häufigste Grund, warum ein Import früher nichts fand: Excel speichert auf einem deutschen System mit Semikolon, und die Listen-Ausgabe von KCForms tut es ebenso. Eine beginnende Excel-Zeile sep=; wird beachtet und nicht als Datenzeile mitgelesen.

Verlässt du sich lieber nicht darauf, gib separator an — eine Angabe schlägt die Erkennung immer.

Umlaute als Fragezeichen? Dann encoding: "auto"

Excel speichert „CSV (Trennzeichen-getrennt)" auf einem deutschen Windows nicht in UTF-8, sondern in der ANSI-Codepage. Als UTF-8 gelesen wird aus „Müller" ein „M�ller" — und zwar ohne Fehlermeldung.

var zeilen = csv.parse(fs["/import/excel.csv"], { encoding: "auto", header: true });

"auto" liest strikt als UTF-8 und wechselt nur dann auf Windows-1252, wenn die Bytes kein gültiges UTF-8 sind. Die Prüfung geht nur in diese Richtung: eine UTF-8-Datei wird nie als ANSI gelesen.

Eine hochgeladene CSV-Anlage einlesen und ins Formular übernehmen:

// z. B. in einem Schaltflächen-Hook
if (ctx.attachments.length > 0) {
  var zeilen = csv.parse(ctx.attachments[0], { separator: ";", header: true, trim: true });
  ctx.form.anzahl_datensaetze = zeilen.length;
  if (zeilen.length > 0) ctx.form.erster_name = zeilen[0].Name;
}

Die ganze CSV auf einmal schreiben — csv.stringify

Das Gegenstück zu csv.parse: eine Liste rein, CSV-Text raus. Es nimmt Arrays oder Objekte — bei Objekten entsteht die Kopfzeile aus den Schlüsseln.

// Objekte → Kopfzeile automatisch
var daten = [
  { Artikel: "Schraube", Menge: 500, Preis: 0.12 },
  { Artikel: "Mutter",   Menge: 120, Preis: 0.08 }
];
log.info(csv.stringify(daten, { separator: ";", decimalSep: "," }));
// Artikel;Menge;Preis
// Schraube;500;0,12
// Mutter;120;0,08

// Arrays → genau so, wie sie dastehen
log.info(csv.stringify([["a", "b"], [1, 2]]));      // a,b  /  1,2

Das ist auch der bequemste Weg für ein Abfrage-Ergebnis, weil db.query schon Objekte liefert:

var zeilen = db.query("SELECT 1 AS nr, 'Hagen' AS ort UNION SELECT 2, 'Köln'");
log.info(csv.stringify(zeilen, { separator: ";" }));
Option Bedeutung Standard
columns Spalten und ihre Reihenfolge festlegen — und damit auch: Spalten weglassen alle Schlüssel des ersten Objekts
header false lässt die Kopfzeile weg (z. B. beim Anhängen an eine bestehende Datei) true

Dazu alle Printer-Optionen (siehe unten), z. B. separator, bom, lineSeparator, decimalSep.

// nur zwei Spalten, in dieser Reihenfolge, ohne Kopfzeile
var daten = [{ Artikel: "Schraube", Menge: 500, intern: "x" }];
log.info(csv.stringify(daten, { columns: ["Menge", "Artikel"], header: false }));
// 500,Schraube

Rundlauf

Was csv.stringify schreibt, liest csv.parse(…, { header: true }) wieder als dieselben Objekte ein — das Trennzeichen muss du dabei nicht nennen.

Zeile für Zeile — StandardParser

Für große Dateien oder wenn du Zeile für Zeile entscheiden willst:

var parser = new csv.StandardParser(fs["/beispiele/csv-service/leute.csv"], { separator: ";" });

var zeile;
while ((zeile = parser.nextLine()) !== null) {     // null = Ende
  log.debug("Zeile " + parser.line + ": " + zeile.join("|"));
  // zeile[0], zeile[1] … — Werte per Index
}
parser.close();                                      // optional
Funktion / Eigenschaft Bedeutung
nextLine() nächste Zeile als Array (null am Ende)
nextValue() nächster Einzelwert (null am Ende)
line Nummer der zuletzt gelesenen Zeile (ab 1)
close() optional

Werte in Anführungszeichen mit eingebetteten Trennzeichen, verdoppelten Anführungszeichen und Zeilenumbrüchen werden korrekt gelesen (RFC 4180).

Kopfzeile selbst in Objekte umsetzen — das nimmt dir csv.parse(…, { header: true }) sonst ab:

var parser = new csv.StandardParser(fs["/beispiele/csv-service/leute.csv"], { separator: ";" });
var kopf = parser.nextLine();
var zeile;
while ((zeile = parser.nextLine()) !== null) {
  var obj = {};
  for (var i = 0; i < kopf.length; i++) obj[kopf[i]] = zeile[i];
  log.info(obj.Name);
}

Mit CSVService einlesen

CSVService hat zwei Parser mit gleicher Bedienung: StandardParser (Standard-Zeichensatz UTF-8) und ExcelParser (Standard-Zeichensatz ISO-8859-1, wie ältere Excel-Exporte). Zusätzlich lassen sich quoteChar und escapeChar einstellen; ein UTF-8-BOM am Anfang wird entfernt.

var m = modules.use("CSVService");
var parser = new m.ExcelParser(fs["/beispiele/csv-service/leute.csv"], { separator: ";" });
var zeile;
while ((zeile = parser.nextLine()) !== null) { log.info(zeile.join(" | ")); }

Auch die CSVService-Parser lesen eine Dateianlage direkt (new m.ExcelParser(ctx.attachments[0], { separator: ";" })). Eine Quelle, die weder Text, Datei, Bytes noch Anlage ist, meldet einen Fehler.

Schreiben — StandardPrinter

Schnelleinstieg: CSV erzeugen und als Download anbieten

var p = new csv.StandardPrinter({ separator: ";" });

p.println(["Nr", "Name"]);                      // Kopfzeile
var zeilen = ctx.form.zeilen || [];
for (var i = 0; i < zeilen.length; i++) {
  p.println([i + 1, zeilen[i].name]);
}

var url = delivery.deliver(p.toByteArray(), { name: "export.csv", mime: "text/csv" });
view.add(new view.Link(url, "Export als CSV öffnen"));

Zu delivery und view.Link siehe pdf und delivery und view.

  • println(wert) / println([w1, w2, …]) — hängt einen Wert bzw. mehrere Werte an und schließt die Zeile ab.
  • print(...) — wie println, aber ohne Zeilenende (weitere Werte kommen in dieselbe Zeile).
  • println() ohne Argument — schließt nur die aktuelle Zeile ab.

Beide liefern den Printer zurück und lassen sich verketten:

var p = new csv.StandardPrinter();
p.println(["a", "b"])
 .print("c").print("d").println()    // Zeile: c,d
 .print("rest");                     // letzte Zeile ohne Zeilenende

null ergibt einen leeren Wert. Eine Zahl wird geschrieben, wie JS sie schreibt: aus 5 wird 5, nicht 5.0.

Ein Objekt ist eine ganze Zeile, keine Zelle — praktisch, wenn die Daten aus csv.parse(…, { header: true }) oder db.query kommen. Mit header stehen Reihenfolge und Kopfzeile fest, und ein fehlender Schlüssel bleibt leer:

var p = new csv.StandardPrinter({ header: ["Artikel", "Menge"], separator: ";" });
p.println({ Menge: 500, Artikel: "Schraube" });   // Reihenfolge egal
p.println({ Artikel: "Mutter" });                 // Menge fehlt → leer
log.info(String(p));
// Artikel;Menge
// Schraube;500
// Mutter;

Ohne header bestimmt das erste Objekt die Spalten, und seine Schlüssel werden als Kopfzeile übernommen. Für mehrere Zeilen in einem Aufruf ist csv.stringify der kürzere Weg.

Optionen

// mit csv
var p = new csv.StandardPrinter({
  separator:     ";",          // Trennzeichen, Standard ","
  encoding:      "UTF-8",      // Zeichensatz der Ausgabe
  lineSeparator: "WINDOWS",    // "WINDOWS" (\r\n) | "UNIX" (\n) | "MAC" (\r)
  quote:         undefined     // true = immer, false = nie, weglassen = nur wenn nötig
});

Weitere Optionen (bei csv und CSVService gleich):

Option Bedeutung Standard
bom UTF-8-BOM voranstellen — damit erkennt Excel die Umlaute false
quoteChar Zeichen, mit dem Werte umschlossen werden "
escapeChar Maskierzeichen für quoteChar im Wert; ohne Angabe wird verdoppelt (""), z. B. "\\" für \" –
nullValue Text für null/leere Werte ""
quote zusätzlich als Text: "all", "minimal", "none" nur wenn nötig
escapeFormulae Zellen entschärfen, die Excel als Formel lesen würde true
decimalSep Dezimaltrenner für Zahlen, z. B. "," .
sepLine Excel-Hinweiszeile sep=<trennzeichen> voranstellen false
header Spaltennamen als Array — Kopfzeile und Spaltenreihenfolge für Objekt-Zeilen –

„Nur wenn nötig" heißt: Werte, die das Trennzeichen, das Anführungszeichen oder einen Zeilenumbruch enthalten, werden in Anführungszeichen gesetzt.

CSV für deutsches Excel

Standard-Trennzeichen ist das Komma. Für eine Datei, die ein deutsches Excel direkt richtig öffnet — Semikolon, Umlaute, Windows-Zeilenenden, Dezimalkomma:

var p = new csv.StandardPrinter({ separator: ";", bom: true,
                                  lineSeparator: "WINDOWS", decimalSep: "," });

decimalSep wirkt nur auf Zahlen. Der Text "1.234 Stück" bleibt, wie er ist — sonst würde aus einem Inhalt ein anderer.

Hilft das BOM nicht, weil Excel die Spalten trotzdem nicht trennt, nimm zusätzlich sepLine: true. Dann steht sep=; in der ersten Zeile. Das ist bewusst nicht der Standard: für jeden anderen Leser ist diese Zeile eine Datenzeile.

Formeln in Zellen — der Schutz ist standardmäßig an

Eine Zelle, die mit =, +, - oder @ beginnt, ist für Excel und LibreOffice eine Formel. Eine CSV mit Formularinhalten trägt fremde Eingaben: wer sie öffnet, führt sie aus. Anführungszeichen helfen dagegen nicht — die gehören zur CSV-Syntax und sind beim Öffnen schon weg.

Darum stellt KCForms solchen Zellen ein Apostroph voran. Excel zeigt dann den Text und rechnet nicht:

var p = new csv.StandardPrinter();
p.print(["=1+1", "-1500,00"]);
log.info(String(p));            // '=1+1,"-1500,00"

Reine Zahlen sind ausgenommen — sonst würde aus jedem negativen Betrag eine Textzelle, mit der niemand mehr rechnen kann. -1500,00 bleibt also eine Zahl, -2+3 wird entschärft.

Brauchst du die Zellen unverändert — etwa weil die Datei in ein anderes Programm geht, das keine Formeln kennt — schalte es ab: { escapeFormulae: false }. Das Apostroph ist im Übrigen der Grund, wenn in einer erzeugten Datei vor einem Wert eines steht, das du nicht geschrieben hast.

// eigenes Anführungs-/Maskierzeichen
new csv.StandardPrinter({ quoteChar: "'", escapeChar: "\\" });
// Wert  a'b  →  'a\'b'

// andere Codepage, leere Werte als "NULL", immer quoten
new csv.StandardPrinter({ encoding: "ISO-8859-1", nullValue: "NULL", quote: "all" });

In eine Datei schreiben — writeTo / appendTo

var p = new csv.StandardPrinter();
p.println(["Nr", "Name"]).println([1, "Müller"]);

p.writeTo(fs["/beispiele/csv-service/tabelle.csv"]);    // überschreibt
p.appendTo(fs["/beispiele/csv-service/tabelle.csv"]);   // hängt ans Ende an

Als zweites Argument geht ein abweichender Zeichensatz (p.writeTo(datei, "ISO-8859-1")); direkt zuweisen geht auch: fs["/exports/tabelle.csv"] = p;. Beim Anhängen an eine bestehende Datei schreibt appendTo kein zweites BOM.

Als Dateianlage anhängen

Beide Printer lassen sich direkt an den Datensatz hängen (in onSubmit, onAfterSave oder einem Schaltflächen-Hook, siehe Dateianlagen):

var m = modules.use("CSVService");
var p = new m.StandardPrinter({ separator: ";", bom: true });
p.println(["Nr", "Name"]);
p.println([1, "Müller"]);

ctx.attachments.add("export.csv", p);          // Typ wird aus ".csv" abgeleitet

Als Text oder Bytes

var p = new csv.StandardPrinter();
p.println(["Nr", "Name"]).println([1, "Müller"]);

var bytes = p.toByteArray();    // Bytes im eingestellten Zeichensatz (mit BOM, falls bom: true)
var text  = p.toString();       // CSV-Text ohne BOM; ebenso String(p)

Verwandte Themen

  • pdf und delivery — Bytes als Download anbieten.
  • view — view.Link für den Link im Formular.
  • fs — Ziel für writeTo/appendTo, Quelle für die Parser.
  • Dateianlagen — CSV anhängen oder hochgeladene CSV einlesen.