четверг, 16 февраля 2012 г.

C++. Чтение CSV файла в двумерный массив

В качестве примера использования предложенных ранее функций разбивки строки и конвертирования строки в число привожу листинг программы, которая читает данные из файла формата CSV в двумерный массив и выводит полученное на консоль.
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
#include <iostream>

using std::string;
using std::vector;
using std::ifstream;
using std::istringstream;
using std::cout;

void splitString(const string &fullstr,
                 vector<string> &elements,
                 const string &delimiter) {

    string::size_type lastpos =
        fullstr.find_first_not_of(delimiter, 0);
    string::size_type pos     =
        fullstr.find_first_of(delimiter, lastpos);

    while ( (string::npos != pos) || (string::npos != lastpos) ) {

        elements.push_back(fullstr.substr(lastpos, pos-lastpos));

        lastpos = fullstr.find_first_not_of(delimiter, pos);
        pos = fullstr.find_first_of(delimiter, lastpos);
    }
}

double stringToDouble(const string &str) {

    istringstream stm;
    double val = 0;

    stm.str(str);
    stm >> val;

    return val;
}

void readData(const string &filename,
              const string &csvdelimiter,
              vector< vector<double> > &sarr) {

    ifstream fin(filename.c_str());

    string s;
    vector<string> selements;
    vector<double> delements;

    while ( !fin.eof() ) {

        getline(fin, s);

        if ( !s.empty() ) {

            splitString(s, selements, csvdelimiter);

            for ( size_t i=0; i<selements.size(); i++ ) {

                delements.
                    push_back(stringToDouble(selements[i]));
            }

            sarr.push_back(delements);
            selements.clear();
            delements.clear();
        }
    }

    fin.close();
}

int main(int argc, char** argv) {

    vector< vector<double> > sarr;
    
    readData("data.csv", ";", sarr);

    for ( size_t i=0; i<sarr.size(); i++ ) {

        for ( size_t j=0; j<sarr[0].size(); j++ ) {

            cout << sarr[i][j] << "\t";
        }

        cout << "\n";
    }

    return 0;
}
Дабы не раздувать приведенный в качестве примера код, проверка доступности файла, возможности его открытия, а также однородности массива в файле с данными были опущены. Не забывайте о подобных проверках в реальных программах!

