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.
También te puede interesar
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);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);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.
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);
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);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
| Para | Usa |
|---|---|
| Escribir una fila | fputcsv($f, $fila, escape: '') |
| Leer una fila | fgetcsv($f, escape: ''), hasta que devuelva false |
| Filas con nombres | array_combine($cabecera, $fila) |
| Para Excel en español | ; como separador y BOM al principio |
| Archivos enormes | un 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.