Leer y escribir archivos CSV en C sin bibliotecas externas: por qué strtok no sirve en cuanto un campo lleva una coma, un lector de campos que respeta las comillas, cómo convertir los números con seguridad y cómo escribir un CSV que cualquier hoja de cálculo abra bien.
También te puede interesar
Un CSV parece fácil de leer: separar cada línea por las comas. Pero en cuanto un campo lleva una coma dentro, va entre comillas, y si lleva comillas, se escriben dobles. Este es el archivo del ejemplo, ventas.csv:
cat ventas.csv
salidaproducto,cantidad,precio Café de Veracruz,3,189.50 "Taza, cerámica",2,95.00 Filtros de papel,10,12.75 "Té ""chai""",1,140.00
Por qué no basta con strtok
#include <stdio.h>
#include <string.h>
int main(void)
{
char linea[] = "\"Taza, cerámica\",2,95.00";
int n = 0;
for (char *campo = strtok(linea, ","); campo != nullptr; campo = strtok(nullptr, ",")) {
printf("campo %d: [%s]\n", ++n, campo);
}
return 0;
}salidacampo 1: ["Taza] campo 2: [ cerámica"] campo 3: [2] campo 4: [95.00]
strtok parte por cada coma, incluida la que está dentro de las comillas, y deja las comillas en el texto. Además salta los campos vacíos (a,,b le da dos campos, no tres) y modifica la cadena original. Para CSV de verdad hace falta un pequeño analizador.
Un lector de CSV que respeta las comillas
Este lee un campo cada vez, siguiendo las reglas del formato (RFC 4180): un campo entre comillas puede contener comas, y "" dentro de él es una comilla. Copia el campo en un búfer con límite y avisa si no cabe:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Lee el siguiente campo de la línea a partir de *p y lo copia en destino (tam bytes).
Devuelve false si el campo no cabe. Deja *p en el siguiente campo, o en nullptr al acabar la línea. */
static bool siguiente_campo(const char **p, char *destino, size_t tam)
{
const char *s = *p;
size_t n = 0;
bool cabe = true;
bool entre_comillas = *s == '"';
if (entre_comillas) s++;
for (; *s != '\0'; s++) {
if (entre_comillas && *s == '"') {
if (s[1] == '"') {
s++;
} else {
entre_comillas = false;
continue;
}
} else if (!entre_comillas && (*s == ',' || *s == '\n' || *s == '\r')) {
break;
}
if (n + 1 < tam) destino[n++] = *s;
else cabe = false;
}
destino[n] = '\0';
*p = (*s == ',') ? s + 1 : nullptr;
return cabe;
}
int main(void)
{
FILE *f = fopen("ventas.csv", "r");
if (f == nullptr) {
perror("ventas.csv");
return EXIT_FAILURE;
}
char linea[512];
double total = 0;
int fila = 0;
while (fgets(linea, sizeof linea, f) != nullptr) {
if (++fila == 1) continue; /* la cabecera */
char producto[64], cantidad[16], precio[16];
const char *p = linea;
if (!siguiente_campo(&p, producto, sizeof producto) || p == nullptr ||
!siguiente_campo(&p, cantidad, sizeof cantidad) || p == nullptr ||
!siguiente_campo(&p, precio, sizeof precio)) {
fprintf(stderr, "Fila %d mal formada\n", fila);
continue;
}
char *fin1, *fin2;
long c = strtol(cantidad, &fin1, 10);
double pr = strtod(precio, &fin2);
if (*fin1 != '\0' || *fin2 != '\0') {
fprintf(stderr, "Fila %d: número no válido\n", fila);
continue;
}
printf("%-20s %3ld x %8.2f = %9.2f\n", producto, c, pr, c * pr);
total += c * pr;
}
fclose(f);
printf("Total: %.2f\n", total);
return 0;
}salidaCafé de Veracruz 3 x 189.50 = 568.50 Taza, cerámica 2 x 95.00 = 190.00 Filtros de papel 10 x 12.75 = 127.50 Té "chai" 1 x 140.00 = 140.00 Total: 1026.00
Las comas de «Taza, cerámica» y las comillas de «chai» salen bien. Los números se convierten con strtol y strtod comprobando que se consumió todo el campo, para rechazar un 12abc en vez de tomarlo por 12. Las columnas con acentos se descuadran un poco porque %-20s cuenta bytes, no letras, como vimos en la lección 4.
Una limitación de este lector: no admite saltos de línea dentro de un campo entre comillas, que el formato sí permite. Para eso habría que leer carácter a carácter con fgetc en lugar de por líneas. Si tu programa recibe CSV de hojas de cálculo, una biblioteca como libcsv lo resuelve todo.
Escribir CSV
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Escribe un campo, entre comillas solo si hace falta, duplicando las comillas de dentro. */
static void campo(FILE *f, const char *s)
{
if (strpbrk(s, ",\"\n") == nullptr) {
fputs(s, f);
return;
}
fputc('"', f);
for (; *s != '\0'; s++) {
if (*s == '"') fputc('"', f);
fputc(*s, f);
}
fputc('"', f);
}
int main(void)
{
const char *clientes[][2] = {
{"Ana López", "Calle 5, número 12"},
{"Bar \"El Puerto\"", "Avenida del Mar 3"},
};
FILE *f = fopen("clientes.csv", "w");
if (f == nullptr) {
perror("clientes.csv");
return EXIT_FAILURE;
}
fputs("nombre,direccion\n", f);
for (int i = 0; i < 2; i++) {
campo(f, clientes[i][0]);
fputc(',', f);
campo(f, clientes[i][1]);
fputc('\n', f);
}
if (fclose(f) != 0) {
perror("clientes.csv");
return EXIT_FAILURE;
}
f = fopen("clientes.csv", "r");
if (f == nullptr) return EXIT_FAILURE;
char linea[256];
while (fgets(linea, sizeof linea, f) != nullptr) fputs(linea, stdout);
fclose(f);
return 0;
}salidanombre,direccion Ana López,"Calle 5, número 12" "Bar ""El Puerto""",Avenida del Mar 3
Escribir es la operación inversa: comillas solo si el campo contiene una coma, una comilla o un salto de línea, y las comillas de dentro duplicadas. strpbrk busca cualquiera de esos caracteres de una vez.
Todo el código de esta página se compiló con GCC 15.3 (C23, -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.