Abrir Arquivos CSV com PHP

Artigo que explica o que são arquivos CSV (Comma-Separated Values), qual é a sintaxe deste formato de arquivo e como podem ser manipulados pelo PHP.

Introdução

CSV (Comma-Separated Values) é um formato de arquivo texto para armazenamento de dados de forma bastante simples, e agrupada na forma de tabela (planilha). Ele pode ser gerado com qualquer editor de texto puro (TXT), bastando seguir as regras de sintaxe (definidas em RFC 4180).

Este formato é bastante útil para exportação e importação de dados entre sistemas. Você pode, por exemplo, pedir para uma tabela do MySQL ser exportada para CSV e, então, ser importada pelo seu sistema que utiliza outro banco de dados.

Sintaxe do CSV

Todo arquivo CSV define dois caracteres especiais: um "delimitador de valores" e uma "cerca de valor". Normalmente o delimitador é a vírgula e a cerca são as aspas duplas. A cerca serve para envolver o valor de uma célula da planilha (linha x coluna) e o delimitador serve para separar o valor de uma célula de outra.

Cada linha da planilha é delimitada por uma quebra de linha no arquivo CSV, porém, nem toda quebra de linha no arquivo CSV indica uma nova linha da planilha.

A utilização da cerca é obrigatória apenas quando o valor possua quebra de linha, o caractere delimitador ou o caractere cerca internamente. Quando um valor é delimitado por uma cerca e este mesmo caractere está presente no valor, então o caractere do valor deve ser escapado com o caractere de cerca (ficam dois caracteres de cerca em sequencia).

A primeira linha da planilha CSV pode ser o cabeçalho dos campos. Neste caso, é caracterizado um CSV com cabeçalho.

Exemplo de arquivo CSV

Veja uma tabela de exemplo e o respectivo arquivo CSV para representá-la:

nome nome informal ano
Luiz Inácio da Silva Luiz Inácio "Lula" da Silva 2003
Dilma Rousseff Dilma 2011

Arquivo CSV:

nome,nome informal,ano
Luís Inácio da Silva,"Luís Inácio ""Lula"" da Silva",2003
Dilma Rousseff,Dilma,2011

Note que o nome informal do primeiro registro possui o caractere cerca, logo, foi preciso obrigatoriamente delimitar o valor com o caractere cerca e escapar os caracteres cercas internos.

Leitura e Escrita de CSV com PHP

Para leitura e escrita de arquivo CSV com PHP, basta abrir um arquivo para leitura ou escrita normalmente (fopen), mas, ao invés de usar funções como fgets ou fputs, utilizar as funções:

  • fgetcsv - Para ler um registro (uma linha) do arquivo CSV.
  • fputcsv - Para incluir um registro (uma linha) em um arquivo CSV.
<?php
// Exemplo de scrip para exibir os nomes obtidos no arquivo CSV de exemplo

$delimitador = ',';
$cerca = '"';

// Abrir arquivo para leitura
$f = fopen('teste.csv', 'r');
if ($f) { 

    // Ler cabecalho do arquivo
    $cabecalho = fgetcsv($f, 0, $delimitador, $cerca);

    // Enquanto nao terminar o arquivo
    while (!feof($f)) { 

        // Ler uma linha do arquivo
        $linha = fgetcsv($f, 0, $delimitador, $cerca);
        if (!$linha) {
            continue;
        }

        // Montar registro com valores indexados pelo cabecalho
        $registro = array_combine($cabecalho, $linha);

        // Obtendo o nome
        echo $registro['nome'].PHP_EOL;
    }
    fclose($f);
}

Note que foi utilizada a função array_combine apenas para facilitar a localização dos valores do registro lido através do nome usado no cabeçalho do arquivo. Caso o arquivo não tenha cabeçalho, basta acessar a posição numérica do array $linha. Por exemplo: a posição 0 (zero) guarda o nome, a posição 1 guarda o nome informal e a posição 2 guarda o ano.

O segundo parâmetro de fgetcsv é apenas um número para indicar o tamanho máximo do registro, caso exista. Caso não exista, basta usar o valor zero.

O procedimento para gerar um arquivo CSV também é simples:

<?php
$delimitador = ',';
$cerca = '"';

$dados = array(
    array('nome', 'idade'),
    array('Rubens Takiguti Ribeiro', 26),
    array('Teste "da Silva" Sauro', 327)
);

