Создание простого CSV-файла

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

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

int main() {
std::ofstream file("example.csv");

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

std::vector<std::vector<std::string>> data = {
{"Name", "Age", "City"},
{"John", "30", "New York"},
{"Alice", "25", "London"},
{"Bob", "35", "Paris"}
};

for (const auto& row : data) {
for (size_t i = 0; i < row.size(); ++i) {
file << row[i];
if (i != row.size() - 1) file << ",";
}
file << "\n";
}

file.close();
std::cout << "CSV file created successfully." << std::endl;

return 0;
}

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

Обработка специальных символов

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

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

std::string escapeCSV(const std::string& field) {
if (field.find(',') != std::string::npos ||
field.find('"') != std::string::npos ||
field.find('\n') != std::string::npos) {
std::ostringstream result;
result << '"';
for (char c : field) {
if (c == '"') result << '"';
result << c;
}
result << '"';
return result.str();
}
return field;
}

void writeCSV(const std::string& filename, const std::vector<std::vector<std::string>>& data) {
std::ofstream file(filename);

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

for (const auto& row : data) {
for (size_t i = 0; i < row.size(); ++i) {
file << escapeCSV(row[i]);
if (i != row.size() - 1) file << ",";
}
file << "\n";
}

file.close();
std::cout << "CSV file created successfully: " << filename << std::endl;
}

int main() {
std::vector<std::vector<std::string>> data = {
{"Name", "Description", "Price"},
{"Widget", "A \"fancy\" widget", "19.99"},
{"Gadget", "A simple, useful gadget", "9.99"},
{"Doodad", "A doodad with, commas", "5.99"}
};

writeCSV("products.csv", data);

return 0;
}

В этой усовершенствованной версии корректно обрабатываются поля с запятыми, кавычками или символами новой строки  —  за счет их закавычивания и экранирования внутренних кавычек.

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

Для CSV-операций посложнее используется библиотека вроде csv-parser. Вот пример:

#include <iostream>
#include <vector>
#include <string>
#include "csv.hpp"

int main() {
std::vector<std::vector<std::string>> data = {
{"Name", "Age", "City"},
{"John", "30", "New York"},
{"Alice", "25", "London"},
{"Bob", "35", "Paris"}
};

csv::CSVWriter writer("output.csv");

for (const auto& row : data) {
writer << row;
}

std::cout << "CSV file created successfully." << std::endl;

return 0;
}

Библиотекой процесс упрощается и пограничные случаи обрабатываются автоматически.

Реальный сценарий: генератор отчетов о продажах

Создадим пример посложнее, в котором генерируется CSV-файл отчетов о продажах:

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

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

double getTotalPrice() const { return quantity * unitPrice; }
};

std::string getCurrentDate() {
std::time_t now = std::time(nullptr);
std::tm* localTime = std::localtime(&now);
char buffer[11];
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d", localTime);
return std::string(buffer);
}

void generateSalesReport(const std::vector<SaleRecord>& sales, const std::string& filename) {
std::ofstream file(filename);

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

file << "Date,Product,Quantity,Unit Price,Total Price\n";

double grandTotal = 0.0;
for (const auto& sale : sales) {
file << sale.date << ","
<< sale.product << ","
<< sale.quantity << ","
<< std::fixed << std::setprecision(2) << sale.unitPrice << ","
<< sale.getTotalPrice() << "\n";
grandTotal += sale.getTotalPrice();
}

file << "\nGrand Total,,," << grandTotal << "\n";
file << "Report Generated," << getCurrentDate() << "\n";

file.close();
std::cout << "Sales report generated: " << filename << std::endl;
}

int main() {
std::vector<SaleRecord> sales = {
{"2023-05-01", "Widget A", 10, 19.99},
{"2023-05-02", "Gadget B", 5, 29.99},
{"2023-05-03", "Widget A", 8, 19.99},
{"2023-05-04", "Gizmo C", 3, 49.99}
};

generateSalesReport(sales, "sales_report.csv");

return 0;
}

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

