PHP CSV

CSV é o formato mais simples de planilha: um arquivo de texto em que cada linha é uma linha da tabela e os campos são separados por um caractere, como ; ou ,. Toda planilha (Excel, LibreOffice Calc, Google Planilhas) abre e salva CSV, e muitos sistemas exportam dados assim: extratos de banco, listas de alunos, cadastros de produtos. Nesta aula o PHP vai ler e gravar CSV do jeito certo, inclusive os casos que quebram as soluções "caseiras".

Você vai aprender
  • Como é um arquivo CSV por dentro, e por que no Brasil ele costuma usar ;.
  • Ler linha a linha com fgetcsv, e por que passar os quatro argumentos (o aviso Deprecated do PHP 8.4+).
  • Usar a primeira linha como cabeçalho, com array_combine.
  • Converter notas e preços com vírgula decimal (9,5) em números.
  • Gravar com fputcsv, que cuida das aspas sozinho.
  • Quebrar uma única linha com str_getcsv, e por que explode não serve.
  • Gerar uma tabela HTML a partir de um CSV.

Esta aula usa o que você viu em Ler e Escrever Arquivos (fopen, fgets, fclose). CSV quer dizer Comma-Separated Values, "valores separados por vírgula".


A ideia: uma planilha em texto puro

A Ana dá aulas de reforço e guarda as notas da turma numa planilha. Ela salvou a planilha como CSV, e o arquivo alunos.csv ficou assim:

$ cd csv
$ cat alunos.csv
nome;turma;nota
Ana Souza;3A;9,5
Bruno Lima;3A;7
Carla Dias;3B;8,25
Diego "Didi" Alves;3B;6,5

Repare em três coisas:

  1. A primeira linha é o cabeçalho: os nomes das colunas.
  2. O separador é o ponto e vírgula. Por quê? Porque no Brasil a vírgula já é o separador decimal (9,5). Se o separador fosse a vírgula, 9,5 viraria dois campos. Por isso, as planilhas em português salvam CSV com ;. Em inglês, onde o decimal é 9.5, o comum é ,.
  3. Um nome tem aspas dentro ("Didi"). Aspas são especiais em CSV, e a gente vai ver como tratá-las.
nometurmanota
Ana Souza3A9,5
Bruno Lima3A7
Carla Dias3B8,25
Diego "Didi" Alves3B6,5

Ler uma linha com fgetcsv

fgetcsv ("pegar CSV do arquivo") funciona como o fgets: lê a próxima linha de um arquivo aberto com fopen. A diferença é que ela já devolve a linha quebrada em campos, num array. Primeiro, a tentativa mais curta:

<?php
$f = fopen('alunos.csv', 'r');
$linha = fgetcsv($f, null, ';');
var_dump($linha);
fclose($f);
$ php ler1.php

Deprecated: fgetcsv(): the $escape parameter must be provided as its default value will change in /home/ana/csv/ler1.php on line 3
array(3) {
  [0]=>
  string(4) "nome"
  [1]=>
  string(5) "turma"
  [2]=>
  string(4) "nota"
}

Funcionou (o cabeçalho veio em três campos), mas com um aviso Deprecated ("obsoleto"): "o parâmetro $escape precisa ser informado, porque o valor padrão dele vai mudar". Desde o PHP 8.4+, as funções de CSV pedem que você diga explicitamente qual é o caractere de escape. A resposta recomendada é '' (nenhum), que faz o PHP seguir o padrão oficial do CSV, o mesmo das planilhas. Então a chamada completa tem quatro argumentos depois do arquivo:

<?php
$f = fopen('alunos.csv', 'r');
while (($campos = fgetcsv($f, null, ';', '"', '')) !== false) {
    echo implode(' | ', $campos), "\n";
}
fclose($f);
$f
O arquivo aberto com fopen(…, 'r').
null
O tamanho máximo da linha. null = sem limite (em código antigo você vai ver 1000 ou 0).
';'
O separador de campos. O padrão do PHP é ','; para planilhas brasileiras, ';'.
'"'
O caractere que envolve campos com caracteres especiais (aspas duplas, o padrão).
''
O caractere de escape: nenhum. Dentro de um campo entre aspas, uma aspa se escreve dobrada (""), como manda o padrão.
!== false
No fim do arquivo, fgetcsv devolve false, e o while para (mesma ideia do fgets).
$ php ler2.php
nome | turma | nota
Ana Souza | 3A | 9,5
Bruno Lima | 3A | 7
Carla Dias | 3B | 8,25
Diego "Didi" Alves | 3B | 6,5
Com argumentos nomeados fica mais legível

