1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
|
# 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 **0** 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.
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 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
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 Returnwert 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 `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 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);
~~~
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 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 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 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 dazugelinkt.
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](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.
~~~
|