Lendo grandes arquivos de texto com fluxos em C #

96

Eu tenho a adorável tarefa de descobrir como lidar com arquivos grandes sendo carregados no editor de script de nosso aplicativo (é como o VBA para nosso produto interno para macros rápidas). A maioria dos arquivos tem cerca de 300-400 KB, o que é um bom carregamento. Mas, quando ultrapassam 100 MB, o processo passa por momentos difíceis (como seria de esperar).

O que acontece é que o arquivo é lido e colocado em um RichTextBox que é navegado - não se preocupe muito com esta parte.

O desenvolvedor que escreveu o código inicial está simplesmente usando um StreamReader e fazendo

[Reader].ReadToEnd()

que pode demorar um pouco para ser concluído.

Minha tarefa é dividir esse trecho de código, lê-lo em partes em um buffer e mostrar uma barra de progresso com uma opção para cancelá-lo.

Algumas suposições:

  • A maioria dos arquivos terá 30-40 MB
  • O conteúdo do arquivo é texto (não binário), alguns em formato Unix, outros em DOS.
  • Depois que o conteúdo é recuperado, descobrimos qual terminador é usado.
  • Ninguém se preocupa depois que ele é carregado, o tempo que leva para renderizar na caixa de texto rico. É apenas o carregamento inicial do texto.

Agora, para as perguntas:

  • Posso simplesmente usar StreamReader, verificar a propriedade Length (portanto, ProgressMax) e emitir um Read para um tamanho de buffer definido e iterar em um loop while WHILST dentro de um trabalhador de segundo plano, de modo que não bloqueie o thread de IU principal? Em seguida, retorne o stringbuilder para o thread principal quando estiver concluído.
  • O conteúdo irá para um StringBuilder. posso inicializar o StringBuilder com o tamanho do fluxo se o comprimento estiver disponível?

Estas são (na sua opinião profissional) boas ideias? Eu tive alguns problemas no passado com a leitura de conteúdo do Streams, porque sempre perderá os últimos bytes ou algo assim, mas farei outra pergunta se for o caso.

Nicole Lee
fonte
29
Arquivos de script de 30-40 MB? Santo cavala! Eu odiaria ter que revisar o código que ...
dthorpe
Eu sei que essa pergunta é um pouco antiga, mas eu a encontrei outro dia e testei a recomendação para MemoryMappedFile e este é sem dúvida o método mais rápido. Uma comparação é ler um arquivo de 345 MB de 7.616.939 linhas por meio de um método readline que leva mais de 12 horas na minha máquina, enquanto a execução do mesmo carregamento e a leitura por meio de MemoryMappedFile levam 3 segundos.
csonon
São apenas algumas linhas de código. Veja esta biblioteca que estou usando para ler arquivos 25gb e mais grandes também. github.com/Agenty/FileReader
Vikash Rathee

Respostas:

175

Você pode melhorar a velocidade de leitura usando um BufferedStream, como este:

using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {

    }
}

ATUALIZAÇÃO de março de 2013

Recentemente, escrevi código para leitura e processamento (pesquisa de texto em) arquivos de texto de 1 GB (muito maiores do que os arquivos envolvidos aqui) e obtive um ganho de desempenho significativo usando um padrão produtor / consumidor. A tarefa do produtor lia linhas de texto usando o BufferedStreame os entregava a uma tarefa separada do consumidor que fazia a pesquisa.

Usei isso como uma oportunidade de aprender TPL Dataflow, que é muito adequado para codificar rapidamente esse padrão.

Por que BufferedStream é mais rápido

Um buffer é um bloco de bytes na memória usado para armazenar dados em cache, reduzindo assim o número de chamadas para o sistema operacional. Os buffers melhoram o desempenho de leitura e gravação. Um buffer pode ser usado para leitura ou escrita, mas nunca para ambos simultaneamente. Os métodos Read e Write de BufferedStream mantêm automaticamente o buffer.

ATUALIZAÇÃO DE dezembro de 2014: sua milhagem pode variar

Com base nos comentários, FileStream deve estar usando um BufferedStream internamente. No momento em que esta resposta foi fornecida pela primeira vez, medi um aumento significativo no desempenho adicionando um BufferedStream. Na época, eu tinha como alvo o .NET 3.x em uma plataforma de 32 bits. Hoje, visando o .NET 4.5 em uma plataforma de 64 bits, não vejo nenhuma melhoria.