Производительность

При работе с большими наборами данных важна производительность. Вот рекомендации по повышению производительности записи в CSV:

  1. Для строковых операций используйте stringstream: производительность повышается применением std::stringstream вместо конкатенации строк.
  2. Записи в буфер: записывайте в буфер памяти, а затем сбрасывайте в файл кусками побольше.
  3. Используйте резерв для векторов: зная приблизительный размер данных, заранее выделяйте память при помощи reserve().

А так реализуются эти оптимизации:

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

void writeCSVOptimized(const std::string& filename, const std::vector<std::vector<std::string>>& data) {
std::ofstream file(filename, std::ios::binary); // Двоичный режим ради производительности

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

std::stringstream buffer;
buffer.reserve(1024 * 1024); // Резервируется буфер на 1 Мб

for (const auto& row : data) {
for (size_t i = 0; i < row.size(); ++i) {
buffer << row[i];
if (i != row.size() - 1) buffer << ",";
}
buffer << "\n";

if (buffer.tellp() > 1024 * 1024) { // Если буфером превышен 1 Мб
file << buffer.rdbuf();
buffer.str("");
buffer.clear();
}
}

// Записываются любые остальные данные
file << buffer.rdbuf();

file.close();
std::cout << "CSV file created successfully: " << filename << std::endl;
}

int main() {
std::vector<std::vector<std::string>> data;
data.reserve(1000000); // Резервируется место для 1 млн строк

// Генерируются примерные данные
for (int i = 0; i < 1000000; ++i) {
data.push_back({std::to_string(i), "Data" + std::to_string(i)});
}

writeCSVOptimized("large_file.csv", data);

return 0;
}

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

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

Для работы с различными типами данных создается параметризованная функция:

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

template<typename T>
std::string toString(const T& value) {
std::ostringstream oss;
oss << value;
return oss.str();
}

template<typename... Args>
void writeCSVRow(std::ofstream& file, const Args&... args) {
std::vector<std::string> row = {toString(args)...};
for (size_t i = 0; i < row.size(); ++i) {
file << row[i];
if (i != row.size() - 1) file << ",";
}
file << "\n";
}

int main() {
std::ofstream file("mixed_data.csv");

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

writeCSVRow(file, "Name", "Age", "Height", "Is Student");
writeCSVRow(file, "John Doe", 30, 1.75, true);
writeCSVRow(file, "Jane Smith", 25, 1.68, false);

file.close();
std::cout << "CSV file created successfully." << std::endl;

return 0;
}

При таком подходе легко записывать строки со смешанными типами данных.

Типичные ошибки и как их избежать

  1. Незакрытый файл. Всегда закрывайте файл после записи. Для автоматического закрытия файлов используйте смарт-указатели или принципы RAII, что расшифровывается как «получение ресурса есть инициализация».
  2. Некорректная обработка специальных символов. Убедитесь, что поля с запятыми, кавычками или символами новой строки корректно экранированы или закавычены.
  3. Предполагается, что все данные строковые. При работе с числовыми данными учитывайте настройки локали, особенно для десятичных точек.
  4. Не проверяется, открылся ли файл. До записи данных в файл всегда проверяйте, открыт ли он.
  5. Игнорирование проблем с кодировкой символов. Не забывайте о ней, особенно при работе с международными данными. Используйте библиотеки, которыми поддерживается кодировка UTF-8.

Заключение

Способы создания CSV-файлов на C++ варьируются от простых манипуляций со строками до использования специализированных библиотек. Выбор определяется сложностью данных и требованиями по производительности.

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

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

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


Перевод статьи ryan: How to Create CSV File Using C++

Предыдущая статьяСетка данных с точки зрения баз данных и на практике
Следующая статьяУчет соседей: повышение эффективности эмбеддингов документов с помощью контекстного кодирования