在C#中使用流读取大型文本文件


96

我有一个可爱的任务,要弄清楚如何处理被加载到应用程序的脚本编辑器中的大文件(就像我们的内部产品的VBA一样,用于快速宏)。大多数文件约为300-400 KB,可以很好地加载。但是,当它们超过100 MB时,该过程将很困难(如您所料)。

发生的情况是将文件读取并推入RichTextBox中,然后进行导航-不必过多担心这一部分。

编写初始代码的开发人员只需使用StreamReader并执行

[Reader].ReadToEnd()

这可能需要一段时间才能完成。

我的任务是分解这段代码,将其分块读取到缓冲区中,并显示一个带有取消它选项的进度条。

一些假设:

  • 大多数文件将为30-40 MB
  • 文件的内容是文本(不是二进制),有些是Unix格式,有些是DOS。
  • 检索到内容后,我们便确定使用了哪种终止符。
  • 一旦加载了RichTextBox中呈现所需的时间,就没有人关心。这只是文本的初始加载。

现在开始提问:

  • 我可以简单地使用StreamReader,然后检查Length属性(即ProgressMax)并发出Read来获取设置的缓冲区大小,并在后台工作程序内的while循环WHILST中进行迭代,以便它不会阻塞主UI线程吗?然后,在完成后将stringbuilder返回到主线程。
  • 内容将转到StringBuilder。如果长度可用,我如何用流的大小初始化StringBuilder?

这些(根据您的专业意见)是好主意吗?过去,我从Streams读取内容时遇到了一些问题,因为它总是会丢失最后几个字节或其他内容,但是如果是这种情况,我会问另一个问题。


29
30-40MB的脚本文件?鲭鱼!我不希望对代码进行审查...
dthorpe

我知道这个问题比较老,但是前几天我发现了这个问题,并测试了MemoryMappedFile的建议,这是最快的方法。一个比较是通过readline方法读取7,616,939行345MB文件在我的计算机上花费了12个小时以上,同时执行了相同的加载,并且通过MemoryMappedFile读取了3秒。
csonon

只是几行代码。请参阅该库,我正在使用它来读取25gb和更大的文件。github.com/Agenty/FileReader
Vikash Rathee

Answers:


175

您可以使用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月更新

我最近编写了用于读取和处理(在其中搜索文本)1 GB大小的文本文件(比此处涉及的文件大得多)的代码,并通过使用生产者/消费者模式实现了显着的性能提升。生产者任务使用读取文本行,BufferedStream并将其交给执行搜索的单独消费者任务。

我以此为契机学习了TPL Dataflow,它非常适合快速编码此模式。

为什么BufferedStream更快

缓冲区是内存中用于缓存数据的字节块,从而减少了对操作系统的调用次数。缓冲区提高了读写性能。缓冲区可用于读取或写入,但不可同时使用。BufferedStream的Read和Write方法自动维护缓冲区。

2014年12月更新:您的里程可能会有所不同

根据评论,FileStream应该在内部使用BufferedStream。首次提供此答案时,我通过添加BufferedStream评估了显着的性能提升。当时,我的目标是在32位平台上使用.NET3.x。今天,针对64位平台上的.NET 4.5,我看不出任何改进。

有关

我遇到了一个案例,其中一个大的生成的CSV文件从ASP.Net MVC操作流到响应流非常慢。在这种情况下,添加BufferedStream可将性能提高100倍。有关更多信息,请参见非常慢的无缓冲输出


12
杜德(Dude),BufferedStream发挥了所有作用。+1 :)
Marcus

2
向IO子系统请求数据会产生成本。在旋转磁盘的情况下,您可能必须等待磁盘旋转到位才能读取下一个数据块,或更糟糕的是,等待磁盘磁头移动。尽管SSD没有机械部件来减慢速度,但是访问它们仍然需要按IO操作的成本进行。缓冲的流不仅仅读取StreamReader的请求,还减少了对OS的调用次数,并最终减少了单独的IO请求的次数。
Eric J.

4
真?这对我的测试方案没有影响。根据Brad Abrams的说法,与FileStream相比,使用BufferedStream没有任何好处。
Nick Cox 2013年

2
@NickCox:您的结果可能会因基础IO子系统而异。在旋转磁盘和磁盘控制器中,在其高速缓存中没有数据(以及Windows未高速缓存的数据)上,加速非常快。布拉德(Brad)的专栏写于2004年。我最近评估了实际的,巨大的改进。
Eric J.

3
根据以下内容,这是没有用的:stackoverflow.com/questions/492283/…FileStream已在内部使用缓冲区。
Erwin Mayer 2014年

21

如果你读了这个网站的性能和基准统计,你会看到,以最快的方式读取(因为阅读,写作和处理都是不同的)文本文件下面的代码片段:

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

所有大约9种不同的方法都标有基准,但似乎大多数时间都领先于其他方法,甚至比其他阅读器所提到的还要执行缓冲的阅读器


2
这对于剥离19GB的postgres文件并将其转换为多个文件中的sql语法非常有效。感谢从未正确执行我的参数的postgres家伙。/感叹
Damon Drake

对于真正的大文件(例如大于150MB),这里的性能差异似乎得到了回报(也确实应该使用a StringBuilder来将它们加载到内存中,加载速度更快,因为它不会在每次添加字符时生成新的字符串)
Joshua G

