Извлечение определенных столбцов из файла CSV в C ++ - PullRequest
0 голосов
/ 03 апреля 2020

Я хотел бы знать, как я могу извлечь / пропустить определенные столбцы, такие как age и weight из файла CSV в C ++.

Имеет ли смысл извлекать нужные информация после того, как я загрузил весь CSV-файл (если память не проблема)?

РЕДАКТИРОВАТЬ : Если возможно, я хотел бы иметь часть для чтения, печати и модификации.

Если возможно, я хочу использовать только STL. Содержимое моего тестового CSV-файла выглядит следующим образом:

*test.csv*

name;age;weight;height;test
Bla;32;1.2;4.3;True
Foo;43;2.2;5.3;False
Bar;None;3.8;2.4;True
Ufo;32;1.5;5.4;True

Я загружаю файл test.csv следующей программой C ++, которая выводит содержимое файла на экран:

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

void readCSV(std::vector<std::vector<std::string> > &data, std::string filename);
void printCSV(const std::vector<std::vector<std::string>> &data);

int main(int argc, char** argv) {
    std::string file_path = "./test.csv";
    std::vector<std::vector<std::string> > data;
    readCSV(data, file_path);
    printCSV(data);
    return 0;
}

void readCSV(std::vector<std::vector<std::string> > &data, std::string filename) {
    char delimiter = ';';
    std::string line;
    std::string item;
    std::ifstream file(filename);
    while (std::getline(file, line)) {
        std::vector<std::string> row;
        std::stringstream string_stream(line);
        while (std::getline(string_stream, item, delimiter)) {
            row.push_back(item);
        }
        data.push_back(row);
    }
    file.close();
}

void printCSV(const std::vector<std::vector<std::string> > &data) {
    for (std::vector<std::string> row: data) {
        for (std::string item: row) {
            std::cout << item << ' ';
        }
        std::cout << std::endl;
    }
}

Буду очень признателен за любую помощь.

1 Ответ

1 голос
/ 04 апреля 2020

В основном я ответил на этот вопрос уже в похожей теме. Но в любом случае, я покажу готовое решение с другим подходом и некоторым объяснением здесь.

Один совет: вам следует лучше ознакомиться с объектно-ориентированным программированием. И продумайте свой дизайн. В вашей функции чтения и записи вы создаете ненужную зависимость от файла или от std::cout. Таким образом, вы не должны передавать имя файла и затем открывать файл в функции, а использовать streams. Потому что в функции, которую я создал, используя средства ввода-вывода C ++, не имеет значения, будем ли мы читать из файла или std::istringstream или записывать в std::cout или файловый поток.

Все будет обрабатываться через (перегруженные) операторы извлечения и вставки.

Итак, так как я хотел, чтобы код был немного более гибким, я сделал мою структуру шаблоном, чтобы иметь возможность вставлять выбранные столбцы и используйте эту же структуру для других комбинаций столбцов.

Если вы хотите исправить выбранные столбцы, вы можете удалить строку с template и заменить std::vector<size_t> selectedFields{ {Colums...} }; на std::vector<size_t> selectedFields{ {1,2} };

Позже мы используем using для шаблона, чтобы упростить его обработку и понимание:

// Define Dataype for selected columns age and weight
using AgeAndWeight = SelectedColumns<1, 2>;

ОК, давайте сначала посмотрим на исходный код, а затем попытаемся понять.

#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <fstream>
#include <initializer_list>
#include <iterator>
#include <algorithm>

std::regex re{ ";" };

// Proxy for reading an splitting a line and extracting certain fields and some simple output
template<size_t ... Colums>
struct SelectedColumns {
    std::vector<std::string> data{};
    std::vector<size_t> selectedFields{ {Colums...} };

    // Overwrite extractor operator
    friend std::istream& operator >> (std::istream& is, SelectedColumns& sl) {

        // Read a complete line and check, if it could be read
        if (std::string line{}; std::getline(is, line)) {

            // Now split the line into tokens
            std::vector tokens(std::sregex_token_iterator(line.begin(), line.end(), re, -1), {});

            // Clear old data
            sl.data.clear();

            // So, and now copy the selected columns into our data vector
            for (const size_t& column : sl.selectedFields) 
                if (column < tokens.size()) sl.data.push_back(tokens[column]);
        }
        return is;
    }
    // Simple extractor
    friend std::ostream& operator << (std::ostream & os, const SelectedColumns & sl) {
        std::copy(sl.data.begin(), sl.data.end(), std::ostream_iterator<std::string>(os, "\t"));
        return os;
    }
};

// Define Dataype for selected columns age and weight
using AgeAndWeight = SelectedColumns<1U, 2U>;

const std::string fileName{ "./test.csv" };

int main() {

    // Open the csv file and check, if it is open
    if (std::ifstream csvFileStream{ fileName }; csvFileStream) {

        // Read complete csv file and extract age and weight columns        
        std::vector sc(std::istream_iterator<AgeAndWeight>(csvFileStream), {});

        // Now all data is available in this vector  sc    Do something
        sc[3].data[0] = "77";

        // Show some debug out put
        std::copy(sc.begin(), sc.end(), std::ostream_iterator<AgeAndWeight>(std::cout, "\n"));

        // By the way, you could also write the 2 lines above in one line.
        //std::copy(std::istream_iterator<AgeAndWeight>(csvFileStream), {}, std::ostream_iterator<AgeAndWeight>(std::cout, "\n"));

    }
    else std::cerr << "\n*** Error: Could not open source file\n\n";
    return 0;
}