Desde o PHP 8.0+, você pode dizer o nome do argumento (veja Parâmetros) e pular os que quer deixar no padrão. Esta linha faz o mesmo, sem o aviso:

while (($campos = fgetcsv($f, separator: ';', escape: '')) !== false) {

E se o separador estiver errado?

Se você ler um CSV de ; dizendo que o separador é ,, não há erro: a linha inteira vira um campo só.

$ php ler-virgula.php     « fgetcsv($f, null, ',', '"', '')
array(1) {
  [0]=>
  string(15) "nome;turma;nota"
}

Esse é o sintoma clássico: array(1) quando você esperava várias colunas. Abra o arquivo com cat (ou num editor de texto) e confira qual é o separador.

Missão: mostre cada aluno no formato Nome: nota (por exemplo, Ana Souza: 9,5), sem mostrar a linha do cabeçalho e sem nenhum aviso Deprecated. Dica: leia o cabeçalho com um fgetcsv antes do laço e ignore o resultado.

<?php
$f = fopen('alunos.csv', 'r');
while (($campos = fgetcsv($f, null, ';')) !== false) {
    echo implode(' | ', $campos), "\n";
}
fclose($f);
nome;turma;nota
Ana Souza;3A;9,5
Bruno Lima;3A;7
Carla Dias;3B;8,25
Diego "Didi" Alves;3B;6,5
<?php
$f = fopen('alunos.csv', 'r');
fgetcsv($f, null, ';', '"', '');          // pula o cabeçalho
while (($campos = fgetcsv($f, null, ';', '"', '')) !== false) {
    echo $campos[0] . ": " . $campos[2] . "\n";
}
fclose($f);

Usar o cabeçalho: array_combine

Escrever $campos[2] funciona, mas quem lê o código não sabe que o 2 é a nota, e se alguém mudar a ordem das colunas na planilha, o programa quebra em silêncio. Melhor transformar cada linha num array associativo, usando o cabeçalho como chaves. A função array_combine ("combinar arrays") faz isso: o primeiro array dá as chaves, o segundo dá os valores.

<?php
$f = fopen('alunos.csv', 'r');
$cabecalho = fgetcsv($f, null, ';', '"', '');
$alunos = [];
while (($campos = fgetcsv($f, null, ';', '"', '')) !== false) {
    $alunos[] = array_combine($cabecalho, $campos);
}
fclose($f);
print_r($alunos[0]);
$soma = 0;
foreach ($alunos as $a) {
    $nota = (float) str_replace(',', '.', $a['nota']);
    $soma += $nota;
    printf("%-20s %s %5.2f\n", $a['nome'], $a['turma'], $nota);
}
echo "Média: " . number_format($soma / count($alunos), 2, ',', '.') . "\n";
LinhaO que faz
$cabecalho = fgetcsv(…);A primeira linha: ['nome', 'turma', 'nota'].
$alunos[] = array_combine($cabecalho, $campos);Junta ['nome','turma','nota'] com ['Ana Souza','3A','9,5'] e acrescenta ['nome' => 'Ana Souza', 'turma' => '3A', 'nota' => '9,5'] à lista.
str_replace(',', '.', $a['nota'])Tudo o que vem de um CSV é texto. O PHP só entende números com ponto, então trocamos a vírgula: '9,5' → '9.5'.
(float)Converte o texto '9.5' no número 9.5.
printf("%-20s %s %5.2f\n", …)Alinha em colunas: nome em 20 posições à esquerda, nota com 2 casas (veja printf).
number_format(…, 2, ',', '.')Mostra a média do jeito brasileiro (veja Formatar Números).
$ php cabecalho.php
Array
(
    [nome] => Ana Souza
    [turma] => 3A
    [nota] => 9,5            « ainda é texto, com vírgula
)
Ana Souza            3A  9.50       « printf usa ponto, sempre
Bruno Lima           3A  7.00
Carla Dias           3B  8.25
Diego "Didi" Alves   3B  6.50
Média: 7,81                         « number_format com vírgula
Não faça (float) '9,5'

A conversão do PHP lê o começo do texto até onde der e para na vírgula: (float) '9,5' vale 9 (o var_dump mostra float(9)), sem aviso nenhum. As médias ficariam erradas e ninguém perceberia. Troque a vírgula por ponto antes.


Gravar com fputcsv

fputcsv ("pôr CSV no arquivo") recebe um array e grava uma linha, com os separadores e as aspas no lugar certo. A Ana quer exportar a lista de produtos da lojinha:

<?php
$produtos = [
    ['produto', 'preco', 'obs'],
    ['Bolo de cenoura', '35,00', 'com cobertura'],
    ['Torta "da vó"', '48,50', 'sob encomenda; 2 dias'],
    ['Brigadeiro', '2,50', ''],
];
$f = fopen('produtos.csv', 'w');
foreach ($produtos as $linha) {
    fputcsv($f, $linha, ';', '"', '');
}
fclose($f);
echo file_get_contents('produtos.csv');
$ php escrever.php
produto;preco;obs
"Bolo de cenoura";35,00;"com cobertura"
"Torta ""da vó""";48,50;"sob encomenda; 2 dias"
Brigadeiro;2,50;

Veja o que o fputcsv decidiu sozinho:

  • Campos com espaço foram colocados entre aspas ("Bolo de cenoura"). Não era obrigatório, mas não faz mal: toda planilha entende.
  • sob encomenda; 2 dias tem um ; dentro. Entre aspas, ele não separa campos.
  • Torta "da vó" tem aspas dentro. Elas foram dobradas (""da vó"") e o campo foi envolvido por aspas. É assim que o CSV "escapa" aspas.
  • O campo vazio virou nada depois do último ;.

Com o separador vírgula (para um sistema em inglês), é o mesmo, trocando o terceiro argumento. Note que aí os preços precisam usar ponto:

$ php escrever-virg.php    « fputcsv($f, ['Torta, de limão', '48.50'], ',', '"', '')
"Bolo de cenoura",35.00
"Torta, de limão",48.50
Acentos no Excel: o BOM

O LibreOffice e o Google Planilhas abrem CSV em UTF-8 sem problema. O Excel, no Windows, às vezes mostra Pão no lugar de Pão. O truque é gravar, antes de tudo, a "marca de ordem de bytes" (BOM), três bytes invisíveis que avisam "isto é UTF-8":

<?php
$f = fopen('para-excel.csv', 'w');
fwrite($f, "\u{FEFF}");              // o BOM
fputcsv($f, ['produto', 'preço'], ';', '"', '');
fputcsv($f, ['Pão de mel', '4,00'], ';', '"', '');
fclose($f);
$ php bom.php
$ head -c 16 para-excel.csv | od -c | head -2
0000000 357 273 277   p   r   o   d   u   t   o   ;   p   r   e 303 247
0000020

Os três primeiros bytes (357 273 277, em octal) são o BOM. Ao ler um CSV que veio do Excel com BOM, o primeiro nome do cabeçalho chega como "\u{FEFF}nome"; tire com ltrim($cabecalho[0], "\u{FEFF}").


Uma linha só: str_getcsv

Quando a linha CSV já está numa variável (veio de um formulário, de uma API, de file()), use str_getcsv ("CSV de uma string"). Os argumentos são os mesmos, sem o arquivo:

<?php
$linha = 'Torta "da vó";48,50;"sob encomenda; 2 dias"';
print_r(str_getcsv($linha, ';', '"', ''));
$linha2 = '"Torta ""da vó""";48,50';
print_r(str_getcsv($linha2, ';', '"', ''));
$ php strget.php
Array
(
    [0] => Torta "da vó"
    [1] => 48,50
    [2] => sob encomenda; 2 dias        « o ; entre aspas não separou
)
Array
(
    [0] => Torta "da vó"                « as aspas dobradas voltaram a ser uma
    [1] => 48,50
)

Por que não usar explode(';', $linha)?

Parece igual, e funciona até o dia em que um campo tem ; ou aspas:

$ php explode.php      « print_r(explode(';', '"Torta ""da vó""";48,50;"sob encomenda; 2 dias"'));
Array
(
    [0] => "Torta ""da vó"""        « as aspas ficaram
    [1] => 48,50
    [2] => "sob encomenda           « um campo virou dois
    [3] =>  2 dias"
)

explode corta em todo ;, sem saber nada de aspas. Para CSV, use sempre as funções de CSV.

SituaçãoUse
Ler um arquivo CSV, linha a linhafopen + fgetcsv num while
Gravar um arquivo CSVfopen(…, 'w') + fputcsv para cada linha
Uma linha CSV que já está numa stringstr_getcsv
Texto simples, sem aspas possíveis (como "a,b,c" que você mesmo montou)explode serve

Do CSV para uma tabela HTML (modo web)

Agora o CSV vira página: um cabeçalho <th> com os nomes das colunas e uma linha <tr> por aluno. Cada valor passa por htmlspecialchars, porque o CSV pode ter vindo de qualquer lugar (e o Diego tem aspas no nome).

<?php
$f = fopen(__DIR__ . '/alunos.csv', 'r');
$cabecalho = fgetcsv($f, null, ';', '"', '');
?>
<table border="1">
  <tr>
<?php foreach ($cabecalho as $titulo): ?>
    <th><?= htmlspecialchars(ucfirst($titulo)) ?></th>
<?php endforeach; ?>
  </tr>
<?php while (($campos = fgetcsv($f, null, ';', '"', '')) !== false): ?>
  <tr>
<?php foreach ($campos as $valor): ?>
    <td><?= htmlspecialchars($valor) ?></td>
<?php endforeach; ?>
  </tr>
<?php endwhile; fclose($f); ?>
</table>
$ php tabela.php
<table border="1">
  <tr>
    <th>Nome</th>
    <th>Turma</th>
    <th>Nota</th>
  </tr>
  <tr>
    <td>Ana Souza</td>
    <td>3A</td>
    <td>9,5</td>
  </tr>
  …                                       « (Bruno e Carla)
  <tr>
    <td>Diego &quot;Didi&quot; Alves</td>    « as aspas viraram &quot;
    <td>3B</td>
    <td>6,5</td>
  </tr>
</table>

O código usa a sintaxe alternativa (foreach (…): … endforeach;), que fica mais legível no meio do HTML.

Missão: a tabela já mostra o CSV. Acrescente uma quarta coluna, Situação, que mostra aprovado quando a nota for 7 ou mais e recuperação quando for menor. Lembre de converter a nota ('6,5') em número antes de comparar. Veja o resultado na aba Resultado.

<?php
$f = fopen(__DIR__ . '/alunos.csv', 'r');
$cabecalho = fgetcsv($f, null, ';', '"', '');
?>
<table border="1">
  <tr>
<?php foreach ($cabecalho as $titulo): ?>
    <th><?= htmlspecialchars(ucfirst($titulo)) ?></th>
<?php endforeach; ?>
  </tr>
<?php while (($campos = fgetcsv($f, null, ';', '"', '')) !== false): ?>
  <tr>
<?php foreach ($campos as $valor): ?>
    <td><?= htmlspecialchars($valor) ?></td>
<?php endforeach; ?>
  </tr>
<?php endwhile; fclose($f); ?>
</table>
nome;turma;nota
Ana Souza;3A;9,5
Bruno Lima;3A;7
Carla Dias;3B;8,25
Diego "Didi" Alves;3B;6,5
<?php
$f = fopen(__DIR__ . '/alunos.csv', 'r');
$cabecalho = fgetcsv($f, null, ';', '"', '');
?>
<table border="1">
  <tr>
<?php foreach ($cabecalho as $titulo): ?>
    <th><?= htmlspecialchars(ucfirst($titulo)) ?></th>
<?php endforeach; ?>
    <th>Situação</th>
  </tr>
<?php while (($campos = fgetcsv($f, null, ';', '"', '')) !== false): ?>
<?php $nota = (float) str_replace(',', '.', $campos[2]); ?>
  <tr>
<?php foreach ($campos as $valor): ?>
    <td><?= htmlspecialchars($valor) ?></td>
<?php endforeach; ?>
    <td><?= $nota >= 7 ? 'aprovado' : 'recuperação' ?></td>
  </tr>
<?php endwhile; fclose($f); ?>
</table>

Quando dá errado

"Deprecated: fgetcsv(): the $escape parameter must be provided"
Deprecated: fgetcsv(): the $escape parameter must be provided as its default value will change in /home/ana/csv/ler1.php on line 3

O que significa: desde o PHP 8.4, chamar fgetcsv, fputcsv ou str_getcsv sem dizer o caractere de escape gera este aviso (o programa continua). O mesmo vale para str_getcsv: Deprecated: str_getcsv(): the $escape parameter must be provided as its default value will change.

Como consertar: passe os quatro argumentos, fgetcsv($f, null, ';', '"', ''), ou use nomes: fgetcsv($f, separator: ';', escape: '').

Tudo numa coluna só (array(1))

O que significa: o separador informado não é o do arquivo (leu com , um CSV de ;, ou o contrário).

Como consertar: olhe o arquivo com cat ou head -3 e use o separador certo.

Linha em branco vira [null]
$ php vazia.php       « o arquivo tem uma linha vazia entre Ana e Bia
…
array(2) {
  [0]=>
  string(3) "Ana"
  [1]=>
  string(1) "9"
}
array(1) {
  [0]=>
  NULL                « a linha vazia
}
array(2) {
  [0]=>
  string(3) "Bia"
…

O que significa: para uma linha em branco, fgetcsv devolve um array com um único null. Se você fizer array_combine com ela, vem o erro do próximo cenário.

Como consertar: pule essas linhas no começo do laço: if ($campos === [null]) { continue; }.

"array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements"
$ php combine-erro.php     « cabeçalho com 3 colunas, linha com só 2 campos

Fatal error: Uncaught ValueError: array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements in /home/ana/csv/combine-erro.php:4
Stack trace:
#0 /home/ana/csv/combine-erro.php(4): array_combine(Array, Array)
#1 {main}
  thrown in /home/ana/csv/combine-erro.php on line 4

O que significa: uma linha do CSV tem mais ou menos campos que o cabeçalho (um campo faltando, um ; a mais, uma linha vazia).

Como consertar: confira antes: if (count($campos) !== count($cabecalho)) { echo "Linha com problema\n"; continue; }. Para saber o número da linha problemática, conte as linhas no laço.


Referência

FunçãoO que faz
fgetcsv($f, null, ';', '"', '')Lê a próxima linha do arquivo e devolve os campos num array (false no fim; [null] numa linha vazia).
fputcsv($f, $array, ';', '"', '')Grava o array como uma linha CSV (põe aspas e dobra as aspas internas quando preciso). Devolve o número de bytes.
str_getcsv($texto, ';', '"', '')Quebra uma linha CSV que está numa string.
array_combine($chaves, $valores)Junta cabeçalho e campos num array associativo.
str_replace(',', '.', $t) + (float)Converte '9,5' em 9.5.
"\u{FEFF}"O BOM do UTF-8, para o Excel reconhecer os acentos.

Exercícios

1. O separador brasileiro

Complete para ler um CSV exportado de uma planilha em português:

$campos = fgetcsv($f, null, '', '"', '');

2. Cabeçalho como chaves

Complete para transformar a linha num array associativo:

$aluno = ($cabecalho, $campos);

3. Gravar uma linha

Complete para gravar $linha no arquivo $f, separando com ;:

($f, $linha, ';', '"', '');

4. Como o campo Torta "da vó" aparece no arquivo gravado por fputcsv?

5. Quanto vale (float) '8,25'?

6. Por que explode(';', $linha) não serve para ler CSV?

Missão final: leia o alunos.csv e grave um novo arquivo, aprovados.csv, com o mesmo cabeçalho e só os alunos com nota 7 ou mais (separador ;). No fim, mostre o conteúdo de aprovados.csv com file_get_contents (os nomes vão aparecer entre aspas, porque têm espaço: é o fputcsv trabalhando). Nenhum Deprecated pode aparecer.

<?php
$entrada = fopen(__DIR__ . '/alunos.csv', 'r');
$saida = fopen(__DIR__ . '/aprovados.csv', 'w');

nome;turma;nota
Ana Souza;3A;9,5
Bruno Lima;3A;7
Carla Dias;3B;8,25
Diego "Didi" Alves;3B;6,5
<?php
$entrada = fopen(__DIR__ . '/alunos.csv', 'r');
$saida = fopen(__DIR__ . '/aprovados.csv', 'w');

$cabecalho = fgetcsv($entrada, null, ';', '"', '');
fputcsv($saida, $cabecalho, ';', '"', '');

while (($campos = fgetcsv($entrada, null, ';', '"', '')) !== false) {
    $aluno = array_combine($cabecalho, $campos);
    $nota = (float) str_replace(',', '.', $aluno['nota']);
    if ($nota >= 7) {
        fputcsv($saida, $campos, ';', '"', '');
    }
}
fclose($entrada);
fclose($saida);

echo file_get_contents(__DIR__ . '/aprovados.csv');

Resumo

  • CSV é uma planilha em texto: uma linha por registro, campos separados por ; (Brasil) ou ,, primeira linha com o cabeçalho.
  • Leia com fgetcsv($f, null, ';', '"', '') num while (… !== false); grave com fputcsv; uma linha solta, com str_getcsv.
  • Passe sempre o $escape (''): sem ele, o PHP 8.4+ mostra Deprecated.
  • array_combine($cabecalho, $campos) dá nomes às colunas.
  • Tudo vem como texto: troque a vírgula decimal por ponto antes de converter para número.
  • As funções de CSV cuidam das aspas (dobradas) e dos separadores dentro dos campos; explode, não.
  • Ao mostrar dados do CSV numa página, use htmlspecialchars.

Próxima aula: JSON, o formato de dados da web, que guarda listas e estruturas inteiras num arquivo.