CSV-файлы  —  в них значения разделены запятыми  —  это обычный формат хранения табличных данных.

Разработчикам на C++ часто приходится считывать и обрабатывать эти файлы.

Изучим различные методы считывания CSV-файлов  —  от простых до продвинутых.

Базовое считывание CSV-файлов стандартными библиотеками C++

Начнем с простого подхода  —  исключительно стандартных библиотек C++. Этот метод хорош для простых CSV-файлов без сложного форматирования.

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

std::vector<std::vector<std::string>> readCSV(const std::string& filename) {
std::vector<std::vector<std::string>> data;
std::ifstream file(filename);

if (!file.is_open()) {
std::cerr << "Failed to open file: " << filename << std::endl;
return data;
}

std::string line;
while (std::getline(file, line)) {
std::vector<std::string> row;
std::stringstream ss(line);
std::string cell;

while (std::getline(ss, cell, ',')) {
row.push_back(cell);
}

data.push_back(row);
}

file.close();
return data;
}

int main() {
auto data = readCSV("example.csv");

for (const auto& row : data) {
for (const auto& cell : row) {
std::cout << cell << "\t";
}
std::cout << std::endl;
}

return 0;
}

В этом коде:
1. Открывается CSV-файл.

2. Файл построчно считывается.

3. Каждая строка разбивается запятыми-разделителями на ячейки.

4. Данные сохраняются в двумерном векторе.

5. И выводятся на консоль.

Метод не плох для базовых CSV-файлов, но не обходится без ограничений. Им некорректно обрабатываются закавыченные поля или запятые внутри полей.

Обработка закавыченных полей и запятых

Для сложных CSV-файлов доработаем логику парсинга:

std::vector<std::string> parseCSVRow(const std::string& row) {
std::vector<std::string> fields;
std::string field;
bool inQuotes = false;

for (char c : row) {
if (!inQuotes && c == ',') {
fields.push_back(field);
field.clear();
} else if (c == '"') {
inQuotes = !inQuotes;
} else {
field += c;
}
}
fields.push_back(field);

return fields;
}

std::vector<std::vector<std::string>> readCSV(const std::string& filename) {
std::vector<std::vector<std::string>> data;
std::ifstream file(filename);

if (!file.is_open()) {
std::cerr << "Failed to open file: " << filename << std::endl;
return data;
}

std::string line;
while (std::getline(file, line)) {
data.push_back(parseCSVRow(line));
}

file.close();
return data;
}

Этой доработанной версией корректно обрабатываются закавыченные поля и запятые внутри полей. Например, ”Smith, John”,42,”Software Engineer” спарсится как три отдельных поля.

Строковое представление для повышения производительности

В больших CSV-файлах производительность повышается исключением лишних копий строк благодаря std::string_view:

#include <string_view>

std::vector<std::string_view> parseCSVRow(std::string_view row) {
std::vector<std::string_view> fields;
size_t start = 0;
bool inQuotes = false;

for (size_t i = 0; i < row.length(); ++i) {
if (!inQuotes && row[i] == ',') {
fields.emplace_back(row.substr(start, i - start));
start = i + 1;
} else if (row[i] == '"') {
inQuotes = !inQuotes;
}
}
fields.emplace_back(row.substr(start));

return fields;
}

std::vector<std::vector<std::string_view>> readCSV(const std::string& filename) {
std::vector<std::vector<std::string_view>> data;
std::ifstream file(filename);

if (!file.is_open()) {
std::cerr << "Failed to open file: " << filename << std::endl;
return data;
}

std::string line;
while (std::getline(file, line)) {
data.push_back(parseCSVRow(line));
}

file.close();
return data;
}

В этой версии применяется std::string_view с его ссылкой на строковые данные без владения. Это эффективнее, особенно для больших CSV-файлов: лишнее копирование строковых данных избегается.

Реальное применение: анализ данных о продажах

Применим приемы считывания CSV к реальному сценарию  —  анализу данных о продажах. Вот CSV-файл sales.csv со столбцами для даты, продукта, количества и цены:

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

struct SaleRecord {
std::string date;
std::string product;
int quantity;
double price;
};

