CSV schreiben & einlesen¶
Für CSV gibt es in Skripten zwei Zugänge mit leicht unterschiedlichem Umfang:
- das Objekt
csv— immer da; mitcsv.parsezum schnellen Einlesen ganzer Dateien; - das Modul
CSVServiceübermodules.use("CSVService")— mit demExcelParserund 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.
"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.
print() und println()¶
println(wert)/println([w1, w2, …])— hängt einen Wert bzw. mehrere Werte an und schließt die Zeile ab.print(...)— wieprintln, 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¶
pdfunddelivery— Bytes als Download anbieten.view—view.Linkfür den Link im Formular.fs— Ziel fürwriteTo/appendTo, Quelle für die Parser.- Dateianlagen — CSV anhängen oder hochgeladene CSV einlesen.