aboutsummaryrefslogtreecommitdiff

csv - CSV-Dateien parsen

  • korrekt
  • einfach
  • intuitiv
  • schnell
  • klein
  • erweiterbar
  • robust
  • einfachst mögliche Lizenz

Kurz: suckless!

Schnellstart für Ungeduldige

1. Füge die beiden Dateien csv.c und 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 Stream in und liefert die Anzahl der erfolgreich analysierten Felder oder 0 bei Dateiende. csv_field() liefert einen (read only) const char-Zeiger für das ite 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.

Mehr Arbeit ist nur noch dann notwendig, wenn eine Fehlerbehandlung zum Projekt hinzugefügt wird. Für die meisten einfachen Fälle reichen diese Schritte wahrscheinlich schon aus.

Motivation

Die meisten anderen CSV-Bibliotheken passen nicht:

  • falsche Verarbeitung bei eingeklammerten Feldern:
    C++: getline() -> stringstream -> getline();
    C: fgets() -> strtok()
  • fehlerhafte Verarbeitung von “problematischen” CSV-Dateien (Zeilenumbrüche innerhalb einer Zelle)
  • unvorteilhafte Lizenz (GPL)
  • umständliche API (Callbacks zum Verarbeiten der Inhalte, seltsames Handling am Dateiende)
  • globale Variablen, die den Einsatz in Multithread-Umgebungen 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

Lösung: => 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 durch 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 mit Hilfe der Funktion csv_nfields() ermittelt. Alternativ liefert auch der Rückgabewert von csv_read() die Feldanzahl:

// ...
size_t nf;
while ( (nf = csv_read(&csv, in)) != 0 ) { 
    assert(csv_nfiedls(&csv) == nf);
    // ...
}
// ...

Der Rückgabewert 0 bei einem Aufruf von csv_read() kennzeichnet das Ende des Datenstroms.

Hinweis:
Das Ende einer CSV-Datei mit 0-gelesenen Feldern zu kennzeichnen ist vollkommen legitim. Eine “leere” Zeile innerhalb der Datei würde nämlich als genau ein (leeres) Feld interpretiert, so dass der Rückgabewert hier 1 wäre. Anders gesagt: Es gibt in CSV-Dateien keine Zeilen mit 0 Feldern.

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 (;) zum Trennen der einzelnen Felder.

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);

Damit wir nicht alle Felder in csv_options ändern müssen, initialisieren wir dieses zunächst mit den Standardwerten aus csv_default_options und passen dann field_delimiter und field_separator für unsere Bedürfnisse an. Das csv-Objekt wird nun mit der Funktion csv_init_opt() initialisiert, die einen zusätzlichen Zeiger auf ein csv_options_t-Objekt erwartet.

Hinweis:
csv_init(&csv); ist letztlich nur eine kürzere Schreibweise für csv_init_opt(&csv, &csv_default_options);.

Hinweis:
Die Standardwerte für Feldtrenner und Feldbegrenzer lassen sich als Übersetzungsoptionen beim Compilieren über die Makros CSV_DEFAULT_SEPARATOR und CSV_DEFAULT_DELIMITER setzen.

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 den 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_error_arg)
{
    fprintf(stderr, "error while csv-processing: %s\n", csv_err_str(err));
    longjmp(*((jmp_buf *) cb_error_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 Callback-Handler:

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;

Context Objekt 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);

Initialisiert ein csv_t-Objekt.

Parameter : csv ein Zeiger vom Typ csv_t. : csv_options ein Zeiger vom Typ csv_options_t

csv_init(&c); ist eine kürzere Schreibweise für csv_init_opt(&c, &csv_csv_default_options);

Mit der Funktion csv_init_opt() und einem angepassten csv_options_t-Objektes kann das Verhalten des CSV-Parsers zur Laufzeit parametrisiert werden.

Manuelles Freigeben

void csv_cleanup(csv_t *csv);

Gibt den belegten Speicher eines csv_t-Objektes wieder frei.

Parameter : csv ein Zeiger auf ein initialisiertes csv_t-Objekt.

csv_cleanup() gibt den verwendeten Speicher des csv-Objektes wieder frei. Ein direkter Aufruf dieser Funktion ist nur im Fehlerfall notwendig weil dann die automatische Speicherfreigabe der Bibliothek nicht greift.

Zeilenweises Lesen

size_t csv_read(csv_t *csv, FILE *in);

Liest einen Datensatz aus dem FILE-Stream, speichert das Ergebnis in csv und liefert die Anzahl der gefundenen Felder zurück.

Parameter : csv ein Zeiger auf ein initialisiertes csv_t-Objekt. : in ein Zeiger auf einen geöffneten C-FILE-Stream.

Rückgabe : Anzahl der gelesenen CSV-Felder einer Zeile oder 0 bei Dateiende.

Feldzugriff

size_t      csv_nfields(const csv_t *const csv);
const char *csv_field(const csv_t *const csv, size_t idx);

Die beiden Funktionen ermöglichen einen Zugriff auf die einzelnen Felder des zuletzt gelesenen Datensatzes.

Parameter : csv ein Zeiger auf ein initialisiertes csv_t-Objekt. : idx Index des zu lesenden Feldes. (0 <= idx < csv_nfields(&csv))

Rückgabe : csv_nfields() liefert die Anzahl der Felder des zuletzt gelesenen Datensatzes. : csv_field() liefert einen Zeiger auf das Feld idx des zuletzt gelesenen Datensatzes.

Fehlercodes als Textmeldung

const char *csv_err_str(csv_err_t csv_err);

Liefere eine textuelle Meldung zum Fehlercode csv_err.

Parameter : csv_err ein Fehlercode vom Typ csv_err_t.

Rückgabe : Zeiger auf die entspreche Fehlermeldung.

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 (,) standardmäßig 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 dazu gelinkt.

Die Headerdatei (csv.h) bleibt unverändert.

Speicherallokierer

TODO!

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 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.