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-файл продаж, вычисляется итог продаж по продукту и выводится сводка. Здесь показано, как:
- Определяется структура для представления каждой строки данных.
- CSV-файл считывается и парсится в вектор структур.
- Обрабатываются данные для расчета итога продаж.
- Форматируются и выводятся результаты.
Обработка больших 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++, будь то небольшие наборы данных или обработка крупных объемов информации.
Читайте также:
- C++: практическое руководство по проверке наличия файла
- C++: подробное руководство по массивам
- Обработка файлов на C
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: Reading CSV Files in C++: How To Guide





