288 votes

Comment lire et analyser des fichiers CSV en C++ ?

Je dois charger et utiliser les données d'un fichier CSV en C++. À ce stade, il peut s'agir d'un analyseur de données délimitées par des virgules (c'est-à-dire qu'il ne faut pas se soucier de l'échappement des nouvelles lignes et des virgules). Le besoin principal est un analyseur ligne par ligne qui retournera un vecteur pour la ligne suivante à chaque fois que la méthode est appelée.

J'ai trouvé cet article qui semble assez prometteur : http://www.boost.org/doc/libs/1_35_0/libs/spirit/example/fundamental/list_parser.cpp

Je n'ai jamais utilisé Boost's Spirit, mais je suis prêt à l'essayer. Mais seulement s'il n'y a pas une solution plus directe que je néglige.

11 votes

J'ai regardé boost::spirit pour l'analyse syntaxique. Il est plus destiné à l'analyse grammaticale qu'à l'analyse d'un simple format de fichier. Quelqu'un de mon équipe a essayé de l'utiliser pour analyser le XML et c'était une douleur à déboguer. Ne vous approchez pas de boost::spirit si possible.

51 votes

Désolé Chrish, mais c'est un mauvais conseil. Spirit n'est pas toujours une solution appropriée mais je l'ai utilisé - et continue de l'utiliser - avec succès dans un certain nombre de projets. Comparé à des outils similaires (Antlr, Lex/yacc etc.) il a des avantages significatifs. Maintenant, pour analyser un CSV, c'est probablement trop...

4 votes

