Leer y escribir CSV en C++: comillas, comas dentro de los campos y conversión segura

Leer un CSV en C++ parece cosa de getline y partir por comas, pero eso falla en cuanto un campo trae una coma, unas comillas o un salto de línea. Aquí tienes un lector corto que respeta las reglas de la RFC 4180, la conversión de los campos con control de errores y cómo escribir un CSV que cualquier programa abra bien.

Un CSV parece fácil: una línea por fila y comas entre los campos. Hasta que un campo lleva una coma, unas comillas o un salto de línea. Este es el archivo de ejemplo, productos.csv, con los tres casos y un precio mal escrito:

cat productos.csv
salidacodigo,nombre,precio,notas
A-1,Teclado,24.90,
A-2,"Ratón, inalámbrico",19.50,"El modelo ""Pro"""
A-3,"Monitor 27""",249.00,"Envío gratis
solo en península"
A-4,Cable USB-C,abc,precio mal escrito

El atajo que no funciona: getline y partir por comas

#include <fstream>
#include <print>
#include <sstream>
#include <string>

int main()
{
    std::ifstream f("productos.csv");
    std::string linea;
    while (std::getline(f, linea)) {
        std::istringstream partes(linea);
        std::string campo;
        int n = 0;
        while (std::getline(partes, campo, ',')) ++n;
        std::println("{} campos: {}", n, linea);
    }
}
salida (incorrecta)4 campos: codigo,nombre,precio,notas
3 campos: A-1,Teclado,24.90,
5 campos: A-2,"Ratón, inalámbrico",19.50,"El modelo ""Pro"""
4 campos: A-3,"Monitor 27""",249.00,"Envío gratis
1 campos: solo en península"
4 campos: A-4,Cable USB-C,abc,precio mal escrito

Tres fallos en seis líneas: el teclado sale con 3 campos porque getline no devuelve el último campo cuando está vacío, el ratón sale con 5 porque la coma de «Ratón, inalámbrico» se toma como separador, y el monitor se parte en dos filas porque su nota lleva un salto de línea. Partir por comas solo vale si controlas los datos y sabes que nunca traerán comas ni comillas.

Un lector que respeta las comillas

Las reglas de las comillas están en la RFC 4180, que es lo más parecido a un estándar que tiene el CSV: un campo entre comillas puede contener comas y saltos de línea, y una comilla dentro del campo se escribe doble (""). La forma más sencilla de cumplirlas es leer carácter a carácter y recordar si estamos dentro de unas comillas. Lo guardamos en csv.hpp:

#include <fstream>
#include <print>
#include <string>
#include <vector>

using Fila = std::vector<std::string>;

// Lector de CSV según la RFC 4180: campos entre comillas con comas, saltos de línea y comillas dobladas ("").
// Devuelve false al llegar al final sin leer nada.
bool leer_fila(std::istream& in, Fila& fila, char sep = ',')
{
    fila.clear();
    std::string campo;
    bool entre_comillas = false, algo = false;
    char c;
    while (in.get(c)) {
        algo = true;
        if (entre_comillas) {
            if (c == '"') {
                if (in.peek() == '"') {
                    campo += '"';
                    in.get();
                } else {
                    entre_comillas = false;
                }
            } else {
                campo += c;
            }
        } else if (c == '"' && campo.empty()) {   // solo abre comillas al principio del campo
            entre_comillas = true;
        } else if (c == sep) {
            fila.push_back(std::move(campo));
            campo.clear();
        } else if (c == '\n') {
            break;
        } else if (c != '\r') {
            campo += c;
        }
    }
    if (!algo) return false;
    fila.push_back(std::move(campo));
    return true;
}
#include "csv.hpp"

int main()
{
    std::ifstream f("productos.csv");
    Fila fila;
    while (leer_fila(f, fila)) {
        std::println("{} campos: {}", fila.size(), fila);
    }
}
salida4 campos: ["codigo", "nombre", "precio", "notas"]
4 campos: ["A-1", "Teclado", "24.90", ""]
4 campos: ["A-2", "Ratón, inalámbrico", "19.50", "El modelo \"Pro\""]
4 campos: ["A-3", "Monitor 27\"", "249.00", "Envío gratis\nsolo en península"]
4 campos: ["A-4", "Cable USB-C", "abc", "precio mal escrito"]

Ahora cada producto tiene sus 4 campos, el ratón conserva su coma, las comillas dobladas se convierten en una sola y la nota del monitor mantiene su salto de línea (std::println imprime los vectores con el formato de C++23, que muestra el \n escapado). El lector también ignora los \r de los archivos creados en Windows.

