用C#中的stream读取大文本文件
我有一个可爱的任务,即如何处理大文件被加载到我们的应用程序的脚本编辑器(这就像我们的内部产品快速macros的VBA )。 大多数文件大约300-400 KB这是很好的加载。 但是当他们超过100 MB时,这个过程很难(如你所期望的)。
会发生什么是该文件被读取并推入一个RichTextBox,然后导航 – 不要太担心这个部分。
编写初始代码的开发人员只需使用StreamReader即可
[Reader].ReadToEnd()
这可能需要很长时间才能完成。
我的任务是打破这一点的代码,阅读成块缓冲区,并显示一个进度条,并取消它的选项。
一些假设:
- 大多数文件将是30-40 MB
- 文件的内容是文本(不是二进制),有些是Unix格式,有些是DOS。
- 一旦内容被检索,我们计算出使用了什么终止符。
- 一旦加载了在richtextbox中渲染所需的时间,就不会有人担心。 这只是文本的初始负载。
现在提问:
- 我可以简单地使用StreamReader,然后检查Length属性(如ProgressMax),并发出一个读取设置的缓冲区大小,并在while循环WHILST内循环遍历一个后台工作,所以它不会阻止主UI线程? 然后在完成之后将stringbuilder返回到主线程。
- 内容将会转到一个StringBuilder。 如果长度可用,我可以初始化StringBuilder与stream的大小?
这些(在你的专业意见)好主意? 过去我曾经从Streams中读过一些内容,因为它总是会丢失最后的几个字节,但是如果是这样的话,我会问另外一个问题。
您可以通过使用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年3月更新
我最近编写了用于阅读和处理(search文本)的代码,其中包含1GB的文本文件(比这里涉及的文件大得多),并通过使用生产者/消费者模式获得了显着的性能增益。 生成器任务使用BufferedStream
按文本行读取,并将其交给执行search的单独消费者任务。
我以此为契机来学习TPL Dataflow,它非常适合快速编码这种模式。
为什么BufferedStream更快
缓冲区是内存中用于caching数据的字节块,从而减less了对操作系统的调用次数。 缓冲区提高了读写性能。 缓冲区可以用于读取或写入,但不能同时使用。 BufferedStream的Read和Write方法自动维护缓冲区。
2014年12月更新:您的里程可能会有所不同
基于注释,FileStream应该在内部使用BufferedStream 。 在第一次提供这个答案的时候,我通过添加一个BufferedStream来衡量显着的性能提升。 当时我在32位平台上将.NET 3.x作为目标。 今天,在64位平台上面向.NET 4.5,我没有看到任何改进。
有关
我遇到了一个情况,从一个ASP.Net MVC行动stream大的,生成的CSV文件到响应stream是非常缓慢的。 在这种情况下添加一个BufferedStream提高了100倍的性能。 有关更多信息,请参阅非缓冲输出非常慢
你说你已经被要求显示一个进度条,而大文件正在加载。 那是因为用户真正想看到文件加载的确切百分比,还是仅仅因为他们需要视觉反馈,正在发生什么?
如果后者是真的,那么解决scheme变得更简单。 只需在后台线程上执行reader.ReadToEnd()
,并显示一个选取框types的进度条,而不是正确的。
我提出这一点,因为根据我的经验,情况往往如此。 当你正在编写一个数据处理程序时,用户一定会对%完整的数字感兴趣,但是对于简单但很慢的UI更新,他们更可能只想知道计算机没有崩溃。 🙂
如果您阅读本网站上的性能和基准testing数据 ,您将看到最快的阅读方式 (因为阅读,写作和处理完全不同),文本文件就是以下代码片段:
using (StreamReader sr = File.OpenText(fileName)) { string s = String.Empty; while ((s = sr.ReadLine()) != null) { //do your stuff here } }
大概9种不同的方法都是基准testing,但是大部分时间似乎都超出了预期, 甚至不像其他读者提到的那样使用缓冲读取器 。
使用后台工作者,只能读取数量有限的行。 只有当用户滚动时才能阅读更多内容。
并尽量不要使用ReadToEnd()。 这是你认为“他们为什么做的?”的function之一。 这是一个脚本小子的助手,可以用小的东西来处理,但正如你所看到的,它吸引大量的文件…
那些告诉你使用StringBuilder的人需要经常阅读MSDN:
性能考虑
Concat和AppendFormat方法都将新数据连接到现有的String或StringBuilder对象。 一个String对象连接操作总是从现有的string和新的数据中创build一个新的对象。 一个StringBuilder对象维护一个缓冲区以适应新数据的连接。 如果空间可用,新的数据被附加到缓冲区的末尾; 否则,分配一个新的,更大的缓冲区,将来自原始缓冲区的数据复制到新的缓冲区,然后将新的数据附加到新的缓冲区。 String或StringBuilder对象的连接操作的性能取决于内存分配的频率。
如果StringBuilder对象缓冲区太小而不能容纳新数据,则String连接操作始终分配内存,而StringBuilder连接操作仅分配内存。 因此,如果串联固定数量的String对象,那么String类对于连接操作来说是可取的。 在这种情况下,编译器甚至可以将各个级联操作组合成单个操作。 如果连接任意数量的string,StringBuilder对象可以用于连接操作; 例如,如果一个循环连接了随机数的用户inputstring。
这意味着巨大的内存分配,大量使用交换文件系统,模拟您的硬盘驱动器的部分行为像RAM内存,但硬盘驱动器是非常缓慢的。
对于谁将系统用作单用户,StringBuilder选项看起来不错,但是当您有两个或更多用户同时读取大文件时,您遇到问题。
对于二进制文件,阅读他们最快的方式,我发现是这样的。
MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file); MemoryMappedViewStream mms = mmf.CreateViewStream(); using (BinaryReader b = new BinaryReader(mms)) { }
在我的testing中,数百倍的速度。
这应该足以让你开始。
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(); } }
看看下面的代码片段。 你提到Most files will be 30-40 MB
。 这宣称在1.4英寸的四核上读取180 MB的内存:
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中发布的。 我之前提到的这个链接是围绕着实现这一目标的一个包装。 您可以将整个文件映射到内存中,并在滚动文件时像滑动窗口一样查看它。
迭代器可能是完美的这种工作types:
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等)时缓冲区正确排列。
这一切都是在不使用后台任务,线程或复杂的自定义状态机的情况下完成的。
我知道这个问题是相当古老的,但我发现它,并已经testing了MemoryMappedFile的build议,这是最快的方法。 比较读取7,616,939行的345MB文件,通过readline方法在我的机器上花费12个小时以上,同时执行相同的加载并通过MemoryMappedFile读取花费3秒钟。
我想在这个build议的意见中发表,但是我的“代表”还不够高。 我想要记下这一点,因为我search了网页,并testing了我可以find的所有build议,以便成功回溯并testingMemoryMapedFile。