Workshop CSV — vom ersten Export bis zum geprüften Import¶
CSV ist das Austauschformat, auf das sich alle einigen können: Excel, die Fachabteilung, das Nachbarsystem. Dieser Workshop baut in kleinen Schritten beides auf — eine Datei erzeugen und eine Datei einlesen — und zeigt an jeder Stelle, woran es in der Praxis scheitert.
Du brauchst keine Vorkenntnisse außer: du weißt, wie man ein Skript speichert und laufen lässt. Jeder Abschnitt ist für sich ausführbar; die Beispiele bauen inhaltlich, aber nicht technisch aufeinander auf.
Was du am Ende kannst
Eine CSV erzeugen, die ein deutsches Excel richtig öffnet. Eine fremde CSV einlesen, ohne an Trennzeichen oder Umlauten zu scheitern. Fehlerhafte Zeilen mit Zeilennummer melden statt still zu verlieren. Und wissen, warum vor manchen Werten plötzlich ein Apostroph steht.
Teil 0 — Wo du das ausprobierst¶
Am schnellsten geht es in der Skript-Konsole unter Verwaltung → Skripting → Konsole. Dort tippst du ein Skript ein, drückst Ausführen und siehst das Protokoll direkt darunter. Kein Formular, kein Speichern, kein Datensatz.

Dort tippst du das Skript in das Feld JavaScript-Code, drückst Ausführen (oder Strg + Eingabe) und liest das Ergebnis unter LOGS:

