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".
- 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 avisoDeprecateddo 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 queexplodenã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:
- A primeira linha é o cabeçalho: os nomes das colunas.
- 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,5viraria dois campos. Por isso, as planilhas em português salvam CSV com;. Em inglês, onde o decimal é9.5, o comum é,. - Um nome tem aspas dentro (
"Didi"). Aspas são especiais em CSV, e a gente vai ver como tratá-las.
| nome | turma | nota |
|---|---|---|
| Ana Souza | 3A | 9,5 |
| Bruno Lima | 3A | 7 |
| Carla Dias | 3B | 8,25 |
| Diego "Didi" Alves | 3B | 6,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 ver1000ou0). - ';'
- 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,
fgetcsvdevolvefalse, e owhilepara (mesma ideia dofgets).
$ 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
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";
| Linha | O 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
(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 diastem 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
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ção | Use |
|---|---|
| Ler um arquivo CSV, linha a linha | fopen + fgetcsv num while |
| Gravar um arquivo CSV | fopen(…, 'w') + fputcsv para cada linha |
| Uma linha CSV que já está numa string | str_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 "Didi" Alves</td> « as aspas viraram "
<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 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: '').
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.
[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; }.
$ 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ção | O 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:
2. Cabeçalho como chaves
Complete para transformar a linha num array associativo:
3. Gravar uma linha
Complete para gravar $linha no arquivo $f, separando com ;:
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, ';', '"', '')numwhile (… !== false); grave comfputcsv; uma linha solta, comstr_getcsv. - Passe sempre o
$escape(''): sem ele, o PHP 8.4+ mostraDeprecated. 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.