Parsen von CSV Daten in JavaScript/TypeScript
Was vermeintlich trivial klingt, kann durchaus herausfordernd und komplex werden.

Was vermeintlich trivial klingt, kann durchaus herausfordernd und komplex werden.
Das Problem bei CSV-Dateien, ist ein etwas schwammiger Standard bzw. unterschiedliche Auslegung dessen, sowie das es eine große Variation in deren Formatierung gibt.
Ein allgemeiner Standard für das Dateiformat CSV existiert nicht, jedoch wird es im RFC 4180 grundlegend beschrieben.
Theoretisch ist auch Plain-text valides CSV.
Wie werden CSV-Dateien eigentlich strukturiert?
CSV steht für comma-separated values oder aber Character-separated values.
Das bedeutet, das verschiedenste Werte (Zeichen, Text, Zahlen etc.) durch Kommas (Separator) getrennt werden und somit eine Struktur aufgebaut wird.
Beispiel:
Date,Time,Room Temperature,Outside Temperature,
01-02-2023,8:00,23.2,5.2,
01-02-2023,9:00,23.0,5.3,
01-02-2023,10:00,22.9,5.3,
Nun gibt es allerdings Länderspezifische Unterschiede. So enthält eine CSV-Datei im Englischsprachigen Raum meist Kommas als Separator. Im Deutschsprachigen Raum werden zum Beispiel gerne Semikolons eingesetzt anstelle des Komma. Das hängt maßgeblich damit zusammen, dass im deutschen z.B. das Komma als Dezimaltrennzeichen genutzt wird.
Nun ist es aber eigentlich egal welches Zeichen als Separator verwendet wird, denn mit bestimmten Techniken, kann jedes Zeichen, ein valides Zeichen innerhalb einer CSV-Datei sein. Allerdings vereinfacht das nutzen eines Länderspezifischen Separator die Komplexität der CSV-Datei und damit die Verarbeitung dieser Datei.
Wollen wir aber zum Beispiel das Separator-Zeichen als Wert verwenden, dann muss der komplette Wert in ein Escape Zeichen gepackt werden. Hierfür wird regulär ein Double-Quote-Zeichen verwendet.
Beispiel:
Text,"One, two and three",Next Value,
Nun kann allerdings jedes Zeichen ein valides Zeichen innerhalb der CSV Datei sein.
Das bedeutet auch das Escape bzw. Double-Quote-Zeichen bedarf einer speziellen Behandlung. In solch einem Fall werden die im Text enthaltenen Double-Quote-Zeichen verdoppelt und der gesamte Wert wird dann ebenfalls in Double-Quote-Zeichen eingepackt.
Beispiel:
Text,"One, two and three","This is a quoted text: ""Some text""",
Das Double-Quote-Zeichen kann vom Escape Zeichen unterschieden werden, da es doppelt vorhanden ist.
Allerdings habe ich selbst noch nie solche Dateien gesehen und rate davon ab, solche Zeichen zu verwenden, da es tendenziell Problematisch sein könnte diese zu verwenden.
Wie können CSV-Daten erzeugt werden?
Das exportieren von CSV Dateien gestaltet sich vergleichsweise einfach
Also müssen wir das Escape-Zeichen verdoppeln, wenn es existiert und den Text in das Escape-Zeichen verpacken, wenn ein Separator- oder Escape-Zeichen vorhanden ist:
const escapeCsvChars = (inputText: string, escapeCharacter: string, delimiter: string) => {
let outputText = inputText;
// If escape character is included it must be escaped by doubling it
if(inputText.includes(escapeCharacter)) {
outputText = outputText.replaceAll(escapeCharacter, escapeCharacter+escapeCharacter);
}
/*
* Complete line must be double quoted if it includes an Escape character or a delimiter character
*/
if(inputText.includes(delimiter) || inputText.includes(escapeCharacter)) {
outputText = `"${outputText}"`;
}
return outputText;
};
Diese Funktion können wir dann folgendermaßen verarbeiten, indem wir durch einen Array aus Strings iterieren. Am Beispiel eines Array welcher eine einzelne Zeile der CSV-Datei repräsentiert:
const delimiter = ',';
const escapeCharacter = '"';
const singleLine = [
'Text',
'One, two and three',
'This is a quoted text: "Some text"',
];
let outputLine = '';
for(const column of singleLine) {
outputLine += escapeCsvChars(column, escapeCharacter, delimiter) + delimiter;
}
console.log(outputLine);
// Expected Output: Text,"One, two and three","This is a quoted text: ""Some text""",
Das ganze kann dann beliebig ausgebaut werden, je nach individueller Anforderung.
Wie können CSV-Daten geparst werden?
Dieser Teil ist deutlich aufwendiger und komplizierter, als der vorherige Schritt.
Um jede Möglichkeit mit einzubeziehen, müssen wir durch jedes Zeichen durch iterieren.
Wenn wir wissen, dass es in einer CSV-Datei keine Escape-Zeichen gibt, dann können wir natürlich, ganz simpel split()
auf einen String anwenden:
const splittedResult = line.split(delimiter);
Ansonsten müssen wir es eben aufwendig parsen:
const charAfter = 1;
/** Parses a line of CSV */
const parseCsvLine = (line: string, escapeCharacter: string, delimiter: string) => {
const result = [];
let startPosition = 0;
let hasEscape = false;
for(let index = 0; index < line.length; index++) {
const currentChar = line[index];
if(startPosition === index && currentChar === escapeCharacter) {
hasEscape = true;
continue;
}
if(!hasEscape) {
// If line does not have escape then we can simply search for the next delimiter
let currentPosition = line.indexOf(delimiter, startPosition);
// If line does not end with delimiter the position will result with -1, but it is end of line
if(currentPosition === noIndexFound) currentPosition = line.length;
const endPosition = currentPosition;
result.push(line.substring(startPosition, endPosition));
startPosition = currentPosition + charAfter;
index = currentPosition;
continue;
}
/*
* If we have escape we need to search for escape followed by delimiter, but when escape is used these chars could appear as normal text.
* And escape can also be used as normal text and is therfore duplicated
* We can simply check that by checking for duplicate escapes.
* All escape signs are doubled except for the first and the last sign and after the last there comes the escape
*/
const nextChar = line[index+charAfter];
if(currentChar === escapeCharacter && nextChar === escapeCharacter) {
// Just text - ignore it by jumping further
index++;
continue;
}
if(currentChar === escapeCharacter && nextChar === delimiter) {
// Found the end!
const endPosition = index;
// We need to correct the position to cut the escape character
const substring = line.substring(startPosition+charAfter, endPosition);
// Remove duplicate quotes/escapes
const withoutDuplicates = substring.replaceAll(escapeCharacter+escapeCharacter, escapeCharacter);
result.push(withoutDuplicates);
startPosition = endPosition + charAfter + charAfter;
index += charAfter;
hasEscape = false;
continue;
}
}
return result;
};
Zusammengefasst können wir dies dann folgendermaßen aufrufen. Inklusive des simplen Ansatz vom Einstieg (Um die Performance zu verbessern).
const lastElement = -1;
/**
* Converts a line of CSV into a usefull array of content
* CSV can contain it's own delimiters as character but then the whole string will be wrapped in the escape character
* Same happens if the escape character is included, but then the original escape character is used doubled.
*/
const convertCsvLine = (line: string, escapeCharacter: string, delimiter: string) => {
// Cannot parse if format does not match
if(!line.includes(delimiter)) throw new Error('Wrong file format: Missing delimiter');
// Delimiters need to be escaped if you want to use them as plain text - So, if not... Save some performance
if(!line.includes(escapeCharacter)) {
const splittedResult = line.split(delimiter);
// The line can end with delimiter which results in an unnessecary empty string
if(splittedResult.at(lastElement) === '') splittedResult.pop();
return splittedResult;
}
// Otherwise parse deeply
return parseCsvLine(line, escapeCharacter, delimiter);
};
const delimiter = ',';
const escapeCharacter = '"';
const testString = 'Text,"One, two and three","This is a quoted text: ""Some text""",';
console.log(convertCsvLine(testString, escapeCharacter, delimiter));
// Expected output: (3) ['Text', 'One, two and three', 'This is a quoted text: "Some text"']
Das ganze kann dann ebenfalls beliebig ausgebaut werden, je nach individueller Anforderung.
Fazit
Solange CSV-Dateien keine Besonderheiten rund um Escape-Zeichen aufweißen, ist deren Handhabung vergleichsweise einfach.
Wie gezeigt, wird es besonders beim einlesen etwas komplizierter, da jedes Zeichen ein valides Zeichen im Text sein kann.
Wenn man sich an diese Formatierung hält, dann kommen auch diverse Programme wie Excel o.ä. damit klar (diese Programme erzeugen ebenfalls eine solche Struktur).