Один важный задача здесь состоит в том, чтобы разбить строку с данными CSV на ее токены. Давайте посмотрим на это.

Разделение строки на токены:

Что люди ожидают от функции, когда читают

getline?

Большинство людей скажут: «Хм, я думаю, что откуда-то будет прочитана полная строка». И угадайте, что это было основным намерением для этой функции. Прочитайте строку из потока и поместите ее в строку.

Но, как вы можете видеть здесь std::getline имеет некоторые дополнительные функции.

И это приводит к серьезному неправильному использованию этой функции для разделения std::string s. в токены.

Разделение строк в токены - очень старая задача. В самом начале C была функция strtok, которая все еще существует даже в C ++. Здесь std::strtok. Пожалуйста, посмотрите std::strtok -пример

std::vector<std::string> data{};
for (char* token = std::strtok(const_cast<char *>(line.data()), ","); token != nullptr; token = std::strtok(nullptr, ",")) 
    data.push_back(token);

Простой, верно?

Но из-за дополнительной функциональности std::getline он сильно использовался для токенизации строк. Если вы посмотрите на главный вопрос / ответ о том, как анализировать файл CSV (см. здесь ), то вы поймете, что я имею в виду.

Люди используют std::getline для чтения текстовая строка, строка из исходного потока, затем вставьте ее в std::istringstream и снова используйте std::getline с разделителем, чтобы разобрать строку в токены. Странно.

Но вот уже много лет у нас есть специальная специальная функция для токенизации строк, специально разработанная специально для этой цели. Это

std::sregex_token_iterator

И поскольку у нас есть такая выделенная функция, мы должны просто использовать ее.

Эта вещь является итератором. Для итерации по строке, следовательно, имя функции начинается с s. Начальная часть определяет, в каком диапазоне ввода мы будем работать, конечная часть создается по умолчанию, а затем есть std :: regex для того, что должно быть сопоставлено / или что не должно совпадать во входной строке. Тип стратегии сопоставления указан с последним параметром.

  • 0 -> дать мне материал, который я определил в регулярном выражении, и (необязательно)
  • -1 -> дать мне то, что НЕ соответствует на основе регулярного выражения.

Мы можем использовать этот итератор для хранения токенов в std::vector. std::vector имеет конструктор диапазона, который принимает 2 итератора в качестве параметра и копирует данные между первым и вторым итератором в std :: vector. Оператор

std::vector tokens(std::sregex_token_iterator(s.begin(), s.end(), re, -1), {});

определяет переменную «tokens» как std :: vector и использует так называемый range-конструктор std :: vector. Обратите внимание: я использую C ++ 17 и могу определить std::vector без аргумента шаблона. Компилятор может вывести аргумент из заданных параметров функции. Эта функция называется CTAD («дедукция аргумента шаблона класса»).

Кроме того, вы можете видеть, что я не использую итератор end () явно.

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

Вы можете прочитать любой Количество жетонов в строке и положить его в std::vector

Но вы можете сделать еще больше. Вы можете подтвердить свои данные. Если вы используете 0 в качестве последнего параметра, вы определяете std::regex, который даже проверяет ваш ввод. И вы получаете только действительные токены.

В целом, использование выделенной функциональности превосходит злоупотребление std::getline, и люди должны просто ее использовать.

Некоторые люди жалуются на издержки функции, и они правы, но сколько из них используют большие данные. И даже тогда подход, вероятно, будет тогда использовать string.find и string.substring или std::stringviews или что-либо еще.


Итак, теперь к дальнейшим темам.

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

Затем мы токенизируем эту только что прочитанную строку, как описано выше.

И затем мы скопируем только выбранные столбцы из токенов в наши результирующие данные. Это сделано в простой для l oop. Здесь мы также проверяем границы, потому что кто-то может указать недопустимые выбранные столбцы или строка может иметь меньше токенов, чем ожидалось.

Таким образом, тело экстрактора очень простое. Всего 5 строк кода. , .


Затем, опять же,

Вы должны начать использовать объектно-ориентированные функции в C ++. В C ++ вы можете поместить данные и методы, которые работают с этими данными, в один объект. Причина в том, что внешний мир не должен заботиться о внутренних объектах. Например, ваша функция readCSV и printCSV должна быть частью структуры (или класса).

И в качестве следующего шага мы не будем использовать ваши функции «чтение» и «печать». Мы будем использовать специальную функцию для Stream-IO, оператор извлечения >> и оператор вставки <<. И мы перезапишем стандартные функции IO в нашей структуре. </p>

В функции main мы откроем исходный файл и проверим, было ли открытие успешным. КСТАТИ. Все функции ввода-вывода должны быть проверены, если они были успешными.

Затем мы используем следующий итератор, the std::istream_iterator. И это вместе с нашим типом AgeAndWeight и потоком входного файла. Также здесь мы используем CTAD и созданный по умолчанию конечный итератор. std::istream_iterator будет неоднократно вызывать оператор извлечения AgeAndWeight, пока все строки исходного файла не будут прочитаны.

Для вывода мы будем использовать std::ostream_iterator. Это будет вызывать оператор вставки для «AgeAndWeight», пока все данные не будут записаны.

...