19 комментариев:

  1. подправил маленький недочет

    ОтветитьУдалить
  2. Здесь еще есть про двумерный массив:
    http://hashcode.ru/questions/57957

    ОтветитьУдалить
  3. Подскажите пожалуйста! Есть csv файл вида:
    "45.085","59.288","",""
    "47.383","59.094","",""

    Как сделать, чтобы программа пропускала еще и кавычки?

    ОтветитьУдалить
    Ответы
    1. Что это за чудо программа сохраняет данные в таком формате? Первый раз вижу, чтобы числа в csv были обернуты в кавычки. Ну что ж... если необходимо обработать такое, то просто напишите функцию фильтр, которой будете передавать каждую считанную из файла строку. Т.е. считали из файла строку, передали функции. Функция прошлась посимвольно по строке, кавычки отбросила, новую строку без кавычек возвратила. Далее все как в примере. Самый простой вариант, если с ходу.

      Удалить
  4. Я конвертирую координаты из Google Earth формата kml и получается такой файл, а кавычки ставятся исходя из описания формата csv: "Значения, содержащие зарезервированные символы (двойная кавычка, запятая, точка с запятой, новая строка) обрамляются двойными кавычками (")".
    Спасибо большое за помощь, вы направили мои мысли в нужную сторону!)

    ОтветитьУдалить
  5. Добрый день! Спасибо большое за статью, очень помогла разобраться в вопросе.
    А как бы вы посоветовали поступить, если в csv файле не только числа, но и элементы с символами. Как раз о координатах опять идет речь, и зноке градусов. Ваш метод считывает только до знака градусов, и потом переходит к новому элементу.
    Скажем, из строки
    10127;8050;49°11'23.10"N,16°32'13.38"E
    Считывает только 10127 8050 49
    Я понимаю, что дело в функции stringToDouble, но не выходит ее переписать так, чтоб дальше все работало.
    Буду благодарна за любой совет

    ОтветитьУдалить
    Ответы
    1. Добрый день. Рад, что статья вам хотя бы частично, но уже помогла. В вашем случае я бы посоветовал поступить следующим образом. Во-первых, создайте класс, экземпляры которого будут хранить координаты. Со структурой класса, надеюсь, все понятно. Затем напишите функцию, которая будет распознавать данные с координатами. К примеру, считываете свою строку из файла, разбиваете по символу ";", каждый элемент прогоняете через функцию распознавания координат. Если функция возвращает true, то такой элемент скармливаете конструктору класса Coordinate (или как вы его там назовете), если false, то уже напрямую в функцию stringToDouble и складируете куда вам там нужно. Как-то так. Как реализовать функцию распознавания координат - ваш выбор. Можно, например, с помощью регулярных выражений.

      Удалить
    2. Спасибо за такой быстрый ответ :)
      А просто убрать stringToDouble и напрямую хранить стринг в элементах массива не сработает?
      Прошу прощения, если вопрос глупый, это по сути моя первая программа в с++, и нужно все максимально просто сделать. Хотя то что вы выше предложили, конечно, звучит намного правильнее.

      Удалить
    3. Пожалуйста, избавьтесь от подхода "сработает - не сработает". Это кривая дорожка ) Конечно можно хранить строки в векторе. Все зависит от вашей задачи, от ваших конечных целей. Если вам нужно будет делать еще какие-то вычисления - никуда не денешься, нужно конвертировать строки в числа, если же вы, к примеру, просто переводите из одного формата в другой, по сути задача считать файл и записать файл, то, разумеется, крутить данные из строки в число и обратно смысла нет никакого. Все зависит от вашей задачи.

      Удалить
  6. Да, вы правы, действительно нужно избавляться =)
    Спасибо за советы! Теперь я хотя бы точно знаю что мне нужно делать) Так как дальше мне полученные координаты нужно переводить в другой формат - таки буду делать по предложенному вами варианту.
    И еще, если не сложно, намекните как с регулярными выражениями в данной ситуации работать) Я общую идею понимаю, что нужно ними описать структуру координат. Из-за отсутствия практики понять как реализовать трудно.

    ОтветитьУдалить
    Ответы
    1. Регулярные выражения стали нам доступны в стандартной библиотеке с приходом C++11. Поэтому здесь важно каким компилятором и какой его версией вы пользуетесь. Если ваш компилятор не имеет реализации регулярных выражений в стандартной библиотеке - используйте boost.
      Как в данной ситуации работать с регулярными выражениями? Напрямую, непосредственно ) Если серьезно, то могу порекомендовать ресурс номер один для плюсовика (для начинающего программиста на С++ он должен стать лучшим другом) http://www.cplusplus.com. Уверен, что разберетесь. Вот страничка по регулярным выражениям http://www.cplusplus.com/reference/regex/. По каждой функции имеются примеры. Изучайте.
      Если будете использовать библиотеки boost, то рекомендую сайт http://en.highscore.de/cpp/boost/. Здесь в подробностях рассказывается о различных компонентах этого замечательного набора библиотек, с примерами и комментариями. В частности, вот раздел по регулярным выражениям http://en.highscore.de/cpp/boost/stringhandling.html#stringhandling_regex.
      Конечно, за вас я регулярное выражение для описания координат составлять не буду, иначе вы ничему не научитесь ) Еще полезно почитать чужой код, посмотреть как люди пишут. Загляните хотя бы в один из моих проектов. К примеру, здесь https://github.com/pa23/reup/blob/master/src/menu.cpp#L213 у меня используется регулярное выражение для поиска строк, содержащих имя файла вида P_986.2.0.0_YMZ-536_S3.14_15.05.2014.hex. Однако, имейте в виду, что здесь у меня используются так называемые "сырые" (raw) строки, опять же фишка из C++11. Если будете использовать обычные строки, то не забывайте экранировать все специальные символы.

      Удалить
    2. ого, такой подробный ответ! Еще раз огромное спасибо =)
      Теперь полностью понятно направление, что читать и что делать.

      Удалить
  7. А если у чисел в качестве разделителя разрядов используется запятая?

    ОтветитьУдалить
    Ответы
    1. Так замените запятую на точку )

      ...
      #include
      ...
      using std::replace;
      ...
      double stringToDouble(const string &str) {

      string tstr = str;
      replace(tstr.begin(), tstr.end(), ',', '.');

      istringstream stm;
      double val = 0;

      stm.str(tstr);
      stm >> val;

      return val;
      }
      ...

      Удалить
    2. Директивой include подключаем файл algorithm. Blogger подрезал текст из-за угловых скобок.

      Удалить
  8. Спасибо огромное за ваш пример! Не часто найдешь работающий понятный код без лишней воды. Стояла задача импортировать данные из очень объемного csv файла для последующей обработки. Благодаря вашему примеру быстро разобралась!

    ОтветитьУдалить
    Ответы
    1. Пожалуйста. Очень рад, что пример полезен.

      Удалить