Como Ler Arquivos CSV Pesados em C# sem Travar a Memória

Como Ler Arquivos CSV Pesados em C# sem Travar a Memória

VocĂȘ jĂĄ viu sua aplicação travar com OutOfMemoryException ao tentar processar um CSV de 200MB? O problema nĂŁo Ă© o C#. É a forma como vocĂȘ estĂĄ lendo o arquivo.

Quem trabalha com integração de sistemas, exportação de relatĂłrios ou importação de dados em massa jĂĄ passou por isso: vocĂȘ precisa processar um arquivo CSV de centenas de megabytes (ou atĂ© gigabytes) e a aplicação simplesmente trava, consome toda a RAM do servidor ou lança a famosa exceção OutOfMemoryException.

O motivo clĂĄssico para isso acontecer Ă© o uso inocente de rotinas que tentam carregar o arquivo inteiro para a memĂłria antes de processar. E eu sei bem como Ă© isso.

Eu particularmente adoro manipular arquivos com programação. Jå falei sobre isso no post Como AplicaçÔes Simples Podem Ser Poderosas, onde mostro C# manipulando arquivos TXT e scripts BAT. Não sei explicar o motivo, mas realmente adoro esse tema de ler arquivos, alterar, incluir dados. Foi o que me motivou a escrever este post.

Neste artigo, vamos entender por que o File.ReadAllLines trava e qual Ă© a abordagem correta usando C# para processar arquivos gigantescos de forma rĂĄpida, estĂĄvel e com consumo de memĂłria praticamente zero.

1. O erro comum: carregar o arquivo inteiro com File.ReadAllLines

Quando precisamos ler um arquivo texto, a primeira ideia costuma ser usar o método File.ReadAllLines():

// EVITE ISSO PARA ARQUIVOS GRANDES!
string[] linhas = File.ReadAllLines(@"C:\dados\relatorio_gigante.csv");

foreach (string linha in linhas)
{
    // Processamento da linha
}

Esse cĂłdigo parece inofensivo, mas tem um problema grave: ele carrega o arquivo inteiro na memĂłria antes de vocĂȘ processar a primeira linha [Stack Overflow].

Em um arquivo de 225MB, por exemplo, o processo pode chegar a consumir mais de 1GB de RAM [Stack Overflow]. Isso acontece porque:

  • O arquivo Ă© lido por completo em um string[];
  • Cada linha vira uma string na memĂłria;
  • Ao mesmo tempo, o encoding e os objetos intermediĂĄrios tambĂ©m ocupam espaço;
  • Se o arquivo tiver 500MB, vocĂȘ pode facilmente estourar 2GB de RAM.

A documentação da Microsoft Ă© clara: "Quando vocĂȘ usa ReadAllLines, vocĂȘ precisa esperar que todo o array de strings seja retornado antes de acessĂĄ-lo. Portanto, ao trabalhar com arquivos muito grandes, ReadLines pode ser mais eficiente" [Stack Overflow].

Quer dominar C# e processamento de dados de forma profissional? Conheça o curso completo:

Curso Completo de C# e .NET

2. A primeira solução: File.ReadLines (lazy loading)

O jeito mais simples de resolver o problema sem trocar quase nada no cĂłdigo Ă© usar File.ReadLines() em vez de File.ReadAllLines().

A diferença crucial: File.ReadLines retorna um IEnumerable<string> que lĂȘ uma linha por vez, sob demanda [Stack Overflow].

// Use ISSO para arquivos grandes
foreach (string linha in File.ReadLines(@"C:\dados\relatorio_gigante.csv"))
{
    // Processamento da linha
    // A memĂłria usada aqui Ă© apenas a da linha atual
}

Com essa mudança simples, o consumo de memória cai de centenas de megabytes para poucos kilobytes [Stack Overflow].

