aboutsummaryrefslogtreecommitdiff
path: root/doc
diff options
context:
space:
mode:
Diffstat (limited to 'doc')
-rw-r--r--doc/csv.md378
1 files changed, 0 insertions, 378 deletions
diff --git a/doc/csv.md b/doc/csv.md
deleted file mode 100644
index acfa330..0000000
--- a/doc/csv.md
+++ /dev/null
@@ -1,378 +0,0 @@
1
2# csv - CSV-Dateien parsen
3
4- [x] korrekt
5- [x] einfach
6- [x] intuitiv
7- [x] schnell
8- [x] klein
9- [x] erweiterbar
10- [x] robust
11- [x] einfachst mögliche Lizenz
12
13Kurz: suckless!
14
15## Schnellstart für Ungeduldige
16
17**1\. Füge die beiden Dateien [csv.c](csv.c) und [csv.h](csv.h) Deinem C/C++ Projekt hinzu.**
18
19**2\. Inkludiere die Headerdatei `csv.h`:**
20
21~~~
22#include "csv.h"
23~~~
24
25**3\. Erstelle und initialisiere ein CSV-Objekt:**
26
27~~~
28csv_t csv = { 0 }; oder csv_t csv;
29 csv_init(&csv);
30~~~
31
32**4\. Verarbeite die CSV-Datei:**
33
34~~~
35for ( size_t nf; (nf = csv_read(&csv, in)) != 0; ) { /* eine Zeile aus `in` lesen */
36 for ( size_t i = 0; i != nf; i++ ) { /* `nf` beinhaltet die Feldanzahl */
37 const char *s = csv_field(&csv, i); /* Feld `i` der gelesenen Zeile ansprechen */
38 /* ... */
39 }
40}
41~~~
42
43Es sind nur zwei Funktionen notwendig, um die Daten des FILE-Streams `in` vollständig zu verarbeiten:
44`csv_read()` liest eine Zeile aus dem Parameter `in` und liefert die Anzahl der erfolgreich geparsten Felder oder **0** bei Dateiende.
45`csv_field()` liefert einen (read only) `const char`-Zeiger für das i-te Feld.
46
47Der 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.
48
49Mehr 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.
50
51## Motivation
52
53Die meisten anderen CSV-Bibliotheken passen nicht:
54
55- falsche Verarbeitung bei eingeklammerten Feldern:
56 C++: `getline()` -> `stringstream` -> `getline()`;
57 C: `fgets()` -> `strtok()`
58- fehlerhafte Verarbeitung von "problematischen" CSV-Dateien (Zeilenumbrüche innerhalb einer Zelle)
59- unvorteilhafte Lizenz (GPL)
60- umständliche API (Callbacks zum Verarbeiten der Inhalte, seltsames Handling am Dateiende)
61- globale Variablen, die den Einsatz in Multi-Threadumgebungen unmöglich machen
62- C++ wenn ich C brauche
63- Java/Python/Rust/Ruby/${YourLanguageHere} wenn ich C brauche
64- Langsam, Aufgebläht
65- schlecht oder gar nicht anpassbar
66
67Lösung: => Selber schreiben!
68
69## Tutorial
70
71**1\. Ausgangspunkt: ein Grundgerüst**
72
73Nehmen wir an, wir schreiben eine Funktion zur CSV-Verarbeitung.
74Das Grundgerüst sieht dann etwa so aus:
75
76~~~
77bool
78processFile(const char *filename)
79{
80 FILE *in;
81
82 if ( (in = fopen(filename, "r")) == NULL ) {
83 return false;
84 }
85
86 // HIERHER KOMMT DER CSV-HANDLING CODE
87
88 fclose(in);
89 return true;
90}
91~~~
92
93Wir fügen als erstes etwas Code hinzu:
94
95~~~
96csv_t csv;
97csv_init(&csv);
98
99while ( csv_read(&csv, in) ) {
100 const size_t nf = csv_nfields(&csv);
101 // ...
102}
103csv_cleanup(&csv);
104~~~
105
106Dieser Code erzeugt ein neues `csv_t`-Objekt im Speicher welches anschließend durch `csv_init()` mit Standardwerten initialisiert wird.
107
108Dann 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:
109
110~~~
111// ...
112size_t nf;
113while ( (nf = csv_read(&csv, in)) != 0 ) {
114 assert(csv_nfiedls(&csv) == nf);
115 // ...
116}
117// ...
118~~~
119
120Der Rückgabewert **0** bei einem Aufruf von `csv_read()` kennzeichnet das Ende des Datenstroms.
121
122> **Hinweis:**
123> 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.
124
125Der 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.
126
127**2\. Optionen einstellen**
128
129Wenn 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.
130
131Wir berücksichtigen dies bei der Initialisierung eines `csv_options_t`-Objektes.
132
133~~~
134csv_options_t csv_options = csv_default_options;
135csv_options.field_delimiter = '"';
136csv_options.field_separator = ',';
137
138csv_t csv;
139csv_init_opt(&csv, &csv_options);
140~~~
141
142Damit 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.
143
144> **Hinweis:**
145> `csv_init(&csv);` ist letztlich nur eine kürzere Schreibweise für `csv_init_opt(&csv, &csv_default_options);`.
146
147> **Hinweis:**
148> Die Standardwerte für Feldtrenner und Feldbegrenzer lassen sich als Übersetzungsoptionen beim Compilieren über die Makros `CSV_DEFAULT_SEPARATOR` und `CSV_DEFAULT_DELIMITER` setzen.
149
150Der Rest der Schleife bleibt gleich.
151
152**3\. Fehlerbehandlung hinzufügen**
153
154Bei 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.
155
156Die 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:
157
158~~~
159jmp_buf env;
160csv_options.cb_error = csvErrorHandler;
161csv_options.cb_error_arg = &env;
162~~~
163
164`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:
165
166~~~
167static void
168csvErrorHandler(csv_err_t err, void *cb_arg)
169{
170 fprintf(stderr, "error while csv-processing: %s\n", csv_err_str(err));
171 longjmp(*((jmp_buf *) cb_arg), err);
172}
173~~~
174
175Mit Hilfe des Parameter `err` wird eine einfache Fehlermeldung protokolliert. Anschließend rufen wir `longjmp()` auf, damit die Fehlerbehandlung auf höherer Ebene fortgesetzt wird.
176
177Die Hauptschleife in unserer Funktion sieht nun so aus:
178
179~~~
180csv_options_t csv_options = csv_default_options;
181csv_options.field_delimiter = '"';
182csv_options.field_separator = ',';
183
184jmp_buf env;
185csv_options.cb_error = csvErrorHandler;
186csv_options.cb_error_arg = &env;
187
188csv_t csv;
189csv_init_opt(&csv, &csv_options);
190
191switch ( setjmp(env) ) {
192case CSV_ERR_OK:
193 while ( csv_read(&csv, in) ) {
194 const size_t nf = csv_nfields(&csv);
195 // ...
196 }
197 // kein csv_cleanup() notwendig weil die Datei an
198 // dieser Stelle vollständig verarbeitet wurde.
199 break;
200
201case CSV_ERR_OUT_OF_MEMORY:
202 csv_cleanup(&csv);
203 break;
204
205case CSV_ERR_OUT_OF_RANGE:
206 csv_cleanup(&csv);
207 break;
208
209case CSV_ERR_IO_READ:
210 csv_cleanup(&csv);
211 break;
212}
213~~~
214
215## API
216
217### Typen
218
219**Fehlercodes** für die Callbackhandler:
220
221~~~
222typedef enum {
223 CSV_ERR_OK = 0,
224 CSV_ERR_OUT_OF_MEMORY = -1,
225 CSV_ERR_OUT_OF_RANGE = -2,
226 CSV_ERR_IO_READ = -3,
227 CSV_ERR_IO_WRITE = -4
228} csv_err_t;
229~~~
230
231**Optionen** für das Parsen eines CSV-Streams:
232
233~~~
234typedef struct {
235 int field_delimiter;
236 int field_separator;
237
238 void (*cb_error)(csv_err_t, void *);
239 void *cb_error_arg;
240
241 void *(*cb_allocate)(size_t, size_t, void *);
242 void *(*cb_reallocate)(void *, size_t, size_t, void *);
243 void (*cb_free)(void *, size_t, size_t, void *);
244 void *cb_memory_arg;
245} csv_options_t;
246
247extern const csv_options_t csv_default_options;
248~~~
249
250**Contextobjekt** zur Aufnahme der aktuell gelesenen Zeile:
251
252~~~
253typedef struct {
254 csv_options_t *csv_options;
255 /* additional internal fields */
256} csv_t;
257~~~
258
259### Funktionen
260
261**Initialisieren**
262
263~~~
264void csv_init(csv_t *csv);
265void csv_init_opt(csv_t *csv, const csv_options_t *const csv_options);
266~~~
267
268Initialisiert ein `csv_t`-Objekt.
269
270Parameter
271: **`csv`** ein Zeiger vom Typ `csv_t`.
272: **`csv_options`** ein Zeiger vom Typ `csv_options_t`
273
274`csv_init(&c);` ist eine kürzere Schreibweise für `csv_init_opt(&c, &csv_csv_default_options);`
275
276Mit der Funktion `csv_init_opt()` und einem angepassten `csv_options_t`-Objektes kann das Verhalten des CSV-Parsers zur Laufzeit parametrisiert werden.
277
278**Manuelles Freigeben**
279
280~~~
281void csv_cleanup(csv_t *csv);
282~~~
283
284Gibt den belegten Speicher eines `csv_t`-Objektes wieder frei.
285
286Parameter
287: **`csv`** ein Zeiger initialisiertes `csv_t`-Objekt.
288
289`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.
290
291**Zeilenweises Lesen**
292
293~~~
294size_t csv_read(csv_t *csv, FILE *in);
295~~~
296
297Liest einen Datensatz aus dem FILE-Stream, speichert das Ergebnis in `csv` und liefert die Anzahl der gefundenen Felder zurück.
298
299Parameter
300: **`csv`** ein Zeiger initialisiertes `csv_t`-Objekt.
301: **`in`** ein Zeiger auf einen geöffneten C-FILE-Stream.
302
303Rückgabe
304: Anzahl der gelesenen CSV-Felder einer Zeile oder 0 bei Dateiende.
305
306**Feldzugriff**
307
308~~~
309size_t csv_nfields(const csv_t *const csv);
310const char *csv_field(const csv_t *const csv, size_t idx);
311~~~
312
313Die beiden Funktionen ermöglichen einen Zugriff auf die einzelnen Felder des zuletzt gelesenen Datensatzes.
314
315Parameter
316: **`csv`** ein Zeiger initialisiertes `csv_t`-Objekt.
317: **`idx`** Index des zu lesenden Feldes. (0 <= idx < `csv_nfields(&csv)`)
318
319Rückgabe
320: `csv_nfields()` liefert die Anzahl der Felder des zuletzt gelesenen Datensatzes.
321: `csv_field()` liefert einen Zeiger auf das Feld `idx` des zuletzt gelesenen Datensatzes.
322
323**Fehlercodes als Textmeldung**
324
325~~~
326const char *csv_err_str(csv_err_t csv_err);
327~~~
328
329Liefere eine textuelle Meldung zum Fehlercode `csv_err`.
330
331Parameter
332: **`csv_err`** ein Fehlercode vom Typ `csv_err_t`.
333
334Rückgabe
335: Zeiger auf die entspreche Fehlermeldung.
336
337## Anpassungen
338
339Die CSV-Bibliothek kann an verschiedene Bedürfnisse angepasst werden.
340
341### Übersetzungsoptionen
342
343Beim 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.
344
345Option Beschreibung Standardwert
346-------------------------- -------------- ---------------------------------
347`CSV_DEFAULT_DELIMITER` Feldbegrenzer `"`
348`CSV_DEFAULT_SEPARATOR` Feldtrenner `,`
349-------------------------- -------------- ---------------------------------
350Table: Makros
351
352Soll 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:
353
354~~~
355cc -DCSV_DEFAULT_SEPARATOR="';'" -o csv-german.o -c csv.c
356~~~
357
358Anschließend wird die Datei `csv-german.o` statt `csv.o` zum Projekt dazugelinkt.
359
360Die Headerdatei (`csv.h`) bleibt unverändert.
361
362### Speicherallokierer
363
364TODO!
365
366## Lizenz
367
368In 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.
369
370Aus 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.
371
372~~~
373Copyright © 2020 Thomas Schmucker
374This work is free. You can redistribute it and/or modify it under the
375terms of the Do What The Fuck You Want To Public License, Version 2,
376as published by Sam Hocevar. See https://www.wtfpl.net/ for more details.
377~~~
378