Relacionados

Eu me deparei com um caso em que o streaming de um grande arquivo CSV gerado para o stream de resposta de uma ação ASP.Net MVC era muito lento. Adicionar um BufferedStream melhorou o desempenho em 100x nesta instância. Para obter mais informações, consulte Saída sem buffer muito lenta

Eric J.
fonte
12
Cara, BufferedStream faz toda a diferença. +1 :)
Marcus
2
Há um custo para solicitar dados de um subsistema IO. No caso de discos giratórios, você pode ter que esperar que o prato gire na posição para ler o próximo bloco de dados, ou pior, esperar que a cabeça do disco se mova. Embora os SSDs não tenham peças mecânicas para desacelerar as coisas, ainda há um custo por operação de E / S para acessá-los. Os fluxos em buffer leem mais do que apenas as solicitações do StreamReader, reduzindo o número de chamadas para o sistema operacional e, por fim, o número de solicitações de E / S separadas.
Eric J.
4
Realmente? Isso não faz diferença no meu cenário de teste. De acordo com Brad Abrams, não há benefício em usar BufferedStream em um FileStream.
Nick Cox
2
@NickCox: Seus resultados podem variar com base em seu subsistema IO subjacente. Em um disco giratório e um controlador de disco que não tem os dados em seu cache (e também dados não armazenados em cache pelo Windows), a aceleração é enorme. A coluna de Brad foi escrita em 2004. Eu medi melhorias reais e drásticas recentemente.
Eric J.
3
Isso é inútil de acordo com: stackoverflow.com/questions/492283/… FileStream já usa um buffer internamente.
Erwin Mayer
21

Se você ler as estatísticas de desempenho e benchmark neste site , verá que a maneira mais rápida de ler (porque ler, escrever e processar são diferentes) um arquivo de texto é o seguinte trecho de código:

using (StreamReader sr = File.OpenText(fileName))
{
    string s = String.Empty;
    while ((s = sr.ReadLine()) != null)
    {
        //do your stuff here
    }
}

Todos os cerca de 9 métodos diferentes foram marcados, mas aquele parece sair na frente na maioria das vezes, até mesmo desempenhando o leitor bufferizado como outros leitores mencionaram.


fonte
2
Isso funcionou bem para separar um arquivo postgres de 19 GB para convertê-lo em sintaxe sql em vários arquivos. Obrigado cara postgres que nunca executou meus parâmetros corretamente. / suspiro
Damon Drake
A diferença de desempenho aqui parece compensar para arquivos realmente grandes, como maiores do que 150 MB (também você realmente deve usar um StringBuilderpara carregá-los na memória, carrega mais rápido, pois não cria uma nova string cada vez que você adiciona caracteres)
Joshua G
15

Você diz que foi solicitado a mostrar uma barra de progresso enquanto um arquivo grande está sendo carregado. Isso é porque os usuários realmente desejam ver a% exata de carregamento do arquivo ou apenas porque desejam um feedback visual de que algo está acontecendo?

Se o último for verdadeiro, a solução se torna muito mais simples. Apenas façareader.ReadToEnd() em um thread de segundo plano e exibir uma barra de progresso do tipo letreiro em vez de uma adequada.

Levanto esse ponto porque, em minha experiência, costuma ser esse o caso. Quando você está escrevendo um programa de processamento de dados, os usuários definitivamente estarão interessados ​​em um número% completo, mas para atualizações de IU simples, mas lentas, é mais provável que eles apenas queiram saber se o computador não travou. :-)

Christian Hayter
fonte
2
Mas o usuário pode cancelar a chamada ReadToEnd?
Tim Scarborough
@Tim, bem localizado. Nesse caso, estamos de volta ao StreamReaderloop. No entanto, ainda será mais simples porque não há necessidade de ler adiante para calcular o indicador de progresso.
Christian Hayter
8

Para arquivos binários, a maneira mais rápida de lê-los que encontrei é esta.

 MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
 MemoryMappedViewStream mms = mmf.CreateViewStream();
 using (BinaryReader b = new BinaryReader(mms))
 {
 }

