# csv - CSV-Dateien parsen - [x] korrekt - [x] einfach - [x] intuitiv - [x] schnell - [x] klein - [x] erweiterbar - [x] robust - [x] einfachst mögliche Lizenz Kurz: suckless! ## Schnellstart für Ungeduldige 1\. Füge die beiden Dateien [csv.c](csv.c) und [csv.h](csv.h) Deinem C/C++ Projekt hinzu. 2\. Inkludiere die Headerdatei `csv.h`. ~~~ #include "csv.h" ~~~ 3\. Erstelle und initialisiere ein CSV-Objekt. ~~~ csv_t csv = { 0 }; oder csv_t csv; csv_init(&csv); ~~~ 4\. Verarbeite die CSV-Datei ~~~ for ( size_t nf; (nf = csv_read(&csv, in)) != 0; ) { /* eine Zeile aus `in` lesen */ for ( size_t i = 0; i != nf; i++ ) { /* `nf` beinhaltet die Feldanzahl */ const char *s = csv_field(&csv, i); /* Feld `i` der gelesenen Zeile ansprechen */ /* ... */ } } ~~~ Es sind nur zwei Funktionen notwendig, um die Daten des FILE-Streams `in` vollständig zu verarbeiten: `csv_read()` liest eine Zeile aus dem Parameter `in` und liefert die Anzahl der erfolgreich geparsten Felder oder **`EOF`** bei Dateiende. `csv_field()` liefert einen (read only) `const char`-Zeiger für das i-te Feld. Der Speicher für das CSV-Objekt wird automatisch von `csv_read()` beim ersten Aufruf alloziert und wieder freigeben nachdem der Stream `in` vollständig gelesen wurde. ## Motivation Die meisten anderen CSV-Bibliotheken passen nicht: - falsche Verarbeitung bei eingeklammerten Feldern: C++: `getline()` -> `stringstream` -> `getline()`; C: `fgets()` -> `strtok`() Irgendwelche regulären Ausdrücke - fehlerhafte Verarbeitung von "problematischen" CSV-Dateien (Linebreaks innerhalb einer Zelle) - unvorteilhafte Lizenz (GPL) - umständliche API (Callbacks zum Verarbeiten der Inhalte, seltsames Handling am Dateiende) - globale Variablen, die den Einsatz in Multi-Threadumgebungen unmöglich machen - C++ wenn ich C brauche - Java/Python/Rust/Ruby/${YourLanguageHere} wenn ich C brauche - Langsam, Aufgebläht - schlecht oder gar nicht anpassbar => Selber schreiben! ## Tutorial **1\. Ausgangspunkt: ein Grundgerüst** Nehmen wir an, wir schreiben eine Funktion zur CSV-Verarbeitung. Das Grundgerüst sieht dann etwa so aus: ~~~ bool processFile(const char *filename) { FILE *in; if ( (in = fopen(filename, "r")) == NULL ) { return false; } // [ HIERHER KOMMT DER CSV-HANDLING CODE ] fclose(in); return true; } ~~~ Wir fügen als erstes etwas Code hinzu: ~~~ csv_t csv; csv_init(&csv); while ( csv_read(&csv, in) ) { const size_t nf = csv_nfields(&csv); // ... } csv_cleanup(&csv); ~~~ Dieser Code erzeugt ein neues `csv_t`-Objekt im Speicher welches anschließend mit `csv_init()` mit Standardwerten initialisiert wird. Dann wird der Datenstrom `in` zeilenweise in den Speicher eingelesen und innerhalb der Schleife verarbeitet. Die Anzahl der Felder pro Zeile wird in der Schleife mit Hilfe der Funktion `csv_nfields()` ermittelt. Alternativ liefert auch der Returnwert des vorherigen `csv_read()`-Aufrufs die Feldanzahl. Der Aufruf von `csv_cleanup()` ist nicht zwingend notwendig, da alle belegten Ressourcen beim letzten `csv_read()` automatisch freigegeben werden, wenn es keinen Fehler bei der Dateiverarbeitung gab. **2\. Optionen einstellen** Wenn nichts weiter angegeben ist, gehen wir von einem Komma (`,`) als Feldtrenner sowie doppelten Anführungszeichen (`"`) als Feldbegrenzer aus. Die meisten CSV-Dateien im deutschsprachigen Raum verwenden jedoch ein Semikolon (`;`) als Feldtrenner. Wir berücksichtigen dies bei der Initialisierung eines `csv_options_t`-Objektes. ~~~ csv_options_t csv_options = csv_default_options; csv_options.field_delimiter = '"'; csv_options.field_separator = ','; csv_t csv; csv_init_opt(&csv, &csv_options); ~~~ Das `csv`-Objekt wird nun mit der Funktion `csv_init_opt()` initialisiert, die einen zusätzlichen Zeiger auf ein `csv_options_t`-Objekt erwartet. Damit wir nicht alle Felder von `csv_options` ändern müssen, initialisieren wir dieses zunächst mit `csv_default_options`. > **Hinweis:** > `csv_init(&csv);` ist letztlich nur eine kürzere Schreibweise für `csv_init_opt(&csv, &csv_default_options);`. Der Rest der Schleife bleibt gleich. **3\. Fehlerbehandlung hinzufügen** Bei der Verarbeitung können verschiedene Fehler auftreten, die ein robustes Programm selbstverständlich behandelt. Leider stellt die Programmiersprache C keinen Exception-Mechanismus zur Verfügung, so dass sich der Programmierer an dieser Stelle anders behelfen muss. Die beste Variante, die dem klassischen Exceptions am nächsten kommt, sind die beiden C-Funktionen `setjmp()` und `longjmp()`, mit deren Hilfe wir nun die Fehlerbehandlung umsetzen. Wir benötigen dafür die beiden Felder `cb_error` und `cb_error_arg` des `csv_options`-Objektes: ~~~ jmp_buf env; csv_options.cb_error = csvErrorHandler; csv_options.cb_error_arg = &env; ~~~ `cb_error` ist ein Zeiger auf eine Callback-Funktion, die im Falle eines Fehlers aus der CSV-Bibliothek heraus aufgerufen wird. Ihr werden ein Fehlerwert vom Typ `csv_err_t` sowie ein Zeiger auf benutzerdefinierte Daten -- in unserem Fall ein Zeiger auf eine `jmp_buf`-Variable -- mit übergeben: ~~~ static void csvErrorHandler(csv_err_t err, void *cb_arg) { fprintf(stderr, "error while csv-processing: %s\n", csv_err_str(err)); longjmp(*((jmp_buf *) cb_arg), err); } ~~~ Mit Hilfe des Parameter `err` wird eine einfache Fehlermeldung protokolliert. Anschließend rufen wir `longjmp()` auf, damit die Fehlerbehandlung auf höherer Ebene fortgesetzt wird. Die Hauptschleife in unserer Funktion sieht nun so aus: ~~~ csv_options_t csv_options = csv_default_options; csv_options.field_delimiter = '"'; csv_options.field_separator = ','; jmp_buf env; csv_options.cb_error = csvErrorHandler; csv_options.cb_error_arg = &env; csv_t csv; csv_init_opt(&csv, &csv_options); switch ( setjmp(env) ) { case CSV_ERR_OK: while ( csv_read(&csv, in) ) { const size_t nf = csv_nfields(&csv); // ... } // kein csv_cleanup() notwendig weil die Datei an // dieser Stelle vollständig verarbeitet wurde. break; case CSV_ERR_OUT_OF_MEMORY: csv_cleanup(&csv); break; case CSV_ERR_OUT_OF_RANGE: csv_cleanup(&csv); break; case CSV_ERR_IO_READ: csv_cleanup(&csv); break; } ~~~ ## API ### Typen **Fehlercodes** für die Callbackhandler: ~~~ typedef enum { CSV_ERR_OK = 0, CSV_ERR_OUT_OF_MEMORY = -1, CSV_ERR_OUT_OF_RANGE = -2, CSV_ERR_IO_READ = -3, CSV_ERR_IO_WRITE = -4 } csv_err_t; ~~~ **Optionen** für das Parsen eines CSV-Streams: ~~~ typedef struct { int field_delimiter; int field_separator; void (*cb_error)(csv_err_t, void *); void *cb_error_arg; void *(*cb_allocate)(size_t, size_t, void *); void *(*cb_reallocate)(void *, size_t, size_t, void *); void (*cb_free)(void *, size_t, size_t, void *); void *cb_memory_arg; } csv_options_t; extern const csv_options_t csv_default_options; ~~~ **Contextobjekt** zur Aufnahme der aktuell gelesenen Zeile: ~~~ typedef struct { csv_options_t *csv_options; /* additional internal fields */ } csv_t; ~~~ ### Funktionen **Initialisieren** ~~~ void csv_init(csv_t *csv); void csv_init_opt(csv_t *csv, const csv_options_t *const csv_options); ~~~ **Manuelles Freigeben** ~~~ void csv_cleanup(csv_t *csv); ~~~ **Zeilenweises Lesen** ~~~ size_t csv_read(csv_t *csv, FILE *in); ~~~ **Feldzugriff** ~~~ size_t csv_nfields(const csv_t *const csv); const char *csv_field(const csv_t *const csv, size_t idx); ~~~ **Fehlercodes als Textmeldung** ~~~ const char *csv_err_str(csv_err_t csv_err); ~~~ ## Anpassungen Die CSV-Bibliothek kann an verschiedene Bedürfnisse angepasst werden. ### Übersetzungsoptionen Beim Compilieren der Datei csv.c können die Standardwerte für Feldbegrenzer und Feldtrenner als Makrodefinitionen `CSV_DEFAULT_DELIMITER` und `CSV_DEFAULT_SEPARATOR` voreingestellt werden. Option Beschreibung Standardwert -------------------------- -------------- --------------------------------- `CSV_DEFAULT_DELIMITER` Feldbegrenzer `"` `CSV_DEFAULT_SEPARATOR` Feldtrenner `,` -------------------------- -------------- --------------------------------- Table: Makros Soll beispielsweise das in Deutschland viel häufiger vorkommende Semikolon (`;`) statt einem Komma (`,`) als Trennsymbol zwischen den Feldern einer CSV-Datei benutzt werden sieht der Aufruf zum Compilieren so aus: ~~~ cc -DCSV_DEFAULT_SEPARATOR="';'" -o csv-german.o -c csv.c ~~~ Anschließend wird die Datei `csv-german.o` statt `csv.o` zum Projekt dazugelinkt. Die Headerdatei (`csv.h`) bleibt unverändert. ## Lizenz In den letzten Jahrzehnten habe ich massiv vom Internet und seinen Inhalten profitiert. Ein gutes Stück meiner Kenntnisse und Fähigkeiten verdanke ich den vielen Foren, Webseiten, Newsgroups und Individuen die Informationen und Software kostenfrei und allgemein zugänglich bereitstellen. Aus diesem Grund stelle ich meine Software (sofern nicht anders angegeben) unter der freizügigen [Do What the Fuck You Want to Public License](http://www.wtfpl.net) zur Verfügung. Vielleicht findet jemand den Code nützlich oder kann Erkenntnisse daraus gewinnen. ~~~ Copyright © 2020 Thomas Schmucker This work is free. You can redistribute it and/or modify it under the terms of the Do What The Fuck You Want To Public License, Version 2, as published by Sam Hocevar. See https://www.wtfpl.net/ for more details. ~~~