De texto a datos, con control de errores

Los campos llegan como texto. Para convertir el precio, std::from_chars es la opción más estricta: no lanza excepciones, no depende del idioma del sistema y dice si sobró algo sin convertir.

#include "csv.hpp"
#include <charconv>

struct Producto {
    std::string codigo, nombre;
    double precio;
};

int main()
{
    std::ifstream f("productos.csv");
    Fila fila;
    leer_fila(f, fila);                          // la cabecera
    std::vector<Producto> productos;
    int numero = 1;
    while (leer_fila(f, fila)) {
        ++numero;
        if (fila.size() != 4) {
            std::println("fila {}: tiene {} campos, se esperaban 4", numero, fila.size());
            continue;
        }
        double precio;
        const std::string& p = fila[2];
        auto [fin, error] = std::from_chars(p.data(), p.data() + p.size(), precio);
        if (error != std::errc{} || fin != p.data() + p.size()) {
            std::println("fila {}: precio no válido «{}»", numero, p);
            continue;
        }
        productos.push_back({fila[0], fila[1], precio});
    }
    double total = 0;
    for (const auto& pr : productos) {
        std::println("{:<4} {:<20} {:>8.2f}", pr.codigo, pr.nombre, pr.precio);
        total += pr.precio;
    }
    std::println("{} productos válidos, total {:.2f}", productos.size(), total);
}
salidafila 5: precio no válido «abc»
A-1  Teclado                 24.90
A-2  Ratón, inalámbrico      19.50
A-3  Monitor 27"            249.00
3 productos válidos, total 293.40

Fíjate en que la fila del error se cuenta como fila del archivo, no como línea: el monitor ocupa dos líneas, pero es una sola fila. Si el usuario abre el CSV en una hoja de cálculo, el número coincidirá con lo que ve.

Escribir CSV válido

Al escribir hay que hacer lo contrario: poner entre comillas los campos que llevan separador, comillas o saltos de línea, y doblar sus comillas.

#include <fstream>
#include <iterator>
#include <print>
#include <string>
#include <vector>

std::string campo_csv(const std::string& s, char sep = ',')
{
    if (s.find_first_of(std::string{sep} + "\"\r\n") == std::string::npos) {
        return s;
    }
    std::string r = "\"";
    for (char c : s) {
        if (c == '"') r += '"';
        r += c;
    }
    return r + '"';
}

void escribir_fila(std::ostream& out, const std::vector<std::string>& fila, char sep = ',')
{
    for (std::size_t i = 0; i < fila.size(); ++i) {
        if (i > 0) out << sep;
        out << campo_csv(fila[i], sep);
    }
    out << "\r\n";              // la RFC 4180 usa CRLF; casi todos los programas aceptan los dos
}

int main()
{
    std::ofstream f("salida.csv", std::ios::binary);
    escribir_fila(f, {"nombre", "comentario"});
    escribir_fila(f, {"Ana", "Dijo \"hola\", y se fue"});
    escribir_fila(f, {"Luis", "dos\nlíneas"});
    escribir_fila(f, {"Eva", ""});
    f.close();

    std::ifstream leido("salida.csv");
    std::println("{}", std::string(std::istreambuf_iterator<char>(leido), {}));
}
salidanombre,comentario
Ana,"Dijo ""hola"", y se fue"
Luis,"dos
líneas"
Eva,

Errores típicos con CSV

  • ProblemaPor qué ocurreQué hacer
  • Comas dentro de un campoPartir con getline(…, ',') no entiende las comillasUn lector como leer_fila o una biblioteca
  • Excel en español no lo abre bienUsa ; como separador, porque la coma es el separador decimalPasar ';' como separador al leer y al escribir
  • Acentos rotos al abrirlo en ExcelExcel en Windows no supone UTF-8Escribir al principio la marca BOM: "\xEF\xBB\xBF"
  • Números con coma decimalfrom_chars solo acepta el puntoCambiar la coma por punto antes de convertir
  • Fórmulas en los datosUn campo que empieza por =, +, - o @ se ejecuta como fórmula en la hoja de cálculoSi el CSV lo abrirá otra persona, anteponer una comilla simple a esos campos

El último punto es un problema de seguridad real, la inyección de fórmulas en CSV: si tu programa exporta datos que escribieron otros usuarios, alguien puede colar una fórmula que se ejecute en el ordenador de quien abra el archivo. Para proyectos grandes, una biblioteca probada como fast-cpp-csv-parser o rapidcsv te ahorra estos detalles.

Todo el código de esta página se compiló con GCC 15.3 (C++23, -std=c++23 -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.