@MattyT IMHO spirit est assez difficile à utiliser pour une bibliothèque de combinateurs d'analyseurs. Ayant eu une certaine expérience (très agréable) avec Haskells (atto)parsec bibliothèques Je m'attendais à ce que (l'esprit) fonctionne aussi bien, mais j'ai abandonné après m'être battu avec des erreurs de compilation de 600 lignes.

328voto

Loki Astari Points 116129

Si vous ne vous souciez pas de l'échappement des virgules et des nouvelles lignes,
ET vous ne pouvez pas intégrer la virgule et la nouvelle ligne dans les guillemets (Si vous ne pouvez pas les échapper alors...)
alors ce n'est qu'environ trois lignes de code (OK 14 ->Mais ce n'est que 15 pour lire le fichier entier).

std::vector<std::string> getNextLineAndSplitIntoTokens(std::istream& str)
{
    std::vector<std::string>   result;
    std::string                line;
    std::getline(str,line);

    std::stringstream          lineStream(line);
    std::string                cell;

    while(std::getline(lineStream,cell, ','))
    {
        result.push_back(cell);
    }
    // This checks for a trailing comma with no data after it.
    if (!lineStream && cell.empty())
    {
        // If there was a trailing comma then add an empty element.
        result.push_back("");
    }
    return result;
}

Je créerais simplement une classe représentant une ligne.
Ensuite, le flux dans cet objet :

#include <iterator>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>

class CSVRow
{
    public:
        std::string_view operator[](std::size_t index) const
        {
            return std::string_view(&m_line[m_data[index] + 1], m_data[index + 1] -  (m_data[index] + 1));
        }
        std::size_t size() const
        {
            return m_data.size() - 1;
        }
        void readNextRow(std::istream& str)
        {
            std::getline(str, m_line);

            m_data.clear();
            m_data.emplace_back(-1);
            std::string::size_type pos = 0;
            while((pos = m_line.find(',', pos)) != std::string::npos)
            {
                m_data.emplace_back(pos);
                ++pos;
            }
            // This checks for a trailing comma with no data after it.
            pos   = m_line.size();
            m_data.emplace_back(pos);
        }
    private:
        std::string         m_line;
        std::vector<int>    m_data;
};

std::istream& operator>>(std::istream& str, CSVRow& data)
{
    data.readNextRow(str);
    return str;
}   
int main()
{
    std::ifstream       file("plop.csv");

    CSVRow              row;
    while(file >> row)
    {
        std::cout << "4th Element(" << row[3] << ")\n";
    }
}

Mais avec un peu de travail, nous pourrions techniquement créer un itérateur :

class CSVIterator
{   
    public:
        typedef std::input_iterator_tag     iterator_category;
        typedef CSVRow                      value_type;
        typedef std::size_t                 difference_type;
        typedef CSVRow*                     pointer;
        typedef CSVRow&                     reference;

        CSVIterator(std::istream& str)  :m_str(str.good()?&str:NULL) { ++(*this); }
        CSVIterator()                   :m_str(NULL) {}

        // Pre Increment
        CSVIterator& operator++()               {if (m_str) { if (!((*m_str) >> m_row)){m_str = NULL;}}return *this;}
        // Post increment
        CSVIterator operator++(int)             {CSVIterator    tmp(*this);++(*this);return tmp;}
        CSVRow const& operator*()   const       {return m_row;}
        CSVRow const* operator->()  const       {return &m_row;}

        bool operator==(CSVIterator const& rhs) {return ((this == &rhs) || ((this->m_str == NULL) && (rhs.m_str == NULL)));}
        bool operator!=(CSVIterator const& rhs) {return !((*this) == rhs);}
    private:
        std::istream*       m_str;
        CSVRow              m_row;
};

int main()
{
    std::ifstream       file("plop.csv");

    for(CSVIterator loop(file); loop != CSVIterator(); ++loop)
    {
        std::cout << "4th Element(" << (*loop)[3] << ")\n";
    }
}

Maintenant que nous sommes en 2020, ajoutons un objet CSVRange :

class CSVRange
{
    std::istream&   stream;
    public:
        CSVRange(std::istream& str)
            : stream(str)
        {}
        CSVIterator begin() const {return CSVIterator{stream};}
        CSVIterator end()   const {return CSVIterator{};}
};

int main()
{
    std::ifstream       file("plop.csv");

    for(auto& row: CSVRange(file))
    {
        std::cout << "4th Element(" << row[3] << ")\n";
    }
}

0 votes

C'est exactement ce que je voulais ! Maintenant, un peu de crédit supplémentaire comment puis-je en faire une classe avec un constructeur et deux méthodes : firstLine() et nextLine(). std::istream n'a pas de constructeur par défaut alors qu'est-ce que je dois utiliser à la place ? Merci pour votre aide !

23 votes

premier() suivant(). Qu'est-ce que c'est que ce Java ! Je plaisante.

1 votes

ou vous pouvez utiliser des bibliothèques boost pour analyser le csv ... voir ci-dessous

68voto

sastanin Points 16061

Ma version n'utilise rien d'autre que la bibliothèque standard C++11. Elle se débrouille bien avec les devis CSV d'Excel :

spam eggs,"foo,bar","""fizz buzz"""
1.23,4.567,-8.00E+09

Le code est écrit comme une machine à états finis et consomme un caractère à la fois. Je pense que c'est plus facile de raisonner dessus.

#include <istream>
#include <string>
#include <vector>

enum class CSVState {
    UnquotedField,
    QuotedField,
    QuotedQuote
};

std::vector<std::string> readCSVRow(const std::string &row) {
    CSVState state = CSVState::UnquotedField;
    std::vector<std::string> fields {""};
    size_t i = 0; // index of the current field
    for (char c : row) {
        switch (state) {
            case CSVState::UnquotedField:
                switch (c) {
                    case ',': // end of field
                              fields.push_back(""); i++;
                              break;
                    case '"': state = CSVState::QuotedField;
                              break;
                    default:  fields[i].push_back(c);
                              break; }
                break;
            case CSVState::QuotedField:
                switch (c) {
                    case '"': state = CSVState::QuotedQuote;
                              break;
                    default:  fields[i].push_back(c);
                              break; }
                break;
            case CSVState::QuotedQuote:
                switch (c) {
                    case ',': // , after closing quote
                              fields.push_back(""); i++;
                              state = CSVState::UnquotedField;
                              break;
                    case '"': // "" -> "
                              fields[i].push_back('"');
                              state = CSVState::QuotedField;
                              break;
                    default:  // end of quote
                              state = CSVState::UnquotedField;
                              break; }
                break;
        }
    }
    return fields;
}

/// Read CSV file, Excel dialect. Accept "quoted fields ""with quotes"""
std::vector<std::vector<std::string>> readCSV(std::istream &in) {
    std::vector<std::vector<std::string>> table;
    std::string row;
    while (!in.eof()) {
        std::getline(in, row);
        if (in.bad() || in.fail()) {
            break;
        }
        auto fields = readCSVRow(row);
        table.push_back(fields);
    }
    return table;
}

10 votes

Merci, je pense que c'est la réponse la plus complète, dommage qu'elle soit enterrée ici.

0 votes

Ce vecteur imbriqué de chaînes de caractères n'est pas compatible avec les processeurs modernes. Cela annule leur capacité de mise en cache.

0 votes

En plus tu as toutes ces déclarations d'échange

48voto

dtw Points 967

Solution utilisant Boost Tokenizer :

std::vector<std::string> vec;
using namespace boost;
tokenizer<escaped_list_separator<char> > tk(
   line, escaped_list_separator<char>('\\', ',', '\"'));
for (tokenizer<escaped_list_separator<char> >::iterator i(tk.begin());
   i!=tk.end();++i) 
{
   vec.push_back(*i);
}

11 votes

Le tokenizer de boost ne prend pas entièrement en charge la norme CSV complète, mais il existe quelques solutions de contournement rapides. Voir stackoverflow.com/questions/1120140/csv-parser-in-c/

3 votes

Vous devez avoir toute la bibliothèque boost sur votre machine, ou vous pouvez juste utiliser un sous-ensemble de leur code pour le faire ? 256mb semble être beaucoup pour le parsing CSV

6 votes

@NPike : Vous pouvez utiliser le bcp qui est fourni avec Boost pour extraire uniquement les en-têtes dont vous avez réellement besoin.

33voto

Le site Bibliothèque C++ String Toolkit (StrTk) dispose d'une classe de grille à jeton qui vous permet de charger des données soit à partir de fichiers texte, chaînes de caractères ou tampons de caractères et de les analyser/traiter en ligne et en colonne.

Vous pouvez spécifier les délimiteurs de ligne et les délimiteurs de colonne ou utiliser les valeurs par défaut.

void foo()
{
   std::string data = "1,2,3,4,5\n"
                      "0,2,4,6,8\n"
                      "1,3,5,7,9\n";

   strtk::token_grid grid(data,data.size(),",");

   for(std::size_t i = 0; i < grid.row_count(); ++i)
   {
      strtk::token_grid::row_type r = grid.row(i);
      for(std::size_t j = 0; j < r.size(); ++j)
      {
         std::cout << r.get<int>(j) << "\t";
      }
      std::cout << std::endl;
   }
   std::cout << std::endl;
}

Vous trouverez d'autres exemples Ici

1 votes

Bien que strtk supporte les champs à double guillemet et même de supprimer les guillemets qui les entourent (par l'intermédiaire de options.trim_dquotes = true ), il ne prend pas en charge la suppression des guillemets doubles (par exemple, le champ "She said ""oh no"", and left." comme la chaîne c "She said \"oh no\", and left." ). Vous devrez le faire vous-même.

1 votes

Lorsque vous utilisez strtk vous devrez également gérer manuellement les champs à double guillemet qui contiennent des caractères de nouvelle ligne.

29voto

stefanB Points 27796

Vous pouvez utiliser Boost Tokenizer avec escaped_list_separator.

Séparateur de liste échappé analyse un sur-ensemble du csv. Boost::tokenizer

Ceci n'utilise que les fichiers d'en-tête du tokenizer de Boost, aucune liaison avec les bibliothèques de Boost n'est nécessaire.

Voici un exemple, (voir Analyser un fichier CSV avec Boost Tokenizer en C++. pour plus de détails ou Boost::tokenizer ) :

#include <iostream>     // cout, endl
#include <fstream>      // fstream
#include <vector>
#include <string>
#include <algorithm>    // copy
#include <iterator>     // ostream_operator
#include <boost/tokenizer.hpp>

int main()
{
    using namespace std;
    using namespace boost;
    string data("data.csv");

    ifstream in(data.c_str());
    if (!in.is_open()) return 1;

    typedef tokenizer< escaped_list_separator<char> > Tokenizer;
    vector< string > vec;
    string line;

    while (getline(in,line))
    {
        Tokenizer tok(line);
        vec.assign(tok.begin(),tok.end());

        // vector now contains strings from one row, output to cout here
        copy(vec.begin(), vec.end(), ostream_iterator<string>(cout, "|"));

        cout << "\n----------------------" << endl;
    }
}

0 votes

Et si vous voulez être capable d'analyser les nouvelles lignes incorporées mybyteofcode.blogspot.com/2010/11/ .

0 votes

Bien que cette technique fonctionne, je l'ai trouvée très peu performante. L'analyse d'un fichier CSV de 90000 lignes avec dix champs par ligne prend environ 8 secondes sur mon Xeon 2 GHz. Le module csv de la bibliothèque standard de Python analyse le même fichier en 0,3 seconde environ.

0 votes

@Rob c'est intéressant - qu'est-ce que le Python csv fait différemment ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X