15

您说有人要求您在加载大文件时显示进度栏。是因为用户确实希望查看文件加载的确切百分比,还是仅仅因为他们希望视觉反馈正在发生某些事情?

如果后者是正确的,那么解决方案将变得更加简单。只需reader.ReadToEnd()在后台线程上执行操作,并显示选取框类型的进度条即可。

我提出这一点是因为根据我的经验,通常是这种情况。当您编写数据处理程序时,用户一定会对完整百分比感兴趣,但是对于简单但缓慢的UI更新,他们更有可能只是想知道计算机没有崩溃。:-)


2
但是用户可以取消ReadToEnd调用吗?
蒂姆·斯卡伯勒

@Tim,发现得很好。在这种情况下,我们回到StreamReader循环中。但是,它仍然会更简单,因为无需提前阅读即可计算进度指示器。
克里斯蒂安·海特

8

对于二进制文件,我发现读取它们的最快方法是这样。

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

在我的测试中,速度快了数百倍。


2
您对此有确凿的证据吗?OP为什么要在其他答案上使用它?请更深入一点,并提供更多细节
Dylan Corriveau

7

使用后台工作人员,并且仅读取有限数量的行。仅在用户滚动时阅读更多内容。

并尝试永远不要使用ReadToEnd()。这是您认为“为什么要这么做”的功能之一;这是一个脚本小子的助手,可以很好地处理小事情,但是如您所见,它可吸收大文件...

那些告诉您使用StringBuilder的人需要更频繁地阅读MSDN:

性能注意事项
Concat和AppendFormat方法都将新数据连接到现有的String或StringBuilder对象。字符串对象串联操作始终根据现有字符串和新数据创建一个新对象。StringBuilder对象维护一个缓冲区以容纳新数据的串联。如果有可用空间,则将新数据附加到缓冲区的末尾;否则,将分配一个更大的新缓冲区,将原始缓冲区中的数据复制到新缓冲区中,然后将新数据附加到新缓冲区中。String或StringBuilder对象的串联操作的性能取决于内存分配发生的频率。
String串联操作始终分配内存,而StringBuilder串联操作仅在StringBuilder对象缓冲区太小而无法容纳新数据时分配内存。因此,如果串联固定数量的String对象,则String类对于串联操作更可取。在这种情况下,编译器甚至可以将单个串联操作组合为单个操作。如果串联任意数量的字符串,则StringBuilder对象对于串联操作是更可取的。例如,如果循环连接了随机数量的用户输入字符串。

这意味着要分配大量的内存,这将大量使用交换文件系统,它可以模拟硬盘驱动器的各个部分,使其像RAM内存一样工作,但是硬盘驱动器的速度非常慢。

StringBuilder选项适用于以单用户身份使用系统的用户,但是当您有两个或多个用户同时读取大文件时,就会遇到问题。


你们真是太快了!不幸的是,由于宏的工作方式,整个流都需要加载。正如我提到的,不要担心RichText部分。它是我们想要提高的初始负载。
妮可·李

因此,您可以分部分进行工作,阅读第一行X线,应用宏,阅读第二行X线,应用宏,依此类推...如果您解释此宏的作用,我们可以为您提供更高的精度
Tufo

5

这应该足以让您入门。

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
我将“ var buffer = new char [1024]”移出循环:没有必要每次都创建一个新缓冲区。只需将其放在“ while(count> 0)”之前。
Tommy Carlier 2010年

4

看一下下面的代码片段。你提到过Most files will be 30-40 MB。这声称在Intel Quad Core上可在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);
        }
    }
}

来源文章


3
众所周知,这种测试是不可靠的。重复测试时,您将从文件系统缓存中读取数据。这比从磁盘读取数据的真实测试至少快一个数量级。180 MB的文件可能不会少于3秒。重新启动计算机,对真实数字运行一次测试。
汉斯·帕桑特

7
该行stringBuilder.Append有潜在的危险,您需要将其替换为stringBuilder.Append(fileContents,0,charsRead); 以确保即使流已提前结束,您也不添加完整的1024个字符。
约翰内斯·鲁道夫

@JohannesRudolph,您的评论为我解决了一个错误。您是怎么想出1024号的?
HeyJude

3

您可能最好在这里使用内存映射文件处理..内存映射文件支持将在.NET 4中实现(我认为...我听说是通过其他人谈论的),因此此包装器使用p /要求执行相同的工作。

编辑:有关MSDN的工作原理,请参见此处,这是博客条目,指示发布时在即将发布的.NET 4中是如何完成的。我之前给出的链接是实现此目标的包装。您可以将整个文件映射到内存中,并在滚动浏览文件时像滑动窗口一样查看它。


2

所有出色的答案!但是,对于寻找答案的人来说,这些似乎有些不完整。

由于标准字符串的大小只能为X,从2Gb到4Gb,具体取决于您的配置,因此这些答案并不能真正满足OP的问题。一种方法是使用字符串列表:

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

有些人可能希望在处理时对令牌进行标记并拆分行。现在,字符串列表可以包含大量文本。


1

迭代器可能非常适合此类工作:

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-8UTF-16等)时缓冲区正确对齐。

这一切都无需使用后台任务,线程或复杂的自定义状态机即可完成。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.