Em meus testes, é centenas de vezes mais rápido.

Inoxidável
fonte
2
Você tem alguma evidência concreta disso? Por que o OP deve usar isso em vez de qualquer outra resposta? Pesquise um pouco mais a fundo e forneça mais detalhes
Dylan Corriveau
7

Use um trabalhador de segundo plano e leia apenas um número limitado de linhas. Leia mais apenas quando o usuário rolar.

E tente nunca usar ReadToEnd (). É uma das funções que você pensa "por que eles fizeram isso?"; é um script kiddies ' ajudante de que vai bem com pequenas coisas, mas como você vê, é péssimo para arquivos grandes ...

Aqueles caras que estão dizendo para você usar StringBuilder precisam ler o MSDN com mais frequência:

Considerações de desempenho
Os métodos Concat e AppendFormat concatenam novos dados a um objeto String ou StringBuilder existente. Uma operação de concatenação de objeto String sempre cria um novo objeto a partir da string existente e dos novos dados. Um objeto StringBuilder mantém um buffer para acomodar a concatenação de novos dados. Novos dados são acrescentados ao final do buffer se houver espaço disponível; caso contrário, um novo buffer maior é alocado, os dados do buffer original são copiados para o novo buffer e, em seguida, os novos dados são anexados ao novo buffer. O desempenho de uma operação de concatenação para um objeto String ou StringBuilder depende da frequência com que ocorre uma alocação de memória.
Uma operação de concatenação String sempre aloca memória, enquanto uma operação de concatenação StringBuilder aloca memória apenas se o buffer do objeto StringBuilder for muito pequeno para acomodar os novos dados. Conseqüentemente, a classe String é preferível para uma operação de concatenação se um número fixo de objetos String for concatenado. Nesse caso, as operações de concatenação individuais podem até ser combinadas em uma única operação pelo compilador. Um objeto StringBuilder é preferível para uma operação de concatenação se um número arbitrário de strings for concatenado; por exemplo, se um loop concatena um número aleatório de strings de entrada do usuário.

Isso significa uma enorme alocação de memória, o que se torna um grande uso de sistema de arquivos de swap, que simula seções do seu disco rígido para agirem como a memória RAM, mas um disco rígido é muito lento.

A opção StringBuilder parece boa para quem usa o sistema como um único usuário, mas quando você tem dois ou mais usuários lendo arquivos grandes ao mesmo tempo, você tem um problema.

Tufo
fonte
de longe vocês são super rápidos! infelizmente, devido à forma como a macro funciona, todo o fluxo precisa ser carregado. Como mencionei, não se preocupe com a parte do richtext. É o carregamento inicial que queremos melhorar.
Nicole Lee
então você pode trabalhar em partes, ler as primeiras X linhas, aplicar a macro, ler as segundas X linhas, aplicar a macro, e assim por diante ... se você explicar o que esta macro faz, podemos ajudá-lo com mais precisão
Tufo
5

Isso deve ser o suficiente para você começar.

class Program
{        
    static void Main(String[] args)
    {
        const int bufferSize = 1024;

        var sb = new StringBuilder();
        var buffer = new Char[bufferSize];
        var length = 0L;
        var totalRead = 0L;
        var count = bufferSize; 

        using (var sr = new StreamReader(@"C:\Temp\file.txt"))
        {
            length = sr.BaseStream.Length;               
            while (count > 0)
            {                    
                count = sr.Read(buffer, 0, bufferSize);
                sb.Append(buffer, 0, count);
                totalRead += count;
            }                
        }

        Console.ReadKey();
    }
}
ChaosPandion
fonte
4
Eu moveria o "var buffer = new char [1024]" do loop: não é necessário criar um novo buffer a cada vez. Basta colocá-lo antes de "while (count> 0)".
Tommy Carlier
4

Dê uma olhada no seguinte trecho de código. Você mencionou Most files will be 30-40 MB. Este afirma ler 180 MB em 1,4 segundos em um Intel Quad Core:

private int _bufferSize = 16384;

