Как я могу читать и анализировать файлы CSV в C ++? - PullRequest
230 голосов
/ 13 июля 2009

Мне нужно загрузить и использовать данные файла CSV в C ++. На данный момент это может быть просто парсер, разделенный запятыми (т.е. не беспокойтесь о экранировании новых строк и запятых). Основная потребность - построчный анализатор, который будет возвращать вектор для следующей строки каждый раз, когда вызывается метод.

Я нашел эту статью, которая выглядит довольно многообещающе: http://www.boost.org/doc/libs/1_35_0/libs/spirit/example/fundamental/list_parser.cpp

Я никогда не использовал Boost's Spirit, но готов попробовать. Но только если нет более простого решения, я пропускаю.

Ответы [ 33 ]

265 голосов
/ 13 июля 2009

Если вам не нужны экранирование от запятой и новой строки,
И вы не можете вставлять запятую и новую строку в кавычки (Если вы не можете убежать, то ...)
тогда его всего около трех строк кода (ОК 14 -> Но всего 15, чтобы прочитать весь файл).

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;
}

Я бы просто создал класс, представляющий строку.
Затем поток в этот объект:

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

class CSVRow
{
    public:
        std::string const& operator[](std::size_t index) const
        {
            return m_data[index];
        }
        std::size_t size() const
        {
            return m_data.size();
        }
        void readNextRow(std::istream& str)
        {
            std::string         line;
            std::getline(str, line);

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

            m_data.clear();
            while(std::getline(lineStream, cell, ','))
            {
                m_data.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.
                m_data.push_back("");
            }
        }
    private:
        std::vector<std::string>    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";
    }
}

Но с небольшой работой мы могли бы технически создать итератор:

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";
    }
}
44 голосов
/ 14 июля 2009

Решение с использованием 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);
}
37 голосов
/ 20 мая 2015

Моя версия не использует ничего, кроме стандартной библиотеки C ++ 11. Хорошо справляется с цитатой Excel CSV:

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

Код написан как конечный автомат и использует один символ за раз. Я думаю, что легче рассуждать.

#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;
}
31 голосов
/ 25 сентября 2009

Библиотека C ++ String Toolkit (StrTk) имеет класс сетки токенов, который позволяет загружать данные из текстовых файлов, строк или буферов символов и анализировать / обрабатывать их в виде ряда столбцов.

Вы можете указать разделители строк и столбцов или просто использовать значения по умолчанию.

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;
}

Больше примеров можно найти Здесь

29 голосов
/ 24 февраля 2010

Вы можете использовать Boost Tokenizer с escaped_list_separator.

escaped_list_separator анализирует расширенный набор символов CSV. подталкивании :: токенизатор

При этом используются только заголовочные файлы Boost tokenizer, никаких ссылок на библиотеки повышения не требуется.

Вот пример (см. Синтаксический анализ файла CSV с токенайзером повышения в C ++ или 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;
    }
}
29 голосов
/ 19 ноября 2009

Не лишним будет использовать Дух для разбора CSV. Spirit хорошо подходит для задач микропарсинга. Например, для Spirit 2.1 это так же просто, как:

bool r = phrase_parse(first, last,

    //  Begin grammar
    (
        double_ % ','
    )
    ,
    //  End grammar

    space, v);

Вектор v заполняется значениями. В серии новых документов Spirit 2.1, выпущенных с Boost 1.41, есть серия уроков .

Обучение проходит от простого к сложному. Парсеры CSV представлены где-то посередине и затрагивают различные методы использования Spirit. Сгенерированный код такой же жесткий, как и рукописный код. Проверьте сгенерированный ассемблер!

17 голосов
/ 20 марта 2010

Если вы DO хотите правильно проанализировать CSV, это будет сделано ... относительно медленно, так как он работает по одному символу за раз.

 void ParseCSV(const string& csvSource, vector<vector<string> >& lines)
    {
       bool inQuote(false);
       bool newLine(false);
       string field;
       lines.clear();
       vector<string> line;

       string::const_iterator aChar = csvSource.begin();
       while (aChar != csvSource.end())
       {
          switch (*aChar)
          {
          case '"':
             newLine = false;
             inQuote = !inQuote;
             break;

          case ',':
             newLine = false;
             if (inQuote == true)
             {
                field += *aChar;
             }
             else
             {
                line.push_back(field);
                field.clear();
             }
             break;

          case '\n':
          case '\r':
             if (inQuote == true)
             {
                field += *aChar;
             }
             else
             {
                if (newLine == false)
                {
                   line.push_back(field);
                   lines.push_back(line);
                   field.clear();
                   line.clear();
                   newLine = true;
                }
             }
             break;

          default:
             newLine = false;
             field.push_back(*aChar);
             break;
          }

          aChar++;
       }

       if (field.size())
          line.push_back(field);

       if (line.size())
          lines.push_back(line);
    }
14 голосов
/ 20 октября 2009

