C# -  Analisando um arquivo CSV


 Neste artigo veremos como analisar, ou seja, fazer o parse de um arquivo CSV.

Vamos iniciar definindo o que é Parse.

Fazer o parse de um arquivo significa analisar o arquivo ou converter o arquivo para outro formato.

No C# existem várias maneiras de fazer o parse de diferentes tipos de arquivos, desde arquivos de texto simples até formatos mais complexos como JSON, XML, CSV, entre outros.

Neste artigo vamos focar em arquivos CSV.

Um arquivo CSV (Comma Separated Values) é um arquivo texto delimitado que utiliza a vírgula para separar os valores existentes no arquivo, sendo que existem implementações onde outros separadores também podem ser usados.

Os arquivos CSV mais simples não permitem valores que contém vírgula (Ex: Rua Teste, 100) ou outros caracteres especiais como o indicador de nova linha CR ou LF. (Carriage Return/ Line Feed) . Implementações mais sofisticadas permitem vírgulas, ponto e vírgula(;), asterístico(*) como delimitadores e outros caracteres especiais.

Vamos supor que temos o seguinte arquivo CSV chamado 'filmes.csv'  em uma pasta c:\dados :

Titulo, Ano
Pulp Fiction, 1994
Jurassic Park, 1993
Titanic, 1997
A Doce Vida, 1960
A lista de Schindler, 1993
Ghost: Do outro lado da vida, 1990
Uma linda Mulher, 1990
Central do Brasil, 1998
Forrest Gump, 1999
De volta para o futuro, 1985

Para analisar este arquivo manualmente podemos fazer o seguinte:

Criando um projeto console no ambiente do .NET 6 podemos usar o seguinte código para realizar esta tarefa:

Console.WriteLine("## Analisador de arquivos CSV ##\n");
Console.WriteLine("Informe o local do arquivo CSV\t");
var arquivo = Console.ReadLine();

if (string.IsNullOrEmpty(arquivo))
{
    Console.WriteLine("\nArquivo inválido");
    return;
}

AnalisarArquivoCSV(arquivo);

Console.ReadKey();

void AnalisarArquivoCSV(string arquivo)
{
    bool temCabecalho = true;
    foreach (var linha in File.ReadLines(@arquivo))
    {
        if (temCabecalho)
        {
            temCabecalho = false;
            continue;
        }

        var campos = linha.Split(",");

        //Processar os campos
        Console.WriteLine($"filme : {campos[0]} ({campos[1]})");
    }
}

Executando o projeto teremos o seguinte resultado:

A análise manual de um arquivo CSV funciona bem em cenários simples como este. É rápido e o código é direto. Mas assim que você começar a adicionar complexidade (ou seja, detecção de erros, muitas colunas, valores entre aspas potencialmente com vírgulas), sugiro usar uma biblioteca de analisador de arquivos específica.

A plataforma .NET possui um analisador embutido chamado TextFieldParser, mas ele não tem um bom desempenho. Ele abstrai algumas das complexidades ao analisar o CSV, mas não tudo.

Existem diversas opções de analisadores CSV e eu vou mostrar neste artigo o parse CsvHelper.

Usando o CsvHelper

Vamos agora complicar um pouco o arquivo CSV que vamos analisar incluindo as colunas Diretor e Bilheteria em um arquivo chamado filmes2.csv na pasta d:\dados:

Titulo, Ano,Diretor,Bilheteria
Pulp Fiction, 1994,Quentin Tarantino, 838.8
Jurassic Park, 1993,Steven Spielberg, 1046
Titanic, 1997, James Cameron, 2200
Forrest Gump, 1999,Robert Zemeckis,78,5
De volta para o futuro, 1985, Robert Zemeckis, 383,4

Aqui os valores estão em milhares de dólares.

Vamos criar um novo projeto do tipo Console no VS 2022 e incluir o pacote nuget CsvHelper no projeto usando o comando : Install-Package CsvHelper

Ou usar o menu Tools e instalar usando o Manage Packages for Solution:

A seguir vamos incluir no projeto a classe Filme com as seguintes propriedades:

public class Filme
{
    public string? Titulo { get; set; }
    public int Ano { get; set; }
    public string? Diretor { get; set; }
    public decimal Bilheteria { get; set; }
}

Observe que define o nome das propriedades correspondendo aos nomes das colunas no arquivo CSV para facilitar.

Agora vamos criar o código abaixo no arquivo Program:

using CsParse1;
using CsvHelper;
using System.Globalization;

Console.WriteLine("## Analisador de arquivos CSV ##\n");
Console.WriteLine("Informe o local do arquivo CSV\t");
var arquivo = Console.ReadLine();

if (string.IsNullOrEmpty(arquivo))
{
    Console.WriteLine("\nArquivo inválido");
    return;
}

CsvHelperAnalisador(arquivo);

Console.ReadKey();

void CsvHelperAnalisador(string arquivo)
{
    using (var reader = new StreamReader(@arquivo))
    using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
    {
        foreach (var filme in csv.GetRecords<Filme>())
        {
            //Processa os registros desserializados
            Console.WriteLine($"O filme: {filme.Titulo} ({filme.Ano}, {filme.Diretor})
arrecadou ${filme.Bilheteria} milhões de dolares");
        }
    }
}

O CsvHelper requer que você especifique o CultureInfo que deseja usar. A cultura é usada para determinar o delimitador padrão, final de linha padrão e formatação durante a conversão de tipo. Você também pode alterar a configuração de qualquer um deles, se desejar.

O método GetRecords<T> retornará um IEnumerable<T> que vai gerar os registros. O que isso significa é que apenas um único registro é retornado por vez à medida que você itera os registros. Isso também significa que apenas uma pequena parte do arquivo é lida na memória.

Assim esteja atento pois se você definir uma consulta LINQ que use .ToList(), o arquivo inteiro será lido na memória. Além disso como método CsvReader é somente leitura se você quiser executar qualquer consulta LINQ em seus dados, terá que carregar o arquivo inteiro para a memória.

Executando o projeto teremos o seguinte resultado:



Carregando registros em uma coleção

Se você desejar carregar todos os registros em uma coleção, como uma lista, para uso posterior pode usar o GetRecords().ToList() e para tornar o código mais flexível podemos criar o seguinte método :

IEnumerable<T> ParseCsv<T>(string csvFilePath)
{
    using var reader = new StreamReader(csvFilePath);
    using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
    foreach (var registro in csv.GetRecords<T>())
        yield return registro;
}

A seguir podemos converter o resultado em qualquer coleção que desejamos usar no no código de chamada deste método como por exemplo:

var listaFilmes = ParseCsv<Filme>(@arquivo).ToList();

Assim o CsvHelper é uma ótima opção para usar como Parser para CSV sendo muito mais rápido que o TextFieldParser além de possuir outros recursos como um ótimo tratamento de erros.

Pegue o projeto completo aqui: CsParse1.zip

"Já estou crucificado com Cristo; e vivo, não mais eu, mas Cristo vive em mim; e a vida que agora vivo na carne, vivo-a pela fé do Filho de Deus, o qual me amou, e se entregou a si mesmo por mim"
Gálatas 2:20

Referências:


José Carlos Macoratti