Как (элегантно) прочитать файл, столбцы которого имеют разные типы, и правильно хранить столбцы? - PullRequest
3 голосов
/ 20 мая 2019

Я изучаю шаблоны и хочу по пути решить следующие задачи: я хотел бы прочитать файл csv, столбцы которого имеют разные типы (string, int и т. Д.), Сохранить каждый столбец в vector, а затем получить доступ к векторам. Может ли кто-нибудь любезно указать, как я могу хранить столбцы хорошо?

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

first_column,second_column
int,string
1, line1
2, line2

В файлах csv всегда будет имя столбца в первой строке и типы данных во второй строке, за которыми следуют фактические данные. Тем не менее, потенциальное количество столбцов не ограничено, как и его «порядок или его» типы. Следовательно, другой пример может быть

first_column,second_column,third_colum
string, double, string
foo, -19.8, mario
bar, 20.1, anna

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

Я представляю, что заголовочный файл класса, решающего задачу, выглядит так:

#include <fstream>
#include <string>
#include <vector>

class ColumnarCSV {
   public:
    ColumnarCSV(std::string filename) {read_data(filename);}
    std::vector<std::string> get_names() { return column_names; }
    std::vector<std::string> get_types() { return column_types; }
    // pseudocode
    template <typename T>
    std::vector<T> get_column(std::string column_name) {
        return column;
    }  //
   private:
    void read_data(std::string filename);
    std::vector<std::string> column_names;
    std::vector<std::string> column_types;
    // storage for the columns;
};

Класс ColumnarCSV состоит из string с указанием местоположения CSV file. Две открытые функции предоставляют имена столбцов и типы столбцов, закодированные в vector<string>. Функция get_column требует имя столбца и возвращает его данные. Обратите внимание, что я не знаю, как написать эту функцию. Тип возврата может быть другим, если это необходимо. У кого-нибудь есть идея, как правильно хранить столбцы и заполнять их во время выполнения в зависимости от типа столбца?

Что я пробовал до сих пор:

  1. Наследование : я пытался работать с базовым классом BaseColumn, который содержит имя столбца и тип данных. Производный класс template <typename T>ActualColumn: public BaseColumn содержит фактические данные. Я хотел получить доступ к данным через виртуальную функцию, но узнал, что не могу определить функции виртуального шаблона.
  2. Std: Variant : Я думал о работе с Std :: вариантом и указанием всех возможных типов столбцов. Однако я подумал, что должен быть способ, не прибегая к инновациям c ++ 17.
  3. Создать пустое vector<vector<T>> для всех непредвиденных обстоятельств: Идеей грубой силы было бы снабдить ColumnarCSV элементом vector<vector<T>> для всех типов данных, которые я могу придумать, и заполнить их во время выполнения , В то время как это завершило свою работу, код был очень запутанным.

Есть ли лучший способ решить определить класс ColumnarCSV?

Ответы [ 2 ]

1 голос
/ 20 мая 2019

Я думаю, что вы можете хранить данные построчно как полные std::string.

Зная типы данных, вы сможете легко преобразовать std::string в реальный тип (std::string, int, double, ...).
Например, если у вас есть std::string, который в действительности является двойным, вы можете использовать std::stod для его преобразования.


Я сделал пример, чтобы быть более ясным. Для обработки данных рассмотрим следующее struct:

typedef std::vector<std::string> StringVec;

struct FileData
{
    StringVec col_names;
    StringVec type_names;
    StringVec data_lines;

    bool loadData(const std::string & file_path);
    bool getColumn(const std::string & col_name, StringVec & result);
};

typedef здесь только для того, чтобы упростить код и сделать его более читабельным.

Метод loadData() будет считывать файл и сохранять его содержимое в структуре.
col_names - список имен столбцов, type_names - список типов, data_lines - список прочитанные строки.

Метод getColumn() записывает в аргументе result содержимое нужного столбца, указанного в аргументе col_name.

Эти два метода возвращают логическое значение, которое указывает, была ли операция успешно выполнена (true) или произошла ошибка (false).

loadData() может возвращать false, если данный файл не может быть открыт или поврежден.
getColumn() может возвращать false, если имя указанного столбца не существует.

