Leer y escribir CSV en PHP: fgetcsv y fputcsv

CSV en PHP: el formato más sencillo para intercambiar tablas tiene más trampas de las que parece, sobre todo con comas y comillas dentro de los datos. PHP trae dos funciones que las resuelven, fgetcsv y fputcsv.

Escribir un CSV con fputcsv

<?php
declare(strict_types=1);

$ruta = sys_get_temp_dir() . '/clientes.csv';

$filas = [
    ['nombre', 'ciudad', 'compras'],
    ['Ana López', 'Lima', 3],
    ['Luis "Lucho" Pérez', 'Bogotá, Colombia', 12],
    ['Eva Ñúñez', 'Madrid', 0],
];

$f = fopen($ruta, 'w');
foreach ($filas as $fila) {
    fputcsv($f, $fila, escape: '');
}
fclose($f);

echo file_get_contents($ruta);

Probar en el editor →

salidanombre,ciudad,compras
"Ana López",Lima,3
"Luis ""Lucho"" Pérez","Bogotá, Colombia",12
"Eva Ñúñez",Madrid,0

fputcsv se ocupa de lo difícil: pone entre comillas los campos que tienen comas o comillas, y duplica las comillas de dentro, como pide el formato. Por eso «Bogotá, Colombia» no se parte en dos columnas. El argumento escape: '' desactiva un carácter de escape propio de PHP que no forma parte del estándar CSV; desde PHP 8.4 no pasarlo da un aviso de que el valor por omisión cambiará.

Leer un CSV con fgetcsv

<?php
declare(strict_types=1);

$ruta = sys_get_temp_dir() . '/clientes.csv';
file_put_contents($ruta, "nombre,ciudad,compras\n\"Ana López\",Lima,3\n\"Luis \"\"Lucho\"\" Pérez\",\"Bogotá, Colombia\",12\n");

$f = fopen($ruta, 'r');
$cabecera = fgetcsv($f, escape: '');

while (($fila = fgetcsv($f, escape: '')) !== false) {
    $cliente = array_combine($cabecera, $fila);
    printf("%s (%s): %d compras\n", $cliente['nombre'], $cliente['ciudad'], (int) $cliente['compras']);
}
fclose($f);

Probar en el editor →

salidaAna López (Lima): 3 compras
Luis "Lucho" Pérez (Bogotá, Colombia): 12 compras

La primera llamada a fgetcsv lee la cabecera; array_combine la usa como claves para que cada fila sea un array con nombres. Todos los valores llegan como texto: por eso (int) en las compras. El !== false es necesario porque fgetcsv devuelve false al llegar al final.

No uses explode(',', $linea). Parece más sencillo, pero se rompe con el primer campo que contenga una coma o un salto de línea entre comillas, que en datos reales siempre acaba apareciendo.

Excel, el punto y coma y los acentos

<?php
declare(strict_types=1);

$ruta = sys_get_temp_dir() . '/para-excel.csv';

$f = fopen($ruta, 'w');
fwrite($f, "\xEF\xBB\xBF");
fputcsv($f, ['producto', 'precio'], ';', escape: '');
fputcsv($f, ['Café de Veracruz', number_format(12.5, 2, ',', '.')], ';', escape: '');
fclose($f);

$bytes = file_get_contents($ruta);
echo bin2hex(substr($bytes, 0, 3)), "\n";
echo substr($bytes, 3);

Probar en el editor →

salidaefbbbf
producto;precio
"Café de Veracruz";12,50

Dos detalles para que un CSV se abra bien con doble clic en Excel en español: el separador punto y coma, porque la coma es el separador decimal, y la marca BOM de UTF-8 (los bytes EF BB BF) al principio, sin la cual Excel puede mostrar «Café» en vez de «Café». Para otros programas, mejor sin BOM y con comas.

Archivos grandes, línea a línea

<?php
declare(strict_types=1);

$ruta = sys_get_temp_dir() . '/ventas.csv';
$f = fopen($ruta, 'w');
fputcsv($f, ['dia', 'importe'], escape: '');
for ($i = 1; $i <= 100_000; $i++) {
    fputcsv($f, [$i, $i % 97], escape: '');
}
fclose($f);

function filas(string $ruta): Generator
{
    $f = fopen($ruta, 'r');
    $cabecera = fgetcsv($f, escape: '');
    while (($fila = fgetcsv($f, escape: '')) !== false) {
        yield array_combine($cabecera, $fila);
    }
    fclose($f);
}

$total = 0;
foreach (filas($ruta) as $venta) {
    $total += (int) $venta['importe'];
}
echo "Total: $total\n";
printf("Memoria máxima: %.1f MB\n", memory_get_peak_usage() / 1048576);

Probar en el editor →

salidaTotal: 4799775
Memoria máxima: 0.5 MB

Con un generador (yield), el archivo se lee de fila en fila y nunca está entero en memoria: cien mil filas o diez millones ocupan lo mismo. file() o file_get_contents() cargarían todo de golpe.

Resumen

ParaUsa
Escribir una filafputcsv($f, $fila, escape: '')
Leer una filafgetcsv($f, escape: ''), hasta que devuelva false
Filas con nombresarray_combine($cabecera, $fila)
Para Excel en español; como separador y BOM al principio
Archivos enormesun generador con yield

Todo el código de esta página se ejecutó con PHP 8.5.12 en un contenedor limpio, sin acceso a la red, antes de publicarla; las salidas y los mensajes de error están copiados de esa ejecución.