$f = fopen('teste2.csv', 'w');
if ($f) { 
    foreach ($dados as $linha) {
        fputcsv($f, $linha, $delimitador, $cerca);
    }
    fclose($f);
}

Por algum motivo (talvez de compatibilidade), a função fputcsv utiliza o caractere de cerca para delimitar valores que tenham espaço (além dos outros caracteres citados). Porém, isso não prejudica a leitura do arquivo.

Enviando um CSV para o cliente

Sabendo como gerar um CSV, para enviá-lo para o cliente (navegador), basta utilizar o cabeçalho HTTP correto e imprimir o conteúdo do documento. Para isso, é usado o mime-type "text/csv", especificada a codificação do arquivo e, opcionalmente, a especificado se o arquivo possui ou não o cabeçalho. Também podemos especificar se o arquivo será enviado para exibição pelo navegador (disposition inline) ou se será enviado para download (disposition attachment), e colocar uma sugestão de nome de arquivo, conforme exemplo a seguir:

<?php
// Gerando o CSV em $arquivo_csv
...

// Enviando o CSV para o usuario
header('Content-type: text/csv; charset=UTF-8; header=present');
header('Content-Disposition: attachment; filename="' . basename($arquivo_csv) . '"');
readfile($arquivo_csv);
exit(0);

Caso você não queira enviar o conteúdo do CSV para um arquivo físico para só depois imprimí-lo com a função readfile, como no exemplo anterior, você pode ir imprimindo o conteúdo dinamicamente. Para isso, basta chamar a função fopen passando o manipulador de I/O "php://output", como no exemplo abaixo:

<?php
$dados = ...
$cabecalho = ...
$delimitador = ...
$cerca = ...

$f = fopen('php://output', 'w');
if ($f) {
    header('Content-Type: text/csv; charset=UTF-8; header=present');
    header('Content-Disposition: inline; filename="teste.csv"');
    fputcsv($f, $cabecalho, $delimitador, $cerca);
    foreach ($dados as $linha) {
        fputcsv($f, $linha, $delimitador, $cerca);
    }
    fclose($f);
}
exit(0);

Desta forma, ao chamar o fputcsv, o resultado é jogado diretamente para a saída, como se estivéssemos executando um echo, print ou similar.

Compatibilidade de CSV

Sabe-se que o OpenOffice.org abre arquivos CSV corretamente, desde que sejam informados os caracteres utilizados. Porém, algumas versões do MS-Office tem alguns probleminhas, especialmente quanto ao uso opcional do caractere de cerca. Para evitar problemas, basta criar sua própria função para gerar um registro CSV e sempre incluir a cerca:

function myfputcsv($handle, $fields, $delimiter = ',', $enclosure = '"') {
    foreach ($fields as &$field) {
        $field = $enclosure.str_replace($enclosure, $enclosure.$enclosure, $field).$enclosure;
    }
    $line = implode($delimiter, $fields).PHP_EOL;
    return fwrite($handle, $line);
}

Existem, ainda, variações de CSV com outros nomes, que fazem parte do grupo de "Delimiter-Separated Values" (Valores Separados por Delimitador). Por exemplo, o TSV (Tab-Separated Values ou Valores Separador por Tab). A sintaxe é a mesma, só muda o caractere utilizado.

Observação

Conforme foi dito, uma quebra de linha em um arquivo CSV não indica um novo registo. Portanto, o número de quebras de linhas não determina o número de linhas da planilha. Ou seja, nada de usar explode, file, ou coisas do tipo para determinar a quantidade de registros.

Veja um exemplo onde um registro começa em uma linha, mas termina na outra:

frase,autor
"Não sabendo que era impossível,
foi lá e fez.","Jean Cocteau"
"Se queres prever o futuro, estuda o passado.","Confúcio"

Veja que o arquivo possui 4 linhas, mas apenas 2 registros.

27 comentários

Anônimo disse...

Muito bom o material, Mas tem como fazer um select no arquivo csv? como seria?

rubS (autor do blog) disse...

Obrigado.

Usando apenas recursos nativos do PHP, para fazer um SELECT você precisaria percorrer todos os registros e usar alguma condição enquanto percorre.

Porém, existem linguagens que até realizam operações semelhantes com SQL. Por exemplo, a linguagem a PIG.

