Чтение больших текстовых файлов с потоками в C # - PullRequest
84 голосов
/ 29 января 2010

У меня есть прекрасная задача - решить, как обрабатывать большие файлы, загружаемые в редактор сценариев нашего приложения (это похоже на VBA для нашего внутреннего продукта для быстрых макросов). Большинство файлов имеют размер около 300-400 КБ, что вполне нормально. Но когда они выходят за пределы 100 МБ, процессу трудно (как и следовало ожидать).

Что происходит, так это то, что файл читается и помещается в RichTextBox, который затем перемещается - не беспокойтесь об этой части.

Разработчик, написавший исходный код, просто использует StreamReader и выполняет

[Reader].ReadToEnd()

, что может занять некоторое время.

Моя задача - разбить этот бит кода, прочитать его порциями в буфер и показать индикатор выполнения с возможностью отменить его.

Некоторые предположения:

  • Большинство файлов будет 30-40 МБ
  • Содержимое файла - текстовое (не двоичное), некоторые в формате Unix, некоторые в DOS.
  • Как только содержимое получено, мы выясняем, какой терминатор используется.
  • Никого не волнует, когда он загружает время, необходимое для рендеринга в richtextbox. Это просто начальная загрузка текста.

Теперь по вопросам:

  • Могу ли я просто использовать StreamReader, затем проверить свойство Length (например, ProgressMax) и выдать Read для установленного размера буфера и выполнить итерацию в цикле while WHILST внутри фонового работника, так что он не ' заблокировать основной поток пользовательского интерфейса? Затем верните построитель строк в основной поток после его завершения.
  • Содержимое будет отправлено в StringBuilder. я могу инициализировать StringBuilder с размером потока, если длина доступна?

Это (по вашему профессиональному мнению) хорошие идеи? В прошлом у меня было несколько проблем с чтением контента из Streams, потому что он всегда будет пропускать последние несколько байтов или что-то еще, но я задам другой вопрос, если это так.

Ответы [ 11 ]

162 голосов
/ 10 марта 2012

Вы можете улучшить скорость чтения с помощью BufferedStream, например так:

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)
    {

    }
}

Март 2013 г. ОБНОВЛЕНИЕ

Недавно я написал код для чтения и обработки (поиска текста в) текстовых файлов размером 1 ГБ (намного больше, чем файлы, которые здесь используются) и добился значительного прироста производительности благодаря использованию шаблона производителя / потребителя. Задача производителя считала строки текста, используя BufferedStream, и передала их отдельной потребительской задаче, которая выполняла поиск.

Я использовал это как возможность изучить поток данных TPL, который очень хорошо подходит для быстрого кодирования этого шаблона.

Почему BufferedStream работает быстрее

Буфер - это блок байтов в памяти, используемый для кэширования данных, тем самым уменьшая количество обращений к операционной системе. Буферы улучшают производительность чтения и записи. Буфер может использоваться для чтения или записи, но никогда одновременно. Методы Read и Write объекта BufferedStream автоматически поддерживают буфер.

Декабрь 2014 ОБНОВЛЕНИЕ: Ваш пробег может меняться

На основании комментариев FileStream должен использовать BufferedStream для внутреннего использования. Когда этот ответ был впервые предоставлен, я измерил значительное повышение производительности, добавив BufferedStream. В то время я ориентировался на .NET 3.x на 32-битной платформе. Сегодня, ориентируясь на .NET 4.5 на 64-битной платформе, я не вижу никаких улучшений.

Относящиеся

Я сталкивался со случаем, когда потоковая передача большого сгенерированного файла CSV в поток ответов из действия ASP.Net MVC была очень медленной. Добавление BufferedStream улучшило производительность в 100 раз в этом случае. Подробнее см. Небуферизованный вывод Очень медленно

14 голосов
/ 29 января 2010

Вы говорите, что вас просили показать индикатор выполнения во время загрузки большого файла. Это потому, что пользователи искренне хотят видеть точный% загрузки файла или просто потому, что им нужна визуальная обратная связь о том, что что-то происходит?

Если последнее верно, то решение становится намного проще. Просто сделайте reader.ReadToEnd() в фоновом потоке и отобразите индикатор выполнения вместо выделенного.

Я поднимаю этот вопрос, потому что по моему опыту это часто так. Когда вы пишете программу обработки данных, то пользователи определенно будут заинтересованы в% полной информации, но для простых, но медленных обновлений пользовательского интерфейса они, скорее всего, просто захотят узнать, что компьютер не вышел из строя. : -)

13 голосов
/ 19 сентября 2014

