воскресенье, 31 октября 2010 г.

Парсер csv

Недавно мне понадобилось парсить файл формата csv. При этом каждый элемент соответствует некоторому полю структуры, например:
Содержимое файла csv:
some string,1,some string 2,100500
hello, 2,good bye,111111

Структура, которую необходимо заполнить:
struct some_struct
{
    std::string name1;
    int count1;
    std::string name2;
    int count2;
};
_Winnie C++ Colorizer
Используем для этих boost:
Нам понадобятся две библиотеки: tokenizer и lexical_cast



Итак, сначала определим нужные нам типы:

typedef boost::char_separator<char> sep_type;
typedef boost::tokenizer<sep_type> tok_type;

struct some_struct
{
    std::string line1;
    int count1;
    std::string line2;
    int count2;
};

typedef std::vector<some_struct> vec_type;
_Winnie C++ Colorizer
В первой строке мы определяем тип - сепаратор, который будет принимать решение о разделении строки. Во второй строке мы определяем тип - токенайзер, он будет разделять строки основываясь на данных приходящих от сепаратора.
Дальше мы определяем структуру и вектор, в который мы будем складывать результаты парсинга.

Посмотрим, каким образом tokenizer разбирает строку:
Допустим у нас есть строка, которая содержит данные в csv:

std::string line = "some string,1,some string 2,100500";
_Winnie C++ Colorizer
Объявляем сепаратор и токенайзер:

sep_type sep(",");
tok_type tok(line, sep);
_Winnie C++ Colorizer
Теперь чтобы вывести элементы csv на экран, нам достаточно сделать что-то вроде:

for (tok_type::const_iterator it = tok.begin(); it != tok.end(); ++it)
{
    std::cout << *it << std::endl;
}
_Winnie C++ Colorizer
С парсером разобрались, теперь мы хотим получить удобный механизм для инициализации структуры, при этом мы хотим диагностировать ошибку (например, в строке csv слишком мало элементов, или элемент неправильного типа: ожидалось число, а пришла строка).
Для этого напишем следующую шаблонную фукнцию:
template <typename InputIterator, typename ValueT>
void bindVariable(InputIterator pos, InputIterator end, ValueT & val)
{
    if (pos == end)
    {
        throw std::runtime_error("bad csv format");
    }
    val = boost::lexical_cast<ValueT>(*pos);
}
_Winnie C++ Colorizer
Эта функция делает очень немного: она проверяет что мы не вышли за границы итерируемых токенов, а также присваивает переменной val соответствующее значение. Однако нам хватит этой функции чтобы организовать заполнение структуры. При этом lexical_cast бросит исключение, если типы входных и выходных параметров несовместимы. В принципе, можно расширить эту функцию, передавая в нее еще один параметр, например сообщение об ошибке, тогда мы сможем бросить исключение, передав в конструктор исключения сообщение об ошибке. Также мы можем ловить исключение bad_lexical_cast, и формировать свое исключение на основе пойманного и сообщения об ошибке.

Поехали дальше, теперь мы будем заполнять собственно нашу структуру:

template <typename InputIterator>
void parseCsvLine(InputIterator it, InputIterator end, some_struct & res)
{
    bindVariable(it, end, res.line1); ++it;
    bindVariable(it, end, res.count1); ++it;
    bindVariable(it, end, res.line2); ++it;
    bindVariable(it, end, res.count2); ++it;
}
_Winnie C++ Colorizer
Вот собственно и все! Так как функция bindVariable - шаблонная, то мы в нее можем передавать параметр любого типа, который корректно обрабатывается функцией lexical_cast. А функция lexical_cast в свою очередь требует чтобы для типа были реализованы операторы
std::ostream & operator << (std::ostream & strm, const Type & val);
std::istream & operator >> (std::istream & strm, Type & val);
_Winnie C++ Colorizer
Вот полный пример того как работает данный парсер:
В примере я добавил свою структуру - my_type, чтобы показать, что парсер может обрабатывать не только стандартные типы, но и определенные пользователем.

#include <boost/tokenizer.hpp>
#include <boost/lexical_cast.hpp>
#include <ostream>
#include <iostream>
#include <fstream>
#include <vector>

typedef boost::char_separator<char> sep_type;
typedef boost::tokenizer<sep_type> tok_type;

struct my_type
{
    int val;
};

std::ostream & operator << (std::ostream & strm, const my_type & t)
{
    strm << t.val;
    return strm;
}

std::istream & operator >> (std::istream & strm, my_type & t)
{
    strm >> t.val;
    return strm;
}

struct some_struct
{
    std::string line1;
    int count1;
    std::string line2;
    //int count2;
    my_type count2;
};

typedef std::vector<some_struct> vec_type;

template <typename InputIterator, typename ValueT>
void bindVariable(InputIterator pos, InputIterator end, ValueT & val)
{
    if (pos == end)
    {
        throw std::runtime_error("bad csv format");
    }
    val = boost::lexical_cast<ValueT>(*pos);
}


template <typename InputIterator>
void parseCsvLine(InputIterator it, InputIterator end, some_struct & res)
{
    bindVariable(it, end, res.line1); ++it;
    bindVariable(it, end, res.count1); ++it;
    bindVariable(it, end, res.line2); ++it;
    bindVariable(it, end, res.count2); ++it;
}

int main(int argc, char * argv [])
{
    if (argc < 2)
    {
        std::cerr << "usage: " << argv[0] << " path/to/csv/file" << std::endl;
        return 1;
    }
    std::ifstream ifile(argv[1]);
    if (!ifile.is_open())
    {
        std::cerr << "faild to open file: " << argv[1] << std::endl;
        return 1;
    }

    std::string line;
    sep_type sep(",");
    vec_type vec;
    while (!ifile.eof())
    {
        std::getline(ifile, line);
        std::cout << "line: " << line << std::endl;
        tok_type tok(line, sep);

        some_struct tmp;
        parseCsvLine(tok.begin(), tok.end(), tmp);
        vec.push_back(tmp);
    }

    for (vec_type::const_iterator it = vec.begin(); it != vec.end(); ++it)
    {
        std::cout <<
                "line1: " << it->line1 <<
                ", count1: " << it->count1 <<
                ", line2: " << it->line2 <<
                ", count2: " << it->count2 << std::endl;
    }

    return 0;
}
_Winnie C++ Colorizer

4 комментария:

  1. CSV это же достаточно простой формат! Нахрена все так сложно было? Ведь достаточно для каждой строки вызвать split(DELIMITER_CHAR) и полученную разность проверить на кол-во столбцов, а если допустимое то работаем далее. И ненадо никаких токинизеров!

    ОтветитьУдалить
    Ответы
    1. Согласен, здесь реализация немного переусложнена. Здесь все же больший упор идет на использование именно библиотеки tokenizer, csv - не более чем один из вариантов применения. Соответственно это и хотелось показать в статье

      Удалить
  2. 2The NT...
    нуда, ну да, а как же экранирование кавычек, например?
    или, собственно, сам символ кавычки.
    всё время забывают о важных мелочах все.

    ОтветитьУдалить
    Ответы
    1. Вот-вот. Тоже хотел написать. В экранировании — самое интересное, там наверное свой separator придётся писать.

      Удалить