В основном я ответил на этот вопрос уже в похожей теме. Но в любом случае, я покажу готовое решение с другим подходом и некоторым объяснением здесь.
Один совет: вам следует лучше ознакомиться с объектно-ориентированным программированием. И продумайте свой дизайн. В вашей функции чтения и записи вы создаете ненужную зависимость от файла или от 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», пока все данные не будут записаны.