Quando isso nĂŁo Ă© suficiente? Quando o arquivo tem campos com quebras de linha internas. Nesse caso, uma linha "lĂłgica" do CSV pode conter \r\n dentro de um campo entre aspas, e o ReadLines vai dividir errado [Stack Overflow].

3. A solução definitiva: StreamReader linha a linha

Para ter controle total sobre a leitura, encoding e buffer, o StreamReader Ă© a escolha profissional. Ele lĂȘ o arquivo em blocos (buffer) e mantĂ©m apenas o necessĂĄrio em memĂłria.

using (var reader = new StreamReader(@"C:\dados\relatorio_gigante.csv"))
{
    string linha;
    while ((linha = reader.ReadLine()) != null)
    {
        // Processamento da linha
        // Apenas a linha atual estĂĄ na memĂłria
    }
}

E a versĂŁo assĂ­ncrona, para nĂŁo bloquear a thread principal:

using (var reader = new StreamReader(@"C:\dados\relatorio_gigante.csv"))
{
    string linha;
    while ((linha = await reader.ReadLineAsync()) != null)
    {
        // Processamento assĂ­ncrono
    }
}

A documentação da Microsoft destaca que o StreamReader é a forma mais eficiente de ler arquivos grandes, pois trabalha com streaming em vez de carregar tudo de uma vez [Stack Overflow].

Vantagens do StreamReader:

  • Controle de encoding: vocĂȘ define UTF-8, GB2312, Latin1, etc.;
  • Buffer configurĂĄvel: pode ajustar o tamanho do buffer para otimizar I/O;
  • Async nativo: ReadLineAsync() libera a thread durante a leitura;
  • MemĂłria mĂ­nima: apenas o buffer e a linha atual ocupam espaço.

4. E quando o CSV tem aspas, vĂ­rgulas e quebras de linha internas?

Se o seu CSV Ă© "bagunçado" — com campos entre aspas, vĂ­rgulas dentro de valores ou quebras de linha internas — aĂ­ o Split(',') nĂŁo resolve. VocĂȘ precisa de um parser de verdade.

A biblioteca mais popular do ecossistema .NET é o CsvHelper. Mas atenção: usar CsvHelper do jeito errado também causa OutOfMemoryException [php.cn].

O erro clĂĄssico Ă© chamar ReadRecords<T>(), que carrega todo o arquivo na memĂłria antes de retornar [php.cn].

// EVITE: ReadRecords carrega tudo na memĂłria
var records = csv.GetRecords<MeuModelo>().ToList();

A forma correta Ă© usar GetRecord<T>() dentro de um loop com Read() explĂ­cito [php.cn]:

using (var reader = new StreamReader("input.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
    // LĂȘ o header primeiro
    csv.Read();
    csv.ReadHeader();

    // Agora lĂȘ linha por linha
    while (csv.Read())
    {
        var record = csv.GetRecord<MeuModelo>();
        // Processamento
    }
}

Um detalhe importante: em versĂ”es mais antigas do CsvHelper, a checagem de ShouldSkipRecord fazia o parser materializar a linha duas vezes — uma para checar e outra para vocĂȘ ler. Isso dobrava o consumo de memĂłria [GitHub].

A boa notĂ­cia Ă© que isso foi corrigido. Se vocĂȘ estĂĄ usando uma versĂŁo recente, o impacto Ă© mĂ­nimo. Se estiver preso a uma versĂŁo antiga, saiba que isso pode ser o gargalo.

Quer se aprofundar em processamento de dados com .NET? Confira este material:

E-book: Processamento de Dados em C#

5. FlameCsv: a alternativa de alta performance (e baixa alocação)

Se vocĂȘ precisa de performance extrema — estamos falando de ler mĂșltiplos gigabytes por segundo — vale conhecer o FlameCsv.

Segundo a própria documentação, o FlameCsv foi construído do zero com Span<T>, SIMD e MemoryPool para oferecer "near-zero allocations" [NuGet].

O que isso significa na prĂĄtica?

  • Zero pressĂŁo no Garbage Collector para arquivos enormes;
  • Parsing com aceleração por hardware (AVX2/SSE2);
  • Suporte a UTF-8 nativo, sem conversĂŁo intermediĂĄria;
  • Leitura e escrita assĂ­ncronas.

O cĂłdigo Ă© simples:

using FlameCsv;

await foreach (var record in CsvReader.ReadAsync<MeuModelo>("dados.csv"))
{
    // Processamento com alocação próxima de zero
}

Quando usar FlameCsv? Quando o volume de dados Ă© tĂŁo grande que o CsvHelper jĂĄ nĂŁo dĂĄ conta. Para a maioria dos casos do dia a dia, o CsvHelper Ă© suficiente. Mas se vocĂȘ estĂĄ processando milhĂ”es de linhas diariamente, vale testar.

6. Como medir o consumo real de memória da sua aplicação

NĂŁo adianta sĂł confiar no gerenciador de tarefas. Em C#, vocĂȘ pode medir o consumo real da sua aplicação com a classe Process [Learn Microsoft].

A propriedade PeakWorkingSet64 retorna o pico de memĂłria fĂ­sica (em bytes) usado pelo processo desde que ele foi iniciado [Learn Microsoft].

using System.Diagnostics;

var processo = Process.GetCurrentProcess();
long picoMemoriaBytes = processo.PeakWorkingSet64;
double picoMemoriaMB = picoMemoriaBytes / (1024.0 * 1024.0);

Console.WriteLine($"Pico de memĂłria: {picoMemoriaMB:F2} MB");

Como usar isso no seu teste:

  1. Meça o pico usando File.ReadAllLines com um CSV de 200MB;
  2. Meça o pico usando File.ReadLines com o mesmo arquivo;
  3. Meça o pico usando StreamReader;
  4. Compare os trĂȘs resultados.

VocĂȘ vai ver uma diferença brutal. O ReadAllLines pode consumir 500MB a 1GB, enquanto o StreamReader fica na casa dos kilobytes.

7. Resumo: qual abordagem usar em cada caso

NĂŁo existe "bala de prata", mas existe a abordagem certa para cada cenĂĄrio:

CenĂĄrio Abordagem MemĂłria
Arquivo pequeno (< 10MB) File.ReadAllLines OK
Arquivo médio (< 100MB) File.ReadLines Baixa
Arquivo grande (> 100MB) StreamReader MĂ­nima
CSV complexo (aspas, vĂ­rgulas) CsvHelper com GetRecord Baixa
Performance extrema (GB/s) FlameCsv Quase zero

A regra de ouro Ă© simples: nunca carregue um arquivo inteiro na memĂłria se vocĂȘ pode processar linha a linha. O File.ReadAllLines Ă© uma armadilha para arquivos grandes, e o StreamReader Ă© seu melhor amigo.

E aĂ­, vocĂȘ jĂĄ passou por esse problema? Qual foi a solução que vocĂȘ adotou? Conta pra gente nos comentĂĄrios.

Quer dominar C# e .NET de verdade?

No Nerd Cult, a gente descomplica o desenvolvimento — da leitura de arquivos Ă  arquitetura de sistemas. Assine nossa newsletter e receba conteĂșdos prĂĄticos direto no seu e-mail.

QUERO RECEBER OS GUIAS

BÎnus: Checklist de performance para aplicaçÔes .NET

Nerd Cult — onde o cĂłdigo encontra o rock, o cinema e a cultura geek. Porque ser nerd Ă© transformar o medo em curiosidade, e a curiosidade em poder.

#CSharp #DotNet #CSV #Performance #StreamReader #NerdCult

Este conteĂșdo foi Ăștil para vocĂȘ?

ComentĂĄrios (0)

Nenhum comentĂĄrio ainda. Seja o primeiro a comentar!