Dê uma olhada em: http://pig.apache.org/

Denilson Araujo disse...

Estava dando erro na sintaxe do PHP para postar aqui. Agora acho que vai dar certo, vamos lá:

$filename = tempnam(sys_get_temp_dir(), "csv");
$file = fopen($filename,"w");
$conn = pg_connect("host=ip_do_banco port=porta_do_banco dbname=nome_do_banco user=seu_nome_de_usuario password=sua_senha");
$result = pg_query($conn, "select * from sua_tabela;");
for($i = 0; $i < pg_num_fields($result); $i++)
$colunas[] = pg_field_name($result, $i);
fputcsv($file, $colunas, ';', '"');
while($dados = pg_fetch_row($result))
fputcsv($file, $dados, ';', '"');
pg_free_result($result);
pg_close($conn);
fclose($file);
header("Content-Type: application/csv");
header("Content-Disposition: attachment;Filename=nome_do_arquivo.csv");
readfile($filename);
unlink($filename);

rubS (autor do blog) disse...

Obrigado pela contribuição, Denilson. O que eu havia entendido era fazer um SELECT diretamente no CSV. O que você mostrou foi como fazer o SELECT no PostgreSQL e jogar o resultado num arquivo CSV. Não sei o que o "Anônimo" queria, em todo caso, ficam aí as possíveis soluções.

Ricardo disse...

Olá, muito boa a explicação, porém como resolver o que você deu na Observação "Veja um exemplo onde um registro começa em uma linha, mas termina na outra:"

Tenho um campo que um campo tem quebras de linha, como fazer o PHP entender isso direito ?

rubS (autor do blog) disse...

Olá, Ricardo

A função fgetcsv já reconhece isso. Ele nâo lê uma linha do arquivo CSV, mas sim um registro (que pode ocupar mais de uma linha).

Para determinar quantos registros há em um arquivo, você precisa percorrê-lo todo e, a cada chamada de fgetcsv, caso seja devolvido um registro válido, você incrementa uma variável que serve como contador.

Jão Travassos disse...

Olá,
fiz um backup de dados num programa e todo o conteudo veio .csv
exsite um programa ou tipo de planilha que posso abrir esse arquivo e te-lo organizado?
desde ja agradeço a atenção de vcs.
obrigado

rubS (autor do blog) disse...

Olá Jão,

O programa Calc (da suíte LibreOffice.org) ou o Exel (da suíte Microsoft Office) abrem arquivos CSV.

rubS (autor do blog) disse...

Oi Jão,

Ao abrir o CSV, você precisa informar o caractere usado como delimitador de valores (normalmente as áspas duplas) e o caractere usado como separador de colunas (normalmente a vírgula).

Não sei como está o Excel (lembro que ele tinha bugs pra abrir CSV, pois exigia que todas as células de uma coluna que usa aspas usasse aspas). No LibreOffice.org abre numa boa. Experimente baixá-lo. É livre e gratuito.

Marcelo Everson disse...

Excelente artigo Rubens!!

O codigo de leitura está com alguns erros, então tomei a liberdade de mexer nele..blz

<?php

// Exemplo de script para exibir os nomes obtidos no //arquivo CSV de exemplo

$delimiter = ',';
$enclosure = '"';
$register = array();

// Abrir arquivo para leitura
$file = fopen('test.csv', 'r');

if ($file) {

// Ler cabecalho do arquivo
$header = fgetcsv($file, 0, $delimiter, $enclosure);
$numColumnHeader = count($header);

// Enquanto nao terminar o arquivo
while (!feof($file)) {

// Ler uma linha do arquivo
$line = fgetcsv($file, 0, $delimiter, $enclosure);
$numColumnLine = count($line);

// Montar registro com valores indexados pelo cabecalho

if($numColumnLine==$numColumnHeader):

$register[] = array_combine($header, $line);

endif;

}

fclose($file);

}

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Marcelo

Obrigado pela contribuição. Mas, pelo que notei, você só incluiu uma checagem de garantia de que o tamanho do array de cabeçalho tenha o mesmo tamanho dos arrays de dados.

De fato, para usar o array_combine, os tamanhos devem ser iguais. Porém, se os tamanhos são diferentes, quer dizer que o arquivo CSV está com erro de sintaxe (e não o script). Afinal, todos os registros do CSV devem ter a mesma quantidade de campos definidos no cabeçalho.