private void ReadFile(string filename)
{
    StringBuilder stringBuilder = new StringBuilder();
    FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);

    using (StreamReader streamReader = new StreamReader(fileStream))
    {
        char[] fileContents = new char[_bufferSize];
        int charsRead = streamReader.Read(fileContents, 0, _bufferSize);

        // Can't do much with 0 bytes
        if (charsRead == 0)
            throw new Exception("File is 0 bytes");

        while (charsRead > 0)
        {
            stringBuilder.Append(fileContents);
            charsRead = streamReader.Read(fileContents, 0, _bufferSize);
        }
    }
}

Artigo original

James
fonte
3
Esses tipos de testes são notoriamente não confiáveis. Você lerá os dados do cache do sistema de arquivos ao repetir o teste. Isso é pelo menos uma ordem de magnitude mais rápido do que um teste real que lê os dados do disco. Um arquivo de 180 MB não pode levar menos de 3 segundos. Reinicie sua máquina, execute o teste uma vez para o número real.
Hans Passant
7
a linha stringBuilder.Append é potencialmente perigosa, você precisa substituí-la por stringBuilder.Append (fileContents, 0, charsRead); para garantir que você não está adicionando 1024 caracteres completos, mesmo quando o fluxo foi encerrado antes.
Johannes Rudolph
@JohannesRudolph, seu comentário acabou de me resolver um bug. Como você chegou ao número 1024?
HeyJude
3

Talvez seja melhor usar arquivos mapeados em memória aqui . O suporte a arquivos mapeados em memória estará disponível no .NET 4 (acho ... ouvi isso por meio de outra pessoa falando sobre isso), por isso este wrapper que usa p / invoca para fazer o mesmo trabalho ..

Edit: Veja aqui no MSDN para saber como funciona, aqui está a entrada do blog que indica como isso é feito no próximo .NET 4 quando ele for lançado. O link que forneci anteriormente é um invólucro em torno da pinvoke para conseguir isso. Você pode mapear o arquivo inteiro na memória e visualizá-lo como uma janela deslizante ao rolar pelo arquivo.

t0mm13b
fonte
2

Todas as respostas excelentes! no entanto, para quem procura uma resposta, elas parecem um tanto incompletas.

Como uma String padrão só pode ter tamanho X, 2 Gb a 4 Gb dependendo da sua configuração, essas respostas não atendem realmente à pergunta do OP. Um método é trabalhar com uma Lista de Strings:

List<string> Words = new List<string>();

using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
{

string line = string.Empty;

while ((line = sr.ReadLine()) != null)
{
    Words.Add(line);
}
}

Alguns podem querer tokenizar e dividir a linha durante o processamento. A String List agora pode conter grandes volumes de texto.

Prego enferrujado
fonte
1

Um iterador pode ser perfeito para este tipo de trabalho:

public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
{
    const int charBufferSize = 4096;
    using (FileStream fs = File.OpenRead(filename))
    {
        using (BinaryReader br = new BinaryReader(fs))
        {
            long length = fs.Length;
            int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
            double iter = 100 / Convert.ToDouble(numberOfChunks);
            double currentIter = 0;
            yield return Convert.ToInt32(currentIter);
            while (true)
            {
                char[] buffer = br.ReadChars(charBufferSize);
                if (buffer.Length == 0) break;
                stringData.Append(buffer);
                currentIter += iter;
                yield return Convert.ToInt32(currentIter);
            }
        }
    }
}

Você pode chamá-lo usando o seguinte:

string filename = "C:\\myfile.txt";
StringBuilder sb = new StringBuilder();
foreach (int progress in LoadFileWithProgress(filename, sb))
{
    // Update your progress counter here!
}
string fileData = sb.ToString();

Conforme o arquivo é carregado, o iterador retornará o número de progresso de 0 a 100, que você pode usar para atualizar sua barra de progresso. Assim que o loop terminar, o StringBuilder conterá o conteúdo do arquivo de texto.

Além disso, como você deseja texto, podemos apenas usar o BinaryReader para ler os caracteres, o que garantirá que seus buffers se alinhem corretamente ao ler quaisquer caracteres multibyte ( UTF-8 , UTF-16 , etc.).

Tudo isso é feito sem o uso de tarefas em segundo plano, threads ou máquinas de estado personalizadas complexas.

Extremeswank
fonte