Dateiinhalt - Komma-Separator - Wie elegant auslesen?
-
Hallo
- Ich habe eine Datei, in der Zeichenketten und Ganzzahlen durch Kommas getrennt stehen.
- Die Zeichenketten an sich können Leerzeichen enthalten.
- Drei aufeinanderfolgende Merkmale repräsentieren die Elemente einer Instanz.Beispieldatei:
Max Mustermann.,22,privat versichert,Hans,34,gesetzlich versichert,Erik,29,privat versichert
Dazugehörige Struktur:
struct Person { string Name; unsigned Alter; string Krankenversicherung; };
Ich will die Datei nun möglichst elegant auslesen. Ich will auf
getline
undstringstream
verzichten. Am liebsten wäre mir, ich könnte einfach das hier machen:for(Person p; file>>p; vector.push_back(p));
Ich denke, ich müsste den Stream irgendwie mittels einer Fassette manipulieren, sodass ich dann elegant lesen kann. Etwas anderes fällt mir im Moment auch gar nicht ein. Wäre das ein guter Weg? Oder habt ihr sonstige Ideen?
Danke im Voraus.
-
Was hältst du davon?
struct Person { std::string Name; unsigned Alter; std::string Krankenversicherung; friend std::istream& operator>>(std::istream& is, Person& p) { return is >> p.Name >> p.Alter >> p.Krankenversicherung; } }; struct csv_ctype : std::ctype<char> { mask table[table_size]; public: csv_ctype(size_t refs = 0) : std::ctype<char>(table, false, refs) { std::fill(table, table+table_size, (mask)alpha); table[','] = (mask)space; } }; int main() { std::istringstream is("Max Mustermann.,22,privat versichert,Hans,34,gesetzlich versichert,Erik,29,privat versichert"); is.imbue(std::locale(is.getloc(), new csv_ctype)); std::vector<Person> v(std::istream_iterator<Person>(is), (std::istream_iterator<Person>())); for (auto& p : v) std::cout << p.Name << ' ' << p.Alter << ' ' << p.Krankenversicherung << '\n'; }
-
Hallo maxermann,
genau so habe ich mir das vorgestellt :). Da ich allerdings erst ein einziges Mal mit einer Fassette zu tun hatte, ist es für mich etwas schwierig nachzuvollziehen, was da passiert. Ich versuche mal, drei Zeilen deines Quellcodes zu erklären. Sag mir dann einfach, ob ich es richtig verstanden habe:
std::fill(table, table+table_size, (mask)alpha); // Alle Zeichen sollen als ein ein alphabetisches Zeichen wahrgenommen werden. table[','] = (mask)space; // Das Komma-Zeichen soll als ein Leerzeichen angesehen werden. is.imbue(std::locale(is.getloc(), new csv_ctype)); // Der Stream wird nach den obig definierten Regeln manipuliert.
Du sagst also, ein Leerzeichen ist kein Leerzeichen mehr, sondern ein Komma ist von nun an ein Leerzeichen. Das ist natürlich clever. Ich hoffe, ich habs richtig verstanden?
-
out schrieb:
Ich will die Datei nun möglichst elegant auslesen. Ich will auf
getline
undstringstream
verzichten. Am liebsten wäre mir, ich könnte einfach das hier machen:for(Person p; file>>p; vector.push_back(p));
Auf getline musst Du nicht verzichten, wenn Du Texte einliest, die durch ein Trennzeichen (hier ',') getrennt sind. Dafür ist es gemacht. Mit Komma getrennte Datensätze sind i.A. csv-Dateien. Kommt also häufiger vor. Suche hier im Forum nach csv .. dann findest Du auch Code wie diesen:
#include <string> #include <sstream> #include <iostream> #include <vector> #include <iterator> // -- Helferlein zum Überlesen eines Zeichens 'C' template< char C > std::istream& Char( std::istream& in ) { char c; if( in >> c && c != C ) in.setstate( std::ios_base::failbit ); return in; } struct Person { std::string Name; unsigned Alter; std::string Krankenversicherung; friend std::istream& operator>>( std::istream& in, Person& p ); friend std::ostream& operator<<( std::ostream& out, const Person& p ); }; std::istream& operator>>( std::istream& in, Person& p ) { // Format: Name , Alter , Krankenversicherung , return getline( getline( in >> std::ws, p.Name, ',' ) >> p.Alter >> Char<','>, p.Krankenversicherung, ',' ); } std::ostream& operator<<( std::ostream& out, const Person& p ) { return out << p.Name << "," << p.Alter << "," << p.Krankenversicherung << ","; } int main() { using namespace std; istringstream file("Max Mustermann.,22,privat versichert,Hans,34,gesetzlich versichert,Erik,29,privat versichert"); vector< Person > personen; for( Person p; file >> p; personen.push_back(p) ) ; // alternativ: vector< Person > personen( (istream_iterator<Person>(file)), istream_iterator< Person >() ); if( file.eof() ) // wenn bis EOF gelesen, dann war bis dahin kein Fehler { cout << personen.size() << "Eintraege gelesen " << endl; copy( personen.begin(), personen.end(), ostream_iterator< Person >( cout, "\n" ) ); cout << endl; } }
out schrieb:
Ich denke, ich müsste den Stream irgendwie mittels einer Fassette manipulieren, sodass ich dann elegant lesen kann.
Hat maxermann schon gezeigt, geht auch. Das Problem dabei wären u.U. leere Einträge - also zwei ',' in Folge.
wg. Char<> sieh auch hier.
:xmas2: Werner
-
Alles klar, danke euch beiden
-
Werner Salomon schrieb:
out schrieb:
Ich denke, ich müsste den Stream irgendwie mittels einer Fassette manipulieren, sodass ich dann elegant lesen kann.
Hat maxermann schon gezeigt, geht auch.
.. na ja - ein kleiner Nachtrag bevor es jemand so verwendet. maxermann hat alle Zeichen mit der Maske 'alpha' versehen, was sie als druckbare Buchstaben kennzeichnet. Das geht schon dann nicht gut, wenn jemand z.B. auf Digits abfragt.
Deshalb würde ich folgende Implementierung von csv_ctype vorschlagen:
struct csv_ctype : std::ctype< char > { mask table[table_size]; public: csv_ctype( std::size_t refs = 0 ) : std::ctype< char >( table, false, refs ) { std::copy( classic_table(), classic_table()+table_size, table ); // zunächst alle Masken übernehmen table[','] = table[' ']; // ',' wird zum Space und damit zum Trennzeichen table[' '] = table['.']; // Space wird behandelt wie ein '.' -> hier wird nicht mehr getrennt. } };
Der Code für out funktioniert anschließend genauso.
:xmas2: Werner
-
Werner Salomon schrieb:
Space wird behandelt wie ein '.' -> hier wird nicht mehr getrennt.
Das erinnert mich an die Fragen, die ich hatte, als ich den Code schrieb.
- Müsste man nicht noch u.a.
'\t'
und'\n'
als Space kennzeichnen? Wäre das folgende korrekt?
std::for_each(table, table+table_size, [](mask& m){m &= ~(mask)space;}); table[','] |= (mask)space;
- Ist
table[',']
streng genommen nicht falsch? char könnte doch signed sein. Wäre hier ein Cast nötig oder geschieht der implizit?
table[static_cast<unsigned char>(',')] |= (mask)space;
- Müsste man nicht noch u.a.
-
Werner Salomon schrieb:
.. na ja - ein kleiner Nachtrag bevor es jemand so verwendet. maxermann hat alle Zeichen mit der Maske 'alpha' versehen, was sie als druckbare Buchstaben kennzeichnet. Das geht schon dann nicht gut, wenn jemand z.B. auf Digits abfragt.
Stimmt, gut zu wissen.
-
maxermann schrieb:
Werner Salomon schrieb:
Space wird behandelt wie ein '.' -> hier wird nicht mehr getrennt.
Das erinnert mich an die Fragen, die ich hatte, als ich den Code schrieb.
- Müsste man nicht noch u.a.
'\t'
und'\n'
als Space kennzeichnen? Wäre das folgende korrekt?
std::for_each(table, table+table_size, [](mask& m){m &= ~(mask)space;}); table[','] |= (mask)space;
Müsste Werner beantworten.
maxermann schrieb:
- Ist
table[',']
streng genommen nicht falsch? char könnte doch signed sein. Wäre hier ein Cast nötig oder geschieht der implizit?
table[static_cast<unsigned char>(',')] |= (mask)space;
Aber ein Zeichen wird doch immer durch eine nicht-negative Ganzzahl repräsentiert. Von dem her kann da doch nichts schief gehen.
- Müsste man nicht noch u.a.
-
out schrieb:
maxermann schrieb:
Werner Salomon schrieb:
Space wird behandelt wie ein '.' -> hier wird nicht mehr getrennt.
Das erinnert mich an die Fragen, die ich hatte, als ich den Code schrieb.
- Müsste man nicht noch u.a.
'\t'
und'\n'
als Space kennzeichnen? Wäre das folgende korrekt?
std::for_each(table, table+table_size, [](mask& m){m &= ~(mask)space;}); table[','] |= (mask)space;
Müsste Werner beantworten.
In meinem Vorschlag wird zunächst die komplette bereits bestehende Tabelle kopiert.
Werner Salomon schrieb:
std::copy( classic_table(), classic_table()+table_size, table ); // zunächst alle Masken übernehmen
Wenn man jetzt nichts weiter macht, so ist das Verhalten von
csv_ctype
identisch mit dem vonstd::ctype
.
Erst danach werden die Masken von ',' und ' ' so angepasst, dass ',' als Space und Space als !Space markiert wird:Werner Salomon schrieb:
table[','] = table[' ']; // ',' wird zum Space und damit zum Trennzeichen table[' '] = table['.']; // Space wird behandelt wie ein '.' -> hier wird nicht mehr getrennt.
- Ist
table[',']
streng genommen nicht falsch? char könnte doch signed sein. Wäre hier ein Cast nötig oder geschieht der implizit?
table[static_cast<unsigned char>(',')] |= (mask)space;
Ja - der Cast ist im Allgemeinen notwendig. Im speziellen Fall sind die 'Indizes' bekannt (',' und ' ') und der Code von beiden liegt unter 0x7f - d.h. im Zweifel werden sie als positive Werte interpretiert. Und damit funktioniert das hier.
:xmas2: Werner
- Müsste man nicht noch u.a.
-
Wenn keine zwei ',' in Folge sind, funktioniert auch folgendes:
#include <iostream> #include <fstream> #include <string> #include <vector> struct Person { std::string Name; unsigned int Alter; std::string Krankenversicherung; }; std::ostream &operator<<(std::ostream &os, const Person &p) { return os << p.Name << ',' << p.Alter << ',' << p.Krankenversicherung; } std::istream &operator>>(std::istream &is, Person &p) { char komma; return std::getline(std::getline(is, p.Name, ',') >> p.Alter >> komma, p.Krankenversicherung, ','); } int main() { std::ifstream file("file.txt"); std::vector<Person> personen; for (Person p; file >> p; personen.push_back(p)); std::copy(personen.begin(), personen.end(), std::ostream_iterator<Person>(std::cout, "\n")); }