Чтение больших текстовых файлов с потоками в C#
У меня есть прекрасная задача - решить, как обрабатывать большие файлы, загружаемые в редактор сценариев нашего приложения (это как VBA для нашего внутреннего продукта для быстрых макросов). Большинство файлов имеют размер около 300-400 КБ, что вполне нормально. Но когда они выходят за пределы 100 МБ, процессу приходится нелегко (как и следовало ожидать).
Что происходит, так это то, что файл читается и помещается в RichTextBox, который затем перемещается - не беспокойтесь об этой части.
Разработчик, который написал исходный код, просто использует StreamReader и делает
[Reader].ReadToEnd()
что может занять некоторое время, чтобы завершить.
Моя задача состоит в том, чтобы разбить этот бит кода, прочитать его порциями в буфер и показать индикатор выполнения с возможностью отменить его.
Некоторые предположения:
- Большинство файлов будет 30-40 МБ
- Содержимое файла - текстовое (не двоичное), некоторые в формате Unix, некоторые в DOS.
- Как только содержимое получено, мы выясняем, какой терминатор используется.
- Никого не волнует, как только он загрузит время рендеринга в richtextbox. Это просто начальная загрузка текста.
Теперь по вопросам:
- Могу ли я просто использовать StreamReader, затем проверить свойство Length (то есть ProgressMax) и выполнить чтение для установленного размера буфера и выполнить итерацию в цикле while WHILST внутри фонового рабочего процесса, чтобы он не блокировал основной поток пользовательского интерфейса? Затем верните построитель строк в основной поток после его завершения.
- Содержимое будет отправлено в StringBuilder. я могу инициализировать StringBuilder с размером потока, если длина доступна?
Это (по вашему профессиональному мнению) хорошие идеи? В прошлом у меня было несколько проблем с чтением контента из Streams, потому что он всегда будет пропускать последние несколько байтов или что-то в этом роде, но я задам другой вопрос, если это так.
14 ответов
Вы можете улучшить скорость чтения с помощью 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 в поток Response из действия ASP.Net MVC была очень медленной. Добавление BufferedStream улучшило производительность в 100 раз в этом случае. Для получения дополнительной информации см. Небуферизованный вывод Очень медленно
Если вы прочтете статистику производительности и производительности на этом веб-сайте, то увидите, что самый быстрый способ чтения (потому что чтение, запись и обработка различаются) - это следующий фрагмент кода:
using (StreamReader sr = File.OpenText(fileName))
{
string s = String.Empty;
while ((s = sr.ReadLine()) != null)
{
//do your stuff here
}
}
Всего около 9 различных методов были протестированы, но этот, кажется, выходит впереди большую часть времени, даже выполняя буферизованный читатель, как упоминали другие читатели.
Вы говорите, что вас просили показать индикатор выполнения во время загрузки большого файла. Это потому, что пользователи искренне хотят видеть точный процент загрузки файлов или просто потому, что им нужна визуальная обратная связь о том, что что-то происходит?
Если последнее верно, то решение становится намного проще. Просто делать reader.ReadToEnd()
в фоновом потоке и отображать индикатор выполнения вместо выделенного.
Я поднимаю этот вопрос, потому что по моему опыту это часто так. Когда вы пишете программу обработки данных, то пользователи определенно будут заинтересованы в% полной информации, но для простых, но медленных обновлений пользовательского интерфейса они, скорее всего, просто захотят узнать, что компьютер не вышел из строя.:-)
Используйте фоновый рабочий и читайте только ограниченное количество строк. Читайте больше только тогда, когда пользователь прокручивает.
И старайтесь никогда не использовать ReadToEnd(). Это одна из функций, которые вы думаете "почему они это сделали?"; это помощник детского сценария, который отлично справляется с мелочами, но, как вы видите, он сосет для больших файлов...
Те парни, которые говорят вам использовать StringBuilder, должны чаще читать MSDN:
Вопросы производительности
Методы Concat и AppendFormat объединяют новые данные с существующим объектом String или StringBuilder. Операция конкатенации объекта String всегда создает новый объект из существующей строки и новых данных. Объект StringBuilder поддерживает буфер для размещения конкатенации новых данных. Новые данные добавляются в конец буфера, если доступно пространство; в противном случае выделяется новый больший буфер, данные из исходного буфера копируются в новый буфер, затем новые данные добавляются в новый буфер. Производительность операции конкатенации для объекта String или StringBuilder зависит от того, как часто происходит выделение памяти.
Операция конкатенации String всегда выделяет память, тогда как операция конкатенации StringBuilder выделяет память только в том случае, если буфер объекта StringBuilder слишком мал для размещения новых данных. Следовательно, класс String предпочтительнее для операции конкатенации, если конкатенируется фиксированное число объектов String. В этом случае отдельные операции конкатенации могут даже объединяться в одну операцию компилятором. Объект StringBuilder предпочтителен для операции конкатенации, если конкатенируется произвольное количество строк; например, если цикл объединяет случайное количество строк пользовательского ввода.
Это означает огромное выделение памяти, что становится большим использованием файловой системы подкачки, которая имитирует разделы вашего жесткого диска, чтобы действовать как память RAM, но жесткий диск очень медленный.
Опция StringBuilder отлично подходит для тех, кто использует систему в качестве однопользовательского, но когда два или более пользователей одновременно читают большие файлы, у вас возникает проблема.
Для двоичных файлов самый быстрый способ их чтения, который я нашел, - это.
MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
MemoryMappedViewStream mms = mmf.CreateViewStream();
using (BinaryReader b = new BinaryReader(mms))
{
}
В моих тестах это в сотни раз быстрее.
Этого должно быть достаточно, чтобы вы начали.
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();
}
}
Все отличные ответы! однако, для кого-то, ищущего ответ, они кажутся несколько неполными.
Поскольку стандартная строка может иметь только размер X, от 2 ГБ до 4 ГБ в зависимости от вашей конфигурации, эти ответы на самом деле не отвечают на вопрос ОП. Одним из методов является работа со списком строк:
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);
}
}
Некоторые могут хотеть токенизировать и разбивать строку при обработке. Список строк теперь может содержать очень большие объемы текста.
Хотя ответ, получивший наибольшее количество голосов, является правильным, но в нем не используется многоядерная обработка. В моем случае, имея 12 ядер, я использую PLink:
Parallel.ForEach(
File.ReadLines(filename), //returns IEumberable<string>: lazy-loading
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
(line, state, index) =>
{
//process line value
}
);
Стоит упомянуть, что я получил это как вопрос на собеседовании с просьбой вернуть 10 самых популярных случаев:
var result = new ConcurrentDictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
Parallel.ForEach(
File.ReadLines(filename),
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
(line, state, index) =>
{
result.AddOrUpdate(line, 1, (key, val) => val + 1);
}
);
return result
.OrderByDescending(x => x.Value)
.Take(10)
.Select(x => x.Value);
Benchmarking:
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
[Host] : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT
DefaultJob : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT
И, как вы можете видеть, это улучшение производительности на 75%.
Посмотрите на следующий фрагмент кода. Вы упомянули Most files will be 30-40 MB
, Это претендует на чтение 180 МБ за 1,4 секунды на 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);
}
}
}
Возможно, вам лучше использовать обработку файлов с отображенной памятью. Поддержка файлов с отображенной памятью будет присутствовать в.NET 4 (я думаю... я слышал об этом от кого-то другого, говорящего об этом), следовательно, эта оболочка, которая использует p/ призывает сделать ту же работу..
Редактировать: см. Здесь в MSDN о том, как это работает, вот запись в блоге, показывающая, как это делается в предстоящем.NET 4, когда он выйдет в виде релиза. Ссылка, которую я дал ранее, является оберткой вокруг pinvoke для достижения этой цели. Вы можете отобразить весь файл в память и просматривать его как скользящее окно при прокрутке файла.
Итератор может быть идеальным для этого типа работы:
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 и т. Д.).
Все это делается без использования фоновых задач, потоков или сложных пользовательских автоматов.
Прошло более 10 лет с момента последних ответов. Это мое решение для чтения текстовых файлов размером более 10 ГБ и возврата результата необходимой длины. Ставлю сюда на случай, если кто-то ищет помощи :)
public static List<string> ReadFileNGetLine(string filepath, int lenghtLine)
{
List<string> listString = new List<string>();
try
{
StringBuilder resultAsString = new StringBuilder();
FileInfo info = new FileInfo(filepath);
if (info.Length < 10)
{
return listString;
}
using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(filepath))
using (MemoryMappedViewStream memoryMappedViewStream = memoryMappedFile.CreateViewStream(0, info.Length))
{
for (int i = 0; i < info.Length; i++)
{
//Reads a byte from a stream and advances the position within the stream by one byte, or returns -1 if at the end of the stream.
if (listString.Count() >= lenghtLine)
{
break;
}
int result = memoryMappedViewStream.ReadByte();
if (result == -1)
{
break;
}
char letter = (char)result;
//khang: checking if the end of line is break line to collect full line
if ((letter.ToString() == "\r" || letter.ToString() == "\n") && letter.ToString() != "")
{
if (letter.ToString() != "\r")
{
listString.Add(resultAsString.ToString());
resultAsString.Clear();
}
}
else
{
resultAsString.Append(letter);
}
}
}
}
catch (Exception ex)
{
throw;
}
return listString;
}
Ссылка ниже содержит код, который легко читает фрагмент файла:
Я знаю, что этот вопрос довольно старый, но я нашел его на днях и проверил рекомендацию для MemoryMappedFile, и это самый быстрый метод. Для сравнения: чтение файла размером 6 616 939 строк размером 345 МБ с помощью метода readline занимает на моей машине более 12 часов, тогда как выполнение той же загрузки и чтение с использованием MemoryMappedFile заняло 3 секунды.