Если вы прочитаете статистику производительности и тестов на этом сайте , вы увидите, что самый быстрый способ чтения (потому что чтение, запись и обработка различны) a Текстовый файл представляет собой следующий фрагмент кода:

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

Всего около 9 различных методов были протестированы, но этот, кажется, превосходит большинство времени, даже опережает выполнение буферизованного считывателя , как упоминали другие читатели.

8 голосов
/ 30 сентября 2014

Для двоичных файлов самый быстрый способ их чтения, который я нашел, это:

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

В моих тестах это в сотни раз быстрее.

6 голосов
/ 29 января 2010

Используйте фоновый рабочий и читайте только ограниченное количество строк. Читайте больше только тогда, когда пользователь прокручивает.

И старайтесь никогда не использовать ReadToEnd (). Это одна из функций, которые вы думаете «почему они это сделали?»; это помощник детишек-сценариев , который отлично справляется с мелочами, но, как вы видите, отстой для больших файлов ...

Те парни, которые говорят вам использовать StringBuilder, должны чаще читать MSDN:

Особенности производительности
Методы Concat и AppendFormat объединяют новые данные с существующим объектом String или StringBuilder. Операция конкатенации объекта String всегда создает новый объект из существующей строки и новых данных. Объект StringBuilder поддерживает буфер для размещения конкатенации новых данных. Новые данные добавляются в конец буфера, если доступно пространство; в противном случае выделяется новый больший буфер, данные из исходного буфера копируются в новый буфер, затем новые данные добавляются в новый буфер. Производительность операции конкатенации для объекта String или StringBuilder зависит от того, как часто происходит выделение памяти.
Операция конкатенации String всегда выделяет память, тогда как операция конкатенации StringBuilder выделяет память, только если буфер объекта StringBuilder слишком мал для размещения новых данных. Следовательно, класс String предпочтителен для операции конкатенации, если конкатенируется фиксированное количество объектов String. В этом случае отдельные операции конкатенации могут даже объединяться в одну операцию компилятором. Объект StringBuilder предпочтителен для операции конкатенации, если конкатенируется произвольное количество строк; например, если цикл объединяет случайное количество строк пользовательского ввода.

Это означает огромное выделение памяти, что широко используется системой файлов подкачки, которая имитирует разделы вашего жесткого диска, чтобы действовать как память ОЗУ, но жесткий диск очень медленный.

Опция StringBuilder отлично подходит для тех, кто использует систему в качестве однопользовательского, но когда два или более пользователей одновременно читают большие файлы, у вас возникает проблема.

5 голосов
/ 29 января 2010

Этого должно быть достаточно, чтобы начать работу.

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();
    }
}
4 голосов
/ 29 января 2010

Посмотрите на следующий фрагмент кода. Вы упомянули Most files will be 30-40 MB. Это претендует на чтение 180 МБ за 1,4 секунды на четырехъядерном Intel:

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);
        }
    }
}

Оригинальный артикул

3 голосов
/ 29 января 2010

Возможно, вам лучше использовать обработку файлов, отображаемых в память здесь .. Поддержка файлов, отображаемых в память, будет примерно в .NET 4 (я думаю ... Я слышал, что кто-то еще говорил о это), следовательно, эта обертка, которая использует p / вызывает для выполнения той же работы ..

Редактировать: См. Здесь на MSDN о том, как это работает, вот запись blog , указывающая, как это делается в предстоящем .NET 4, когда он выходит как релиз. Ссылка, которую я дал ранее, является оберткой вокруг pinvoke для достижения этой цели. Вы можете отобразить весь файл в память и просматривать его как скользящее окно при прокрутке файла.

1 голос
/ 09 июля 2010

Итератор может быть идеальным для этого типа работы:

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);
            }
        }
    }
}

Вы можете позвонить, используя следующее:

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();

Когда файл загружен, итератор вернет номер прогресса от 0 до 100, который вы можете использовать для обновления индикатора выполнения. После завершения цикла StringBuilder будет содержать содержимое текстового файла.

Кроме того, поскольку вам нужен текст, мы можем просто использовать BinaryReader для чтения символов, что обеспечит правильное выравнивание ваших буферов при чтении любых многобайтовых символов ( UTF-8 , UTF-16 и др.).

Все это делается без использования фоновых задач, потоков или сложных пользовательских автоматов.

0 голосов
/ 18 августа 2018

Мой файл превышает 13 ГБ: enter image description here

Ссылка ниже содержит код, который легко читает фрагмент файла:

Читать большой текстовый файл

Дополнительная информация

...