При использовании Boost Tokenizer escaped_list_separator для файлов CSV следует помнить следующее:

  1. Требуется escape-символ (по умолчанию обратная косая черта - \)
  2. Требуется символ разделителя / разделителя (запятая по умолчанию -,)
  3. Требуется символ кавычки (цитата по умолчанию - ")

Формат CSV, указанный в вики, гласит, что поля данных могут содержать разделители в кавычках (поддерживается):

1997, Ford, E350, "Супер, роскошный грузовик"

Формат CSV, указанный в вики, гласит, что одинарные кавычки должны обрабатываться двойными кавычками (escaped_list_separator удаляет все символы кавычек):

1997, Ford, E350, "Супер", "роскошный", "грузовой автомобиль"

Формат CSV не указывает, что любые символы обратной косой черты должны быть удалены (escaped_list_separator удалит все escape-символы).

Возможный обходной путь для исправления поведения по умолчанию для надстройки escaped_list_separator:

  1. Сначала замените все символы обратной косой черты (\) на два символа обратной косой черты (\\), чтобы они не были удалены.
  2. Во-вторых, заменить все двойные кавычки ("") на один символ обратной косой черты и кавычку (\ ")

Этот обходной путь имеет побочный эффект: пустые поля данных, представленные в двойных кавычках, будут преобразованы в токен в одинарных кавычках. При переборе токенов необходимо проверить, является ли токен одинарной кавычкой, и обращаться с ним как с пустой строкой.

Не красиво, но работает, пока в кавычках нет перевода строки.

7 голосов
/ 18 июля 2012

Поскольку все вопросы CSV, кажется, перенаправляются сюда, я решил опубликовать свой ответ здесь. Этот ответ не имеет прямого отношения к вопросу автора. Я хотел иметь возможность читать в потоке, который, как известно, находится в формате CSV, а также типы каждого поля уже были известны. Конечно, метод ниже может использоваться для обработки каждого поля как строкового типа.

В качестве примера того, как я хотел иметь возможность использовать входной поток CSV, рассмотрим следующий вход (взят со страницы википедии на CSV ):

const char input[] =
"Year,Make,Model,Description,Price\n"
"1997,Ford,E350,\"ac, abs, moon\",3000.00\n"
"1999,Chevy,\"Venture \"\"Extended Edition\"\"\",\"\",4900.00\n"
"1999,Chevy,\"Venture \"\"Extended Edition, Very Large\"\"\",\"\",5000.00\n"
"1996,Jeep,Grand Cherokee,\"MUST SELL!\n\
air, moon roof, loaded\",4799.00\n"
;

Затем я хотел иметь возможность читать такие данные:

std::istringstream ss(input);
std::string title[5];
int year;
std::string make, model, desc;
float price;
csv_istream(ss)
    >> title[0] >> title[1] >> title[2] >> title[3] >> title[4];
while (csv_istream(ss)
       >> year >> make >> model >> desc >> price) {
    //...do something with the record...
}

Это решение, которое я выбрал.

struct csv_istream {
    std::istream &is_;
    csv_istream (std::istream &is) : is_(is) {}
    void scan_ws () const {
        while (is_.good()) {
            int c = is_.peek();
            if (c != ' ' && c != '\t') break;
            is_.get();
        }
    }
    void scan (std::string *s = 0) const {
        std::string ws;
        int c = is_.get();
        if (is_.good()) {
            do {
                if (c == ',' || c == '\n') break;
                if (s) {
                    ws += c;
                    if (c != ' ' && c != '\t') {
                        *s += ws;
                        ws.clear();
                    }
                }
                c = is_.get();
            } while (is_.good());
            if (is_.eof()) is_.clear();
        }
    }
    template <typename T, bool> struct set_value {
        void operator () (std::string in, T &v) const {
            std::istringstream(in) >> v;
        }
    };
    template <typename T> struct set_value<T, true> {
        template <bool SIGNED> void convert (std::string in, T &v) const {
            if (SIGNED) v = ::strtoll(in.c_str(), 0, 0);
            else v = ::strtoull(in.c_str(), 0, 0);
        }
        void operator () (std::string in, T &v) const {
            convert<is_signed_int<T>::val>(in, v);
        }
    };
    template <typename T> const csv_istream & operator >> (T &v) const {
        std::string tmp;
        scan(&tmp);
        set_value<T, is_int<T>::val>()(tmp, v);
        return *this;
    }
    const csv_istream & operator >> (std::string &v) const {
        v.clear();
        scan_ws();
        if (is_.peek() != '"') scan(&v);
        else {
            std::string tmp;
            is_.get();
            std::getline(is_, tmp, '"');
            while (is_.peek() == '"') {
                v += tmp;
                v += is_.get();
                std::getline(is_, tmp, '"');
            }
            v += tmp;
            scan();
        }
        return *this;
    }
    template <typename T>
    const csv_istream & operator >> (T &(*manip)(T &)) const {
        is_ >> manip;
        return *this;
    }
    operator bool () const { return !is_.fail(); }
};

Со следующими помощниками, которые могут быть упрощены новыми шаблонами интегральных черт в C ++ 11:

template <typename T> struct is_signed_int { enum { val = false }; };
template <> struct is_signed_int<short> { enum { val = true}; };
template <> struct is_signed_int<int> { enum { val = true}; };
template <> struct is_signed_int<long> { enum { val = true}; };
template <> struct is_signed_int<long long> { enum { val = true}; };

template <typename T> struct is_unsigned_int { enum { val = false }; };
template <> struct is_unsigned_int<unsigned short> { enum { val = true}; };
template <> struct is_unsigned_int<unsigned int> { enum { val = true}; };
template <> struct is_unsigned_int<unsigned long> { enum { val = true}; };
template <> struct is_unsigned_int<unsigned long long> { enum { val = true}; };

template <typename T> struct is_int {
    enum { val = (is_signed_int<T>::val || is_unsigned_int<T>::val) };
};
7 голосов
/ 13 июля 2009

Возможно, вы захотите взглянуть на мой проект FOSS CSVfix ( обновленная ссылка ), который представляет собой редактор потоков CSV, написанный на C ++. Синтаксический анализатор CSV не является призом, но выполняет свою работу, и весь пакет может делать то, что вам нужно, без написания кода.

См. alib / src / a_csv.cpp для синтаксического анализатора CSV и csvlib / src / csved_ioman.cpp (IOManager::ReadCSV) для примера использования.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...