Alles, was hier steht, läuft genauso in einem Formular-Hook oder in einem Zeitplaner-Job. Nur die Quelle der Daten ist dann eine andere.
Teil 1 — Deine erste CSV¶
Eine CSV ist Text: eine Zeile je Datensatz, die Werte durch ein Zeichen
getrennt. Du musst diesen Text nicht selbst zusammenbauen —
csv.stringify macht aus einer Liste fertigen CSV-Text.
var daten = [
{ Artikel: "Schraube", Menge: 500 },
{ Artikel: "Mutter", Menge: 120 }
];
log.info(csv.stringify(daten));
Im Protokoll steht:
Drei Dinge sind dabei schon passiert, ohne dass du sie hinschreiben musstest:
- Die Kopfzeile entstand aus den Schlüsseln des ersten Objekts.
- Die Reihenfolge der Spalten ist die des ersten Objekts — alle weiteren Zeilen folgen ihr, auch wenn ihre Schlüssel anders sortiert sind.
- Werte mit Komma, Anführungszeichen oder Zeilenumbruch werden in Anführungszeichen gesetzt; nur die, die es brauchen.
Wenn deine Daten schon als Liste von Listen vorliegen, geht das genauso — dann gibt es keine Kopfzeile, weil es keine Schlüssel gibt:
Teil 2 — Damit deutsches Excel sie richtig öffnet¶
Die Datei aus Teil 1 ist korrekt, aber ein deutsches Excel zeigt sie als eine einzige Spalte an. Das liegt nicht an KCForms: Excel erwartet auf einem deutschen System das Semikolon, und das Komma hält es für einen Dezimaltrenner.
Für eine Datei, die per Doppelklick richtig aufgeht, brauchst du drei Einstellungen:
var daten = [
{ Artikel: "Schraube", Menge: 500, Preis: 0.12 },
{ Artikel: "Mutter", Menge: 120, Preis: 0.08 }
];
var text = csv.stringify(daten, {
separator: ";", // deutsches Excel trennt mit Semikolon
decimalSep: ",", // 0.12 → 0,12, sonst ist es für Excel Text
bom: true // damit Umlaute nicht zu Kästchen werden
});
log.info(text);
| Einstellung | Warum |
|---|---|
separator: ";" |
Sonst landet die ganze Zeile in Spalte A. |
decimalSep: "," |
Ohne sie ist 0.12 für deutsches Excel Text, und du kannst nicht damit rechnen. |
bom: true |
Drei unsichtbare Bytes am Anfang, an denen Excel UTF-8 erkennt. Ohne sie wird aus „Müller" ein „Müller". |
decimalSep wirkt nur auf Zahlen. Der Text "1.234 Stück" bleibt, wie er
ist — sonst würde aus einem Inhalt ein anderer.
Excel trennt trotzdem nicht?
Manche Excel-Installationen richten sich nicht nach der Länder-Einstellung.
Dann hilft sepLine: true: damit steht als erste Zeile sep=; in der
Datei, und Excel folgt dieser Angabe. Lies die Datei aber nur mit
Programmen, die diese Zeile kennen — für alle anderen ist sie eine
Datenzeile. KCForms selbst erkennt sie beim Einlesen.
Teil 3 — Die Datei beim Nutzer ankommen lassen¶
Text im Protokoll hilft dir beim Entwickeln, nicht der Fachabteilung. Es gibt drei Wege, und sie unterscheiden sich darin, wem die Datei gehört.
Als Download anbieten¶
Der häufigste Fall: ein Knopf im Formular, ein Klick, die Datei liegt im Download-Ordner.
var p = new csv.StandardPrinter({ separator: ";", bom: true });
p.println(["Artikel", "Menge"]);
p.println(["Schraube", 500]);
var url = delivery.deliver(p.toByteArray(), { name: "artikel.csv", mime: "text/csv" });
view.add(new view.Link(url, "Artikelliste als CSV"));
log.info("Link steht bereit");
Hier steht bewusst der StandardPrinter und nicht csv.stringify:
delivery.deliver will Bytes, keinen Text. Und das ist kein Formfehler —
zu Bytes gehören Zeichensatz und BOM, und die entscheidet der Printer. Mit
p.toByteArray() bekommst du genau die Datei, die beim Nutzer ankommt.
Hast du die Daten schon als Liste, kannst du dir die Schleife trotzdem sparen und den Printer nur zum Verpacken nehmen:
var daten = [{ Artikel: "Schraube", Menge: 500 }, { Artikel: "Mutter", Menge: 120 }];
var p = new csv.StandardPrinter({ separator: ";", bom: true });
for (var i = 0; i < daten.length; i++) p.println(daten[i]); // Objekt = eine Zeile
var url = delivery.deliver(p.toByteArray(), { name: "artikel.csv", mime: "text/csv" });
log.info("fertig verpackt: " + url);
In einen Ordner schreiben¶
Für Übergaben an ein anderes System, das einen Ordner überwacht:
var text = csv.stringify([{ Artikel: "Schraube", Menge: 500 }],
{ separator: ";", bom: true });
fs["/beispiele/csv-workshop/artikel.csv"].writeText(text);
log.info(fs["/beispiele/csv-workshop/artikel.csv"].size + " Bytes geschrieben");
bom: true wirkt hier auch im Text: csv.stringify stellt das
BOM-Zeichen voran, und writeText schreibt es als die drei Bytes, an denen
Excel UTF-8 erkennt.
An den Datensatz hängen¶
Damit die Datei dauerhaft beim Vorgang liegt — nachvollziehbar, auch Jahre später:
var text = csv.stringify([{ Artikel: "Schraube", Menge: 500 }], { separator: ";" });
ctx.attachments.add("artikel.csv", text);
log.info("angehängt");
Teil 4 — Eine CSV einlesen¶
Jetzt die andere Richtung. csv.parse liest eine ganze Datei auf einmal.
Mit header: true bekommst du Objekte, deren Eigenschaften die Spaltennamen
sind — das liest sich wie Fließtext:
var text = "Artikel;Menge\nSchraube;500\nMutter;120";
var zeilen = csv.parse(text, { header: true });
for (var i = 0; i < zeilen.length; i++) {
log.info(zeilen[i].Artikel + ": " + zeilen[i].Menge);
}
Beachte: Im Beispiel steht kein separator. Das Trennzeichen wird erkannt —
dazu gleich mehr.
Die eine Schleife, die du nicht nehmen darfst
Das Ergebnis von csv.parse ist eine Liste vom Server, kein JS-Array. Bei
einem Namen fällt das auf: forEach gibt es 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
for (var i = 0; i < zeilen.length; i++) { log.info("Zeile " + (i + 2)); }
map, filter, reduce und slice sind nicht betroffen. Brauchst du
keine Zeilennummer, ist forEach ohne zweiten Parameter völlig in Ordnung.
Dasselbe gilt für db.query(...).
Alle Werte sind Text¶
csv.parse liefert Zeichenfolgen — auch bei 500. In einer Datei steht
nichts, was eine Zahl von ihrer Schreibweise unterscheidet. Rechnen willst du
mit kc.num:
var zeilen = csv.parse("Artikel;Menge\nSchraube;500\nMutter;120", { header: true });
var summe = 0;
for (var i = 0; i < zeilen.length; i++) summe += kc.num(zeilen[i].Menge);
log.info("Gesamt: " + summe);
kc.num versteht dabei auch "1.234,56" — den Fall, der aus deutschem Excel
kommt. Siehe Hilfsfunktionen.
Teil 5 — Fremde Dateien: die drei üblichen Stolpersteine¶
Bis hier kam der Text aus deinem eigenen Skript. Eine Datei von außen bringt drei Probleme mit, und alle drei sind leise.
Das Trennzeichen¶
Ohne Angabe prüft KCForms ;, ,, Tabulator und | und nimmt das, das über
die ersten Zeilen durchgehend dieselbe Spaltenzahl ergibt. Deshalb
funktionieren beide Dateien ohne Zutun:
var mitSemikolon = csv.parse("a;b;c\n1;2;3");
var mitKomma = csv.parse("a,b,c\n1,2,3");
log.info(mitSemikolon[0].length + " Spalten / " + mitKomma[0].length + " Spalten");
Weißt du das Trennzeichen sicher, gib es an — eine Angabe schlägt die Erkennung immer. Das ist der ehrlichere Weg, wenn ein Partnersystem eine feste Zusage gemacht hat.
Die Kodierung¶
Excel speichert „CSV (Trennzeichen-getrennt)" auf einem deutschen Windows nicht als UTF-8. Liest man so eine Datei trotzdem als UTF-8, wird aus „Müller" ein Zeichen, das niemand bestellt hat — ohne Fehlermeldung.
// Eine Datei anlegen, wie Excel sie schreiben würde
fs["/beispiele/csv-workshop/ansi.csv"].writeText("Name;Ort\nMüller;Köln\n", "windows-1252");
var falsch = csv.parse(fs["/beispiele/csv-workshop/ansi.csv"], { header: true });
var richtig = csv.parse(fs["/beispiele/csv-workshop/ansi.csv"], { header: true, encoding: "auto" });
log.info("ohne auto: " + falsch[0].Name);
log.info("mit auto: " + richtig[0].Name);
encoding: "auto" liest streng 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 fälschlich als ANSI gelesen.
Faustregel für Importe von außen
{ header: true, encoding: "auto", trim: true } — Spaltennamen nutzen,
Kodierung erkennen, Leerzeichen um die Werte wegschneiden. Damit laufen die
meisten Dateien auf Anhieb.
Eine hochgeladene Datei¶
Im Formular kommt die CSV meist als Anlage. Die gibst du direkt an
csv.parse — ohne Umweg über das Dateisystem:
if (ctx.attachments.length > 0) {
var zeilen = csv.parse(ctx.attachments[0], { header: true, encoding: "auto", trim: true });
log.info(zeilen.length + " Zeilen in " + ctx.attachments[0].name);
}
Teil 6 — Prüfen statt hoffen¶
Ein Import, der die Hälfte verschluckt, ist schlimmer als einer, der abbricht. Zwei Werkzeuge dafür.
Spaltenzahl prüfen¶
Standardmäßig ist csv.parse tolerant: fehlende Werte werden null,
überzählige fallen weg. Für eine Datei, die ein festes Format haben muss,
schaltest du strict ein und bekommst die Zeilennummer genannt:
var text = "Artikel;Menge;Preis\nSchraube;500;0,12\nMutter;120";
try {
csv.parse(text, { header: true, strict: true });
log.info("Format in Ordnung");
} catch (e) {
log.warn("Import abgebrochen: " + e);
}
Einen Fehlerbericht sammeln¶
Meist willst du aber nicht abbrechen, sondern am Ende sagen können, was zu korrigieren ist. Das Muster dafür: gute Zeilen sammeln, schlechte mit Zeilennummer notieren.
var text = "Artikel;Menge\nSchraube;500\n;120\nMutter;viele";
var zeilen = csv.parse(text, { header: true, trim: true });
var gut = [], schlecht = [];
for (var i = 0; i < zeilen.length; i++) {
var z = zeilen[i];
var nr = i + 2; // +1 für die Kopfzeile, +1 weil Menschen ab 1 zählen
if (!z.Artikel) schlecht.push("Zeile " + nr + ": Artikel fehlt");
else if (!/^\d+$/.test(z.Menge)) schlecht.push("Zeile " + nr + ": Menge „" + z.Menge + "“ ist keine Zahl");
else gut.push({ artikel: z.Artikel, menge: kc.num(z.Menge) });
}
log.info(gut.length + " gültig, " + schlecht.length + " zu korrigieren");
for (var k = 0; k < schlecht.length; k++) log.warn(schlecht[k]);
Die + 2 ist der Grund, warum die Schleifen-Falle aus Teil 4 so teuer ist: mit
forEach stünde hier überall „Zeile NaN", und die Meldung wäre wertlos.
Teil 7 — Große Dateien Zeile für Zeile¶
csv.parse liest die ganze Datei in den Speicher. Das ist bis in die
Hunderttausende Zeilen kein Problem. Willst du aber während des Lesens
entscheiden — früh abbrechen, in Blöcken speichern —, nimm den
StandardParser:
var parser = new csv.StandardParser("Artikel;Menge\nSchraube;500\nMutter;120");
var kopf = parser.nextLine(); // erste Zeile = Spaltennamen
var anzahl = 0, zeile;
while ((zeile = parser.nextLine()) !== null) { // null = Dateiende
anzahl++;
log.debug("Zeile " + parser.line + ": " + zeile.join(" | "));
}
parser.close();
log.info(anzahl + " Zeilen gelesen");
parser.line ist die echte Zeilennummer in der Datei — bequemer als
mitzuzählen, und richtig auch dann, wenn ein Wert einen Zeilenumbruch enthält.
Teil 8 — Der Rundlauf¶
Zum Abschluss das, was in der Praxis am häufigsten gebraucht wird:
exportieren, extern bearbeiten, wieder einlesen. Weil
csv.stringify und csv.parse Gegenstücke sind, passt das zusammen, ohne dass
du dich um Trennzeichen kümmern musst.
// 1. exportieren
var original = [
{ Artikel: "Schraube", Menge: "500" },
{ Artikel: "Mutter", Menge: "120" }
];
var text = csv.stringify(original, { separator: ";" });
// 2. (hier würde die Fachabteilung die Datei bearbeiten)
// 3. wieder einlesen
var zurueck = csv.parse(text, { header: true });
log.info("zurück: " + zurueck.length + " Zeilen, erste Menge " + zurueck[0].Menge);
Dasselbe mit Daten aus der Datenbank — db.query liefert schon Objekte, also
ist der Export ein Einzeiler:
var zeilen = db.query("SELECT 1 AS nr, 'Hagen' AS ort UNION SELECT 2, 'Köln'");
log.info(csv.stringify(zeilen, { separator: ";" }));
Wichtig: Formeln in Zellen¶
Eine Zelle, die mit =, +, - oder @ beginnt, ist für Excel und
LibreOffice eine Formel. In einer CSV aus KCForms stehen Formularinhalte,
also fremde Eingaben — wer so eine Datei öffnet, führt sie aus. Das ist eine
bekannte Angriffsart, und Anführungszeichen helfen nicht dagegen: die gehören
zur CSV-Syntax und sind beim Öffnen schon weg.
KCForms stellt solchen Zellen deshalb von sich aus ein Apostroph voran. Excel zeigt dann den Text und rechnet nicht:
Reine Zahlen bleiben verschont — sonst würde aus jedem negativen Betrag
eine Textzelle, mit der niemand rechnen kann. -1500,00 bleibt also eine Zahl,
-2+3 wird entschärft.
Wenn du in einer erzeugten Datei ein Apostroph findest, das du nicht
geschrieben hast: das ist der Grund. Brauchst du die Werte unverändert — etwa
für ein System, das keine Formeln kennt — schaltest du es ab:
{ escapeFormulae: false }.
Wenn etwas nicht geht¶
| Beobachtung | Ursache | Abhilfe |
|---|---|---|
| Excel zeigt alles in Spalte A | Komma als Trennzeichen | separator: ";" |
Umlaute sind Kästchen oder ü |
BOM fehlt | bom: true |
| Umlaute sind Fragezeichen beim Lesen | Datei ist ANSI, nicht UTF-8 | encoding: "auto" |
| Zahlen sind in Excel linksbündig (= Text) | Dezimalpunkt | decimalSep: "," |
| Beim Lesen nur eine Spalte | Trennzeichen fest auf Komma gesetzt | Angabe weglassen oder richtig setzen |
| „Zeile NaN" in einer Meldung | forEach statt indizierter Schleife |
Teil 4 |
Kopfzeile heißt remove, size, get, … |
Object.keys auf einer Server-Zeile |
csv.stringify nimmt die Spalten selbst richtig |
Rechnen ergibt NaN |
Werte aus CSV sind Text | kc.num(wert) |
| Apostroph vor einem Wert | Formel-Schutz | Abschnitt oben |
| Eine Spalte fehlt im Ergebnis | zwei gleiche Spaltennamen | die zweite heißt name_2 |
sort(...) bricht ab |
auf einer Server-Liste gibt sort nichts zurück |
erst filter(...) oder slice(...) |
Kurz-Referenz¶
// Schreiben
csv.stringify(zeilen); // Objekte oder Arrays
csv.stringify(zeilen, { separator: ";", decimalSep: ",", bom: true });
csv.stringify(zeilen, { columns: ["b", "a"], header: false });
// Lesen
csv.parse(quelle); // Array von Arrays
csv.parse(quelle, { header: true }); // Array von Objekten
csv.parse(quelle, { header: true, encoding: "auto", trim: true, strict: true });
// Zeilenweise
var p = new csv.StandardParser(quelle);
while ((z = p.nextLine()) !== null) { /* … */ }
// Ausliefern
delivery.deliver(p.toByteArray(), { name: "export.csv", mime: "text/csv" }); // Bytes!
fs["/pfad/export.csv"].writeText(text);
ctx.attachments.add("export.csv", text);
Quelle für alle Lese-Funktionen: Text, fs-Datei, Dateianlage
(ctx.attachments[i]) oder Bytes.
Aufräumen¶
Die Übungsdateien dieses Workshops wieder entfernen:
var ordner = fs["/beispiele/csv-workshop"];
if (ordner.exists) {
var dateien = ordner.list();
for (var i = 0; i < dateien.length; i++) dateien[i].remove();
}
log.info("aufgeräumt");
Weiterlesen¶
- CSV schreiben & einlesen — die vollständige Referenz aller
Optionen, dazu
CSVServicefür übernommene Skripte. - Beispiele: CSV — 25 fertige Skripte vom Gruppenbericht bis zum Listenabgleich.
fs— Ordner, Dateien, Zeichensätze.- Dateianlagen — hochgeladene Dateien lesen und eigene anhängen.
pdfunddelivery— Dateien zum Herunterladen anbieten.- Hilfsfunktionen —
kc.numfür Zahlen aus Text.