Возможная реализация этих методов может быть:

#include <fstream>

// ========== ========== ========== ========== ==========

StringVec split(const std::string & s, char c)
{
    StringVec splitted;

    std::string word;
    for(char ch : s)
    {
        if((ch == c) && (!word.empty()))
        {
            splitted.push_back(word);
            word.clear();
        }
        else
            word += ch;
    }
    if(!word.empty())
        splitted.push_back(word);

    return splitted;
}
void removeExtraSpaces(std::string & word)
{
    while(!word.empty() && (word[0] == ' '))
        word.erase(word.begin());

    while(!word.empty() && (word[word.size()-1] == ' '))
        word.erase(word.end()-1);
}

// ========== ========== ========== ========== ==========

bool FileData::loadData(const std::string & file_path)
{
    bool success(false);

    std::ifstream in_s(file_path);
    if(in_s)
    {
        bool names_read(false);
        bool types_read(false);

        std::string line;
        while(getline(in_s, line))
        {
            if(!names_read) // first line
            {
                col_names = split(line, ',');

                if(col_names.empty())
                    return false; // FILE CORRUPTED

                for(std::string & word : col_names)
                    removeExtraSpaces(word);

                names_read = true;
            }
            else if(!types_read) // second line
            {
                type_names = split(line, ',');

                if(type_names.size() != col_names.size())
                {
                    col_names.clear();
                    type_names.clear();
                    return false; // FILE CORRUPTED
                }

                for(std::string & word : type_names)
                    removeExtraSpaces(word);

                types_read = true;
            }
            else // other lines
            {
                if(split(line, ',').size() != col_names.size())
                {
                    col_names.clear();
                    type_names.clear();
                    data_lines.clear();
                    return false; // FILE CORRUPTED
                }

                data_lines.push_back(line);
            }
        }

        in_s.close();
        success = true;
    }

    return success;
}
bool FileData::getColumn(const std::string & col_name, StringVec & result)
{
    bool success(false);

    bool contains(false);
    size_t index(0);
    while(!contains && (index < col_names.size()))
    {
        if(col_names[index] == col_name)
            contains = true;
        else
            ++index;
    }
    if(contains)
    {
        for(const std::string & line : data_lines)
        {
            std::string field(split(line, ',').at(index));
            removeExtraSpaces(field);
            result.push_back(field);
        }
        success = true;
    }

    return success;
}

// ========== ========== ========== ========== ==========

Функции split() и removeExtraSpaces() определены, чтобы упростить код (и сделать этот пример более читабельным).

Со стороны пользователя это можно использовать следующим образом:

DataFile df;
bool loadSuccessful = df.loadData("data.txt"); // if true, df contains now the content of the file.
StringVec col;
bool columnFound = df.getColumn("col_name", col); // if true, col contains now the content of the desired column.

Как видите, очень прост в использовании :)
Я знаю, что на данный момент у вас есть вектор std::string, но так как структура содержит имена реального типа каждого столбца, вы можете преобразовать что вы получили в реальном типе.
Возможно, вы можете добавить шаблонный метод convert() в структуру, чтобы сделать его незаметным для пользователя.


Я провел тесты со следующими файлами данных:

data.txt:

first_col, second_col
string, double
line1, 1.1
line2, -2.5
line3, 10.03

_other_data.txt: _

first_col, second_col, third_col
int, string, char
0, line1, a
5, line2, b

И он успешно работал для обоих.


Я не знаю, достаточно ли элегантна обработка данных, как std::string, но я надеюсь, что она вам поможет.

1 голос
/ 20 мая 2019

Я думаю, вы слишком усложняете проблему. Вам на самом деле не нужны шаблоны, и определенно вам не нужно наследование или какая-либо форма стирания типов, когда у вас всегда есть int и string. Если одна строка соответствует одной «записи» в файле, все, что вам нужно, это

struct entry { 
    int id;
    std::string x;
};

и оператор ввода

std::istream& operator>>(std::istream& in, entry& e) {
    in >> e.id;
    in >> e.x;
    return in;
}

Теперь читать записи просто. Чтобы прочитать одну строку, вы делаете

std::ifstream file("file.name");
entry x;
file >> x;    
...