J'ai écrit sur mon blog un billet décrivant l'analyse syntaxique monadique. Je voudrais écrire une suite pour comparer avec l'analyse syntaxique d'un document .csv dans un langage impératif (j'ai choisi le C++).
Il me faut donc un analyseur de .csv en C++. Je me débrouille pas mal mais je voudrais avoir des conseils et avis pour aboutir à un code idiomatique, esthétique et performant (dans cet ordre) pour que la comparaison soit le plus équitable possible (il ne s'agira pas au bout de déclarer un vainqueur, mais de montrer les différences d'approche et les éventuels points forts/points faibles).
Un petit rappel sur le format csv:il nous faut
- un séparateur de ligne
- un séparateur de colonne
- ignorer ces séparateurs lorsqu'ils apparaissent dans un champ placé entre guillemets
- permettre, au sein des champs entre guillemets, l'utilisation du caractère "guillemet", qui doit être doublé (ex: "name;"John\"\"the Snake\"\" Smith" => name / John "the Snake" Smith)
l'input est représenté sous forme de std::istream. Je vois un certain nombre de stratégies possibles. La première qui me vient à l'esprit est d'utiliser une forme modifiée de getline, puis d'appliquer une fonction splitIntoCells aux strings obtenues:
- la fonction getCsvLine est simple (mais dangereuse à élaborer: il faut s'appuyer sur std::getline et non sur istream::get car ils ont des sémantiques différentes: en particulier, quand elle rencontre eof, std::getline n'allume pas le flag failbit, tandis que istream::get le fait ):
- la fonction splitIntoCells serait:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 std::istream& getCsvLine(std::istream& is, std::string& res) { std::string tmp; // il faut un stockage temporaire car getline réinitialise la std::string passée en argument avant de la remplir for (;;) { std::getline(is, res); tmp += res; if ((std::count(tmp.begin(), tmp.end(), '"') % 2) == 0) break; } res = tmp; return is; }
Elle n'est pas très élégantes et plutôt "bas niveau" mais une formulation de plus haut niveau implique des allers-retours dans la std::string:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14 std::vector<std::string> splitIntoCells(const std::string& line) { std::vector<std::string> res; bool quoted = false; std::string::const_iterator a = line.begin(); for (std::string::const_iterator b = a; b < line.end(); ++b) { if (*b == '"') quoted = !quoted; if (*b == ';' && !quoted) { res.push_back(suppressExtraQuotes(std::string(a,b))); // il faut supprimer les guillemets qui font partie du format mais pas du contenu a = b+1; } } res.push_back(suppressExtraQuotes(std::string(a,line.end()))); return res; }
Je mets également la fonction spécial-guillemets:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 std::vector<std::string> splitIntoCellsB(const std::string& line) { std::vector<std::string> res; for (std::string::const_iterator b = line.begin();;) { std::string::const_iterator f = std::find(b, line.end(), ';'); while (std::count(b, f, '"') % 2 == 1) { // tant que les guillemets ne sont pas fermés f = std::find(f+1, line.end(), ';'); // on continue à chercher } res.push_back(suppressExtraQuotes(std::string(b,f))); if (f == line.end()) break; b = ++f; } return res; }
Il ne reste plus qu'à écrire une fonction qui prenne un fichier en argument:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 std::string suppressExtraQuotes(const std::string& s) { std::string tmp = s, res; if (*tmp.begin() == '"') tmp = std::string(tmp.begin()+1, tmp.end()-1); // on enlève les guillemets de début et de fin for (std::string::iterator it = tmp.begin(); it < tmp.end(); ++it) { res.push_back(*it); if (*it == '"') ++it; // si c'est un guillemet on saute le prochaine caractère (aussi un guillemet) } return res; }
Donc ma question: est-ce que cela vous paraît un code acceptable (hors bibliothèques spécialisées et autres regexp, évidemment)? Avez-vous des propositions d'amélioration?
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 std::vector<std::vector<std::string> > parseCSV(std::istream& is) { std::vector<std::vector<std::string> > res; std::string line; while (getCsvLine(is, line)) { res.push_back(splitIntoCells(line); } return res; }
Merci d'avance!
Partager