std::vector<SaleRecord> readSalesCSV(const std::string& filename) {
std::vector<SaleRecord> sales;
std::ifstream file(filename);

if (!file.is_open()) {
std::cerr << "Failed to open file: " << filename << std::endl;
return sales;
}

std::string line;
// Заголовок пропускаем
std::getline(file, line);

while (std::getline(file, line)) {
std::stringstream ss(line);
SaleRecord record;
std::string quantity_str, price_str;

if (std::getline(ss, record.date, ',') &&
std::getline(ss, record.product, ',') &&
std::getline(ss, quantity_str, ',') &&
std::getline(ss, price_str, ',')) {

record.quantity = std::stoi(quantity_str);
record.price = std::stod(price_str);
sales.push_back(record);
}
}

file.close();
return sales;
}

int main() {
auto sales = readSalesCSV("sales.csv");

std::unordered_map<std::string, double> total_sales;

for (const auto& sale : sales) {
total_sales[sale.product] += sale.quantity * sale.price;
}

std::cout << "Total sales by product:\n";
for (const auto& [product, total] : total_sales) {
std::cout << std::setw(20) << std::left << product
<< "$" << std::fixed << std::setprecision(2) << total << '\n';
}

return 0;
}

В этом примере считывается CSV-файл продаж, вычисляется итог продаж по продукту и выводится сводка. Здесь показано, как:

  1. Определяется структура для представления каждой строки данных.
  2. CSV-файл считывается и парсится в вектор структур.
  3. Обрабатываются данные для расчета итога продаж.
  4. Форматируются и выводятся результаты.

Обработка больших CSV-файлов

При работе с очень большими CSV-файлами считывать весь файл в память нецелесообразно. В таких случаях файл обрабатывается построчно:

void processSalesCSV(const std::string& filename) {
std::ifstream file(filename);

if (!file.is_open()) {
std::cerr << "Failed to open file: " << filename << std::endl;
return;
}

std::string line;
std::unordered_map<std::string, double> total_sales;

// Заголовок пропускаем
std::getline(file, line);

while (std::getline(file, line)) {
std::stringstream ss(line);
std::string date, product, quantity_str, price_str;

if (std::getline(ss, date, ',') &&
std::getline(ss, product, ',') &&
std::getline(ss, quantity_str, ',') &&
std::getline(ss, price_str, ',')) {

int quantity = std::stoi(quantity_str);
double price = std::stod(price_str);
total_sales[product] += quantity * price;
}
}

file.close();

std::cout << "Total sales by product:\n";
for (const auto& [product, total] : total_sales) {
std::cout << std::setw(20) << std::left << product
<< "$" << std::fixed << std::setprecision(2) << total << '\n';
}
}

При таком подходе каждая строка обрабатывается при ее считывании; в памяти сохраняется не все записи, а только нарастающий итог. Для больших файлов это экономичнее с точки зрения расходования памяти.

Использование сторонних библиотек

Для более сложных задач парсинга CSV привлекаются сторонние библиотеки. Так, например, применяется библиотека CSV Parser Бена Штрассера:

#include <iostream>
#include <csv.h>

int main() {
io::CSVReader<4> in("sales.csv");
in.read_header(io::ignore_extra_column, "Date", "Product", "Quantity", "Price");
std::string date, product;
int quantity;
double price;

std::unordered_map<std::string, double> total_sales;

while(in.read_row(date, product, quantity, price)){
total_sales[product] += quantity * price;
}

std::cout << "Total sales by product:\n";
for (const auto& [product, total] : total_sales) {
std::cout << std::setw(20) << std::left << product
<< "$" << std::fixed << std::setprecision(2) << total << '\n';
}

return 0;
}

Этой библиотекой учитываются многие пограничные случаи, она быстрее выполняемого вручную парсинга сложных CSV-файлов.

Заключение

На C++ считываются простые и сложные CSV-файлы. Для первых достаточно стандартных библиотек C++. Для сценариев посложнее реализовывается пользовательская логика парсинга или привлекаются сторонние библиотеки.

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

Освоив эти подходы, вы подготовитесь к работе с CSV-файлами в проектах на C++, будь то небольшие наборы данных или обработка крупных объемов информации.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: Reading CSV Files in C++: How To Guide

Предыдущая статьяKotlin Multiplatform: как усовершенствовать процесс разработки iOS
Следующая статьяNest.js и Next.js: в чем разница?