Marcelo Everson disse...

Olá ,Rubens beleza!

O csv que testei foi o que está postado:

nome,nome informal,ano
Luís Inácio da Silva,"Luís Inácio ""Lula"" da Silva",2003
Dilma Rousseff,Dilma,2011

O que acontece que vamos percorrer todas a linhas ate o final do arquivo:

-> while (!feof($file))

E basta que o arquivo tenha \n depois do ultimo registro e o seu codigo já era!
Não se pode esperar que todos os aquivos estejam do jeito que deseja!E isso não configura um erro no arquivo csv.

Por isso a verificação é obrigatoria!
Além de ao combinar os arrays o registro anterior e sobrescrito ... o que torna o codigo sem muita utilidade

Antes -> $registro = array_combine($cabecalho, $linha);
echo $registro['nome'].PHP_EOL;

Depois-> $register[] = array_combine($header, $line);


Veja que na correção o codigo gera um array(similar ao do FETCH_ALL) com todos os registros e assim pode ser usado um foreach para recuperar os dados em outra parte de uma aplicação.


Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Marcelo
Na verdade o script que coloquei não foi feito para ter utilidade. Foi apenas um exemplo para entender o funcionamento do CSV. Afinal, cada CSV tem seus próprios campos e cada um pode querer usá-lo para um propósito específico.

Se alguém precisa de um vetor de registros como você fez, é só fazer os ajustes como você fez. Porém, lembre-se que jogar tudo num array significa que vai consumir mais memória.

Quanto à sintaxe do arquivo, eu já tinha colocado uma verificação para garantir que o fgetcsv tenha devolvido um array preenchido, caso contrário continua o loop. Ou seja, pode ter uma quebra de linha no final do arquivo, sem problemas. O que não pode é ter mais de uma quebra de linha, pois aí configura um novo registro do arquivo CSV e aí ele está errado mesmo. O formato CSV tem regras e se uma delas é quebrada, eu entendo que o arquivo possui erros de sintaxe.

Não discordo que tratar a integridade do arquivo pode ser importante em vários casos. Mas a ideia ali foi apenas mostrar como ler o CSV e não como validá-lo. Inclusive a validação poderia ser feita em outro método. Afinal, se já validou uma vez, não precisa validar novamente o mesmo arquivo. Enfim, estes detalhes são muito específicos para cada utilização, por isso cada um precisa avaliar o que é melhor para o seu caso.

Rubens Takiguti Ribeiro (autor do blog) disse...

Marcelo, veja este link:
http://tools.ietf.org/html/rfc4180

No capítulo 2 tem as regras de sintaxe do CSV. Lá diz que o último registro do arquivo CSV pode ter uma quebra de linha ou não (apenas uma). Também diz que a linha de cabeçalho é opcional e deve ter a mesma quantidade de campos que os registros do arquivo.

Por isso, acredito que o script que coloquei já seja o suficiente para ler um CSV correto. Mas, de fato, não está preparado para identificar arquivos incorretos.

Aliás, notei que a sua adaptação também não verifica se o retorno devolvido pela função fgetcsv é um array. A função devolve "null" se o handler é invalido ou retorna "false" em caso de erro ou fim de arquivo.

Veja em: http://www.php.net/manual/en/function.fgetcsv.php

gracy ss disse...

Muito bom seu artigo, parabéns! Apenas uma dúvida, você saberia me dizer como eu faço para salvar uma tabela que é gerada por uma busca sql, ser salva em csv? Assim, faz a busca e é gerada uma tabela, dai clica em um botão e salva em csv o resultado.

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Gracy SS.
Se você já tem uma ferramenta que exibe a tabela em HTML, precisa que ela esteja dividida em funções por responsabilidades: uma função para devolver um array com os registros consultados via SQL e uma função para renderizar estes registros (gerar a tabela em HTML). Ou seja, separação de Lógica (Model) e Exibição (View).

Separando desta forma, você só vai precisar fazer uma função de exibição em CSV. Assim, tanto a função que renderiza na forma de tabela HTML quanto a função que renderiza na forma de CSV receberão a mesma coisa (o array de resultados), mas cada uma resultaria numa renderização diferente. Por fim, você precisa criar o botão ou link que vai informar à ferramenta se você quer renderizar de uma forma ou de outra (papel do Controller). Isso pode ser feito passando um parâmetro por GET.

