Einfache CSV Daten zu parsen, ist nicht besonders kompliziert. Mit "einfach" meine ich, dass es keine Separatoren, Zeilenumbrüche oder Anführungszeichen in den Datenfeldern gibt. In diesem Fall reicht zeilenweises Lesen und anschließendes Sequenzieren. Dafür benötigt man keine gesonderte Library,
Es gibt bereits eine ganze Reihe von CSV Parsern. Nicht ganz so viele sind in C geschrieben, vermutlich weil objektorientierte Sprachen bessere Möglichkeiten bieten. Im Download ist mein Versuch für einen Parser, der CSV Daten in eine Struktur in den virtuellen Speicher deserialisiert. Das bedeutet einerseits, dass man jederzeit in der Lage ist auf jedes beliebige Datenfeld zuzugreifen. Andererseits bedeutet es aber auch, dass es sicher bessere, speicherschonendere Libraries gibt, wenn man wirklich große Datenmengen verarbeiten will.
Das Interface habe ich versucht so einfach wie möglich zu halten. Die dahinterliegende Implementierung ist allerdings ein Type-Punning Feuerwerk mit gleichzeitigem Versuch die längste jemals gesehene Parameterliste zu schreiben. (Selbstironisch für "ich hab schon verständlicheren Code geschrieben als diesen". C kennt leider keine C++ Templates um Code zu deduplizieren.)
Das Verhalten des Parsers lehnt sich stark an die im RFC 4180 beschriebenen Empfehlungen an.
Darüber hinaus gibt es folgende Features, die auch von einigen anderen Parsern unterstützt werden:
- Definition des Separators
- mögliche Definition eines Zeichens, das einen Kommentar einleitet
- mögliches Überspringen von Leerzeilen
- Zugriff auf Datenfelder mit Hilfe des Spaltennamens
Unterschiede zu anderen Parsern:
- Unterstützung von UTF-16 Dateinamen für Windows
- Unterstützung von
- Unterstützung eines möglichen Byte Order Marks am Beginn der zu verarbeiteten Daten
Nicht implementiert:
- Trim Funktionalität für Leerzeichen/Tabs am Anfang und Ende von Datenfeldern (da diese einen erheblichen Zuwachs an Komplexität bedeuten würde)
CSV transportiert oft numerische Werte. Das Textformat dieser numerischen Werte ist dabei nicht standardisiert und oft von lokalen Einstellungen abhängig. Zusätzlich zum reinen Parsen der CSV Daten ist die Möglichkeit implementiert, numerische Datenfelder für die
Vergleich zum RFC 4180:
Interface:
Strukturen:
Rohdaten als gelesene Bytes einer Datei oder Zeichen eines Strings.
Parameter und Optionen zur Steuerung des Parsing Prozesses.
Repräsentiert ein CSV Datenfeld
Repräsentiert einen CSV Datensatz
Repräsentiert einen Spaltenname in der CSV Kopfzeile
Repräsentiert die Liste von CSV Datensätzen, sowie weitere Metadaten
Funktionen:
Lesen von CSV Rohdaten aus einer Datei.
Lesen von CSV Rohdaten aus einer Datei. (Unterstützung für nicht-ASCII Dateinamen unter Windows)
Aufbereitung von CSV Rohdaten aus einem String.
Deserialisierung von CSV Daten in ein `csv_t` Objekt.
Zugriff auf ein Datenfeld über die Indizes des Datensatzes und des Felds.
Zugriff auf ein Datenfeld über den Index des Datensatzes und den Spaltenname.
Änderung von Dezimaltrennzeichen und Tausendertrennzeichen eines Datenfelds um mit den
Freigabe von reserviertem Speicher.
Macro:
Ruft
Globales Objekt (nur C++):
Lambda Objekt, das den Deleter für Smart Pointers (z.B.
Typ (nur C++):
Typ des
Detaillierte Beschreibungen finden sich im
Beispiele:
C Code
(*) Wann ist es nützlich dem Parser einen Hinweis auf den erwarteten Zeichentyp zu geben?
Dies ist nur für Windows relevant. Sehr kleine Datenmengen können mehrdeutig sein. Das Beispiel unten zeigt das Verhalten mit nur 2 Bytes Datenmenge. Wenn diese das Ohm Zeichen repräsentieren sollen, dann muss `fallbackNarrow` mit `false` angegeben werden. Üblicherweise besteht CSV aber aus ausreichend Daten, um Unklarheiten zu vermeiden. Komma-Trennzeichen oder Zeilenumbrüche würden bspw. genügen um den Zeichentyp eindeutig zu machen.
C++ Code
Die
Über Fragen, Bug-Reports, Vorschläge oder jegliches andere Feedback würde ich mich freuen.
fgets
und strtok
sind oft ausreichend um solche Daten zu verarbeiten. Aber darum soll es hier nicht gehen.Es gibt bereits eine ganze Reihe von CSV Parsern. Nicht ganz so viele sind in C geschrieben, vermutlich weil objektorientierte Sprachen bessere Möglichkeiten bieten. Im Download ist mein Versuch für einen Parser, der CSV Daten in eine Struktur in den virtuellen Speicher deserialisiert. Das bedeutet einerseits, dass man jederzeit in der Lage ist auf jedes beliebige Datenfeld zuzugreifen. Andererseits bedeutet es aber auch, dass es sicher bessere, speicherschonendere Libraries gibt, wenn man wirklich große Datenmengen verarbeiten will.
Das Interface habe ich versucht so einfach wie möglich zu halten. Die dahinterliegende Implementierung ist allerdings ein Type-Punning Feuerwerk mit gleichzeitigem Versuch die längste jemals gesehene Parameterliste zu schreiben. (Selbstironisch für "ich hab schon verständlicheren Code geschrieben als diesen". C kennt leider keine C++ Templates um Code zu deduplizieren.)
Das Verhalten des Parsers lehnt sich stark an die im RFC 4180 beschriebenen Empfehlungen an.
Darüber hinaus gibt es folgende Features, die auch von einigen anderen Parsern unterstützt werden:
- Definition des Separators
- mögliche Definition eines Zeichens, das einen Kommentar einleitet
- mögliches Überspringen von Leerzeilen
- Zugriff auf Datenfelder mit Hilfe des Spaltennamens
Unterschiede zu anderen Parsern:
- Unterstützung von UTF-16 Dateinamen für Windows
- Unterstützung von
char
- und wchar_t
-basierten Daten und automatische Erkennung dieser- Unterstützung eines möglichen Byte Order Marks am Beginn der zu verarbeiteten Daten
Nicht implementiert:
- Trim Funktionalität für Leerzeichen/Tabs am Anfang und Ende von Datenfeldern (da diese einen erheblichen Zuwachs an Komplexität bedeuten würde)
CSV transportiert oft numerische Werte. Das Textformat dieser numerischen Werte ist dabei nicht standardisiert und oft von lokalen Einstellungen abhängig. Zusätzlich zum reinen Parsen der CSV Daten ist die Möglichkeit implementiert, numerische Datenfelder für die
strto...
bzw. wcsto...
Funktionen neu zu formatieren und die entsprechende Konvertierung zu numerischen Typen vorzunehmen.Vergleich zum RFC 4180:
RFC 4180 | ccsv library implementation |
---|---|
[...] | |
2. Definition of the CSV Format | |
While there are various specifications and implementations for the CSV format (for ex. [4], [5], [6] and [7]), there is no formal specification in existence, which allows for a wide variety of interpretations of CSV files. This section documents the format that seems to be followed by most implementations: | |
1. Each record is located on a separate line, delimited by a line break (CRLF). For example: | CRLF is automatically accepted to separate records. |
aaa,bbb,ccc CRLF | |
zzz,yyy,xxx CRLF | |
2. The last record in the file may or may not have an ending line break. For example: | The implementation accepts if the last record has no line break. |
aaa,bbb,ccc CRLF | |
zzz,yyy,xxx | |
3. There maybe an optional header line appearing as the first line of the file with the same format as normal record lines. This header will contain names corresponding to the fields in the file and should contain the same number of fields as the records in the rest of the file (the presence or absence of the header line should be indicated via the optional "header" parameter of this MIME type). For example: | The implementation is not able to decide whether or not a header line exists. There is no optional "header" parameter because the data used is not assumed to get received along with optional parameters.
However, a function is provided which treats the first line as header line and allows access of fields using the column-header name. |
field_name,field_name,field_name CRLF | |
aaa,bbb,ccc CRLF | |
zzz,yyy,xxx CRLF | |
4. Within the header and each record, there may be one or more fields, separated by commas. Each line should contain the same number of fields throughout the file. Spaces are considered part of a field and should not be ignored. The last field in the record must not be followed by a comma. For example: | The comma can be specified as separator of fields.
The implementation allows different numbers of fields in lines. Both the minimum and maximum found number of fields in the file are automatically determined.
Spaces are considered part of a field.
If the last field in a record is followed by a separator, it is assumed that it specifies an additional empty field. |
aaa,bbb,ccc | |
5. Each field may or may not be enclosed in double quotes (however some programs, such as Microsoft Excel, do not use double quotes at all). If fields are not enclosed with double quotes, then double quotes may not appear inside the fields. For example: | Surrounding quotes are accepted and removed during the parsing process. Double quotes may appear in fields (extension of this implementation), and are treated as surrounding quotes of a substring. |
"aaa","bbb","ccc" CRLF | |
zzz,yyy,xxx | |
6. Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes. For example: | Line breaks and separators in a quoted field are treated to belong to the field value. Quotes in a quoted field have to appear escaped as described in section 2.7. |
"aaa","b CRLF | |
bb","ccc" CRLF | |
zzz,yyy,xxx | |
7. If double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote. For example: | Two consecutive quotes in a quoted field are treated as an escaped quote, and are collapsed to one quote during the parsing process. |
"aaa","b""bb","ccc" | |
[...] | |
3. MIME Type Registration of text/csv | The MIME type is not a subject of the ccsv implementation. However, this section describes some further considerations. |
[...] | |
Optional parameters: charset, header | The data used is not assumed to get received along with optional parameters. |
Common usage of CSV is US-ASCII, but other character sets defined by IANA for the "text" tree may be used in conjunction with the "charset" parameter. | The implementation does not try to determine whether or not the CSV data is US-ASCII or any other charset. However, some charsets may require wide character types. The implementation determines automatically if the received text is based on "char", or on "wchar_t".
The implementation expects to find Byte Order Marks for UTF-8, UTF-16 LE, or UTF-32 LE. Those are skipped. |
The "header" parameter indicates the presence or absence of the header line. Valid values are "present" or "absent". Implementors choosing not to use this parameter must make their own decisions as to whether the header line is present or absent. | Refer to 2.3. |
Encoding considerations: | |
As per section 4.1.1. of RFC 2046 [3], this media type uses CRLF to denote line breaks. However, implementors should be aware that some implementations may use other values. | The implementation accepts CRLF and LF to denote line breaks. |
[...] | |
Interoperability considerations: | |
Due to lack of a single specification, there are considerable differences among implementations. Implementors should "be conservative in what you do, be liberal in what you accept from others" (RFC 793 [8]) when processing CSV files. An attempt at a common definition can be found in Section 2. | The implementation provides parameters to customize the parsing of the received data:
- As the comma is the decimal separator in some local environments, the semicolon is often used as field separator in this case. To make the implementation even more flexible, almost any character can be chosen as field separator.
- In some cases comments may be supported in CSV data. Those are introduced with a certain character at the beginning of a line. The implementation supports the definition of such a character in order to ignore comment lines while parsing the data.
- The implementation supports ignoring empty lines.
- Even if CSV is usually meant to represent tabular data, the implementation supports records with different numbers of fields. |
[...] |
Interface:
Strukturen:
csv_raw_t
Rohdaten als gelesene Bytes einer Datei oder Zeichen eines Strings.
csv_param_t
Parameter und Optionen zur Steuerung des Parsing Prozesses.
csv_field_t
Repräsentiert ein CSV Datenfeld
csv_record_t
Repräsentiert einen CSV Datensatz
csv_col_head_t
Repräsentiert einen Spaltenname in der CSV Kopfzeile
csv_t
Repräsentiert die Liste von CSV Datensätzen, sowie weitere Metadaten
Funktionen:
csv_raw_t *read_csv_raw(const char *fileName)
Lesen von CSV Rohdaten aus einer Datei.
csv_raw_t *read_csv_raw_w(const wchar_t *fileName)
Lesen von CSV Rohdaten aus einer Datei. (Unterstützung für nicht-ASCII Dateinamen unter Windows)
csv_raw_t *make_csv_raw(const void *bytes, size_t byteCount)
Aufbereitung von CSV Rohdaten aus einem String.
csv_t *parse_csv(csv_raw_t *pRaw, const csv_param_t *pParam)
Deserialisierung von CSV Daten in ein `csv_t` Objekt.
csv_field_t *get_by_index(const csv_t *pCsv, size_t recordIdx, size_t fieldIdx)
Zugriff auf ein Datenfeld über die Indizes des Datensatzes und des Felds.
csv_field_t *get_by_name(const csv_t *pCsv, size_t recordIdx, const void *colName)
Zugriff auf ein Datenfeld über den Index des Datensatzes und den Spaltenname.
csv_field_t *repl_num_separators(csv_field_t *pNumField, int decSep, int thousandsSep, bool isNarrow)
Änderung von Dezimaltrennzeichen und Tausendertrennzeichen eines Datenfelds um mit den
strto...
bzw. wcsto...
C Funktionen konvertierbar zu sein.void delete_csv(const csv_t *pCsv)
Freigabe von reserviertem Speicher.
Macro:
field_to_num(_type, _pNumField, _decSep, _thousandsSep, _isNarrow)
Ruft
repl_num_separators
auf um die numerischen Trennzeichen zu ersetzen, konvertiert anschließend in den durch _type
angegebenen numerischen Typ und gibt den konvertierten Wert zurück.Globales Objekt (nur C++):
constexpr inline auto csv_deleter
Lambda Objekt, das den Deleter für Smart Pointers (z.B.
std::unique_ptr<const csv_t, csv_deleter_t>
) repräsentiert.Typ (nur C++):
csv_deleter_t
Typ des
csv_deleter
Objekts.Detaillierte Beschreibungen finden sich im
ccsv.h
Header. Kommentare sind so aufgebaut, dass "Doxygen" aus ihnen eine Dokumentation aufbereiten kann.Beispiele:
C Code
C:
// Lesen der Rohdaten aus einer Datei.
csv_raw_t *const pRaw = read_csv_raw("test.csv");
// Definition der Parameter des Parsers.
const csv_param_t params = {
.separator = ',', // Komma als Feldseparator
.commentPrefix = '#', // erstes Zeichen einer Zeile, das einen Kommentar einleitet
.skipEmpty = true, // Leerzeilen überspringen
.fallbackNarrow = true // (*) bei nicht eindeutigem Zeichentyp, als char-basierten Inhalt parsen (nur Windows)
};
// Daten parsen.
const csv_t *const pCsv = parse_csv(pRaw, ¶ms);
if (!pCsv)
abort();
// Zugriff auf ein Datenfeld, mit jeweils nullbasierten Indizes Y (Zeile) und X (Spalte).
puts(get_by_index(pCsv, Y, X)->txt);
// Zugriff auf ein Datenfeld, mit nullbasiertem Index Y (Zeile) und Spaltenname.
puts(get_by_name(pCsv, Y, "Beispielname")->txt);
// Konvertierung eines Datenfelds zu einem numerischen Typ
// Beispiel: Wenn der der Ausgangswert des Felds `1.234,5` ist, dann wird
// 1.) der Wert zu `1234.5` aktualisiert, um mit den `strto...` Funktionen konvertiert werden zu können, sowie
// 2.) basierend auf dem übergebenen Typ die passende `strto...` Funktion aufgerufen, um den Wert zu konvertieren.
printf("%f\n", field_to_num(double, // numerischer Typ in den der String konvertiert werden soll
get_by_index(pCsv, Y, X), // Pointer auf das Feld mit dem zu konvertierenden String
',', // derzeitiges Dezimaltrennzeichen in den CSV Daten
'.', // derzeitiges Tausendertrennzeichen in den CSV Daten
pCsv->isNarrow)); // gibt den Zeichentyp des zu konvertierenden Strings an
// Speicher freigeben.
delete_csv(pCsv);
(*) Wann ist es nützlich dem Parser einen Hinweis auf den erwarteten Zeichentyp zu geben?
Dies ist nur für Windows relevant. Sehr kleine Datenmengen können mehrdeutig sein. Das Beispiel unten zeigt das Verhalten mit nur 2 Bytes Datenmenge. Wenn diese das Ohm Zeichen repräsentieren sollen, dann muss `fallbackNarrow` mit `false` angegeben werden. Üblicherweise besteht CSV aber aus ausreichend Daten, um Unklarheiten zu vermeiden. Komma-Trennzeichen oder Zeilenumbrüche würden bspw. genügen um den Zeichentyp eindeutig zu machen.
C++ Code
C++:
// Lesen der Rohdaten aus einer Datei.
auto *const pRaw{read_csv_raw("test.csv")};
// Definition der Parameter des Parsers.
constexpr csv_param_t params{';', '#', true, true};
// Daten parsen.
// Das `unique_ptr` Objekt nimmt den von `parse_csv()` zurückgegebenen Pointer in Besitz.
// Sowohl `csv_deleter_t` als auch `csv_deleter` sind im `ccsv.h` Header definiert wenn er in C++ Code eingebunden wird.
// `csv_deleter` ruft `delete_csv()` auf um das nötige "Deep Delete" der Struktur auszuführen.
// Achtung: Der Standard-Deleter `default_delete` ist hier definitiv nicht anwendbar!
std::unique_ptr<const csv_t, csv_deleter_t> upCsv{parse_csv(pRaw, ¶ms), csv_deleter};
if (!upCsv)
throw std::runtime_error{"parsing failed"};
// Zugriff auf ein Datenfeld, mit jeweils nullbasierten Indizes Y (Zeile) und X (Spalte).
std::cout << get_by_index(upCsv.get(), Y, X)->txt << '\n';
// Zugriff auf ein Datenfeld, mit nullbasiertem Index Y (Zeile) und Spaltenname.
std::cout << get_by_name(upCsv.get(), Y, "Beispielname")->txt << '\n';
// Konvertierung eines Datenfelds zu einem numerischen Typ
std::cout << field_to_num(double, get_by_index(upCsv.get(), Y, X), ',', '.', upCsv->isNarrow) << std::endl;
Die
main.c
enthält Testcode zu den im Download mitgelieferten CSV Dateien und zeigt weitere Beispiele wie mit den Funktionen zu arbeiten ist.Über Fragen, Bug-Reports, Vorschläge oder jegliches andere Feedback würde ich mich freuen.