Caso não esteja familiarizada com este tipo de divisão de responsabilidades, recomendo a leitura deste outro artigo sobre MVC:
http://rubsphp.blogspot.com.br/2013/02/mvc-essencia-e-web.html

Se tiver alguma dúvida mais específica com código, é só comentar ou mandar um e-mail.

Yuri Nassar disse...

Boa tarde amigo.
Muito bom seu artigo,esta precisando ter uma ideia de como trabalhar com CSV.
No momento estou tendo problemas com os caracteres especiais(acentuações). Sabe o que posso fazer?
Abaixo estou disponibilizando o código.
Abraços.

$handle= fopen("export.csv","r");
$data = fgetcsv($handle,0,",");





echo "Cabeçalho variável 'DATA'.
";
echo $data[0];

$temp = explode(";",$data[0]);
echo "

";
echo "Variavel 'TEMP' é: .........

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Yuri
As funções fgetcsv e fputcsv independem da codificação do arquivo. Se você está vendo erros de codificação na página gerada, é porque o seu arquivo CSV tem uma codificação e a página que você está exibindo tem outra.

Se o seu arquivo CSV é UTF-8, experimente colocar essa linha no PHP, antes de imprimir qualquer coisa na tela:
header('Content-Type: text/html; charset=UTF-8');

E se o seu arquivo CSV é um ISO-8859-9, experimente colocar essa linha:
header('Content-Type: text/html; charset=ISO-8859-1');

Ou então, use as funções de conversão utf8_encode e utf8_decode.

Para mais detalhes, recomendo a leitura deste artigo:
http://rubsphp.blogspot.com.br/2011/07/problemas-com-charset-nunca-mais.html

Yuri Nassar disse...

Muito grato pela ajuda Rubens. :)
Nesse intervalo eu havia resolvido pela conversão utf8_encode, mas é muito legal pegar essas outras dicas.
Há alguma função que capture o padrão atual? Dessa forma creio que possa fazer algo mais dinâmico, fazer ele se adaptar.

Rubens Takiguti Ribeiro (autor do blog) disse...

Yuri, em se tratando de codificação de caracteres, é impossível fazer uma função que retorne qual a codificação usada em uma string. Isso acontece porque existem codificações onde o mesmo símbolo é representado da mesma forma em bytes. Ou seja, se a string tem apenas esse símbolo, ele tem compatibilidade com mais de uma codificação.

Na minha opinião, cabe ao "criador" daquele texto, documento, etc. saber com qual codificação ele gerou aquilo e informar para os programas que manipulam o texto, para que o programa o manipule adequadamente.

Em todo caso, é possível:
* fazer uma função que verifique se uma string possui (é compatível com) determinada codificação.
* fazer uma função que devolva a codificação de uma string a partir de uma lista de prioridade (ou seja, quando alguma string se enquadra em mais de uma, você informa a prioridade e a função devolve a mais prioritária).

Uma forma de checar se uma string é compatível com UTF-8 é assim:
if (preg_match('/./u', $string)) {

Se quiser se aprofundar no assunto, sugiro que leia esse outro artigo:
http://rubsphp.blogspot.com.br/2010/10/unicode.html

Fernanda Delbello disse...

Oi Rubens,

estou tentando ler um arquivo csv que contem 140 linhas, mas na verdade o meu codigo só está conseguindo ler 15 linhas,
o q está errado, será que vc pode me ajudar?
http://nopaste.info/8e18f976bb.html

Paulo Afonso Jr disse...

Pessoal, boa noite.
Eu preciso saber como eu faço para que o código php gere vários arquivos .txt com o seguinte conteúdo:

Channel: teste1/teste2/081(aqui, sem os (), o número do telefone que está na linha do csv)
Application: Playback
Data: teste
Context: geral

Lembrando que deve ser respeitado o enter.
Lembrando que 1 arquivo para cada número.

Podem me ajudar?
pauloafonsojr@pauloafonsojr.com.br

Rubens Takiguti Ribeiro (autor do blog) disse...

Paulo, neste caso, use as funções file_get_contents e file_put_contents:

$conteudo_arquivo = <<<TXT
Channel: teste1/teste2/081
Application: Playback
Data: teste
Context: geral
TXT;

file_put_contents('arquivo.txt', $conteudo_arquivo);