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

时间:2010-01-29 12:36:43

标签: c# .net stream streamreader large-files

我有一个很好的任务,就是如何处理加载到应用程序脚本编辑器中的大文件(对于我们用于快速宏的内部产品,它就像VBA)。大多数文件大约300-400 KB,这是很好的加载。但是当它们超过100 MB时,这个过程很难(正如你所期待的那样)。

该文件被读取并被推入RichTextBox然后导航 - 不要过于担心这部分。

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

[Reader].ReadToEnd()

可能需要很长时间才能完成。

我的任务是打破这段代码,将其以块的形式读入缓冲区并显示一个带有取消选项的进度条。

一些假设:

  • 大多数文件都是30-40 MB
  • 文件的内容是文本(非二进制),有些是Unix格式,有些是DOS。
  • 检索完内容后,我们会找出使用的终结符。
  • 一旦加载了在richtextbox中渲染所需的时间,没有人关注。这只是文本的初始加载。

现在提出问题:

  • 我可以简单地使用StreamReader,然后检查Length属性(so ProgressMax)并发出Read for set buffer size并在后台worker中的while循环 WHILST 中迭代,所以它不会阻止主UI线程?然后在完成后将stringbuilder返回到主线程。
  • 内容将转到StringBuilder。如果长度可用,我可以用流的大小初始化StringBuilder吗?

这些(在您的专业意见中)是好主意吗?我在过去读过Streams的内容时遇到过一些问题,因为它总会错过最后几个字节或者其他东西,但如果是这样的话,我会问另一个问题。

12 个答案:

答案 0 :(得分:161)

您可以使用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-ish文本文件的代码(比这里涉及的文件大得多),并通过使用生产者/消费者模式获得了显着的性能提升。生产者任务使用BufferedStream以文本行读取,并将其交给执行搜索的单独的消费者任务。

我以此为契机学习TPL数据流,非常适合快速编码这种模式。

为什么BufferedStream更快

  

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

2014年12月更新:您的里程可能会发生变化

根据评论,FileStream应该在内部使用BufferedStream。在首次提供此答案时,我通过添加BufferedStream测量了显着的性能提升。当时我在32位平台上瞄准.NET 3.x.今天,在64位平台上面向.NET 4.5,我没有看到任何改进。

相关

我遇到过这样一种情况:从ASP.Net MVC操作将大量生成的CSV文件传输到Response流非常慢。在这种情况下,添加BufferedStream可将性能提高100倍。有关详情,请参阅Unbuffered Output Very Slow

答案 1 :(得分:14)

您说在加载大文件时,系统会要求您显示进度条。这是因为用户真的想要查看文件加载的确切百分比,还是仅仅因为他们想要视觉反馈才能发生某些事情?

如果后者为真,则解决方案变得更加简单。只需在后台线程上执行reader.ReadToEnd(),并显示一个选取框类型的进度条而不是正确的进度条。

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

答案 2 :(得分:14)

如果您阅读performance and benchmark stats on this website,您会发现读取的最快方式(因为阅读,写入和处理都不同),文本文件如下代码片段:

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

所有大约9种不同的方法都是基准标记,但是大多数时候这种方法似乎都提前了,甚至像其他读者所提到的那样执行缓冲读取器

答案 3 :(得分:8)

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

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

在我的测试中它的速度提高了数百倍。

答案 4 :(得分:6)

使用后台工作程序并只读取有限数量的行。仅在用户滚动时阅读更多内容。

并尝试永远不要使用ReadToEnd()。这是你认为“他们为什么要成功?”的功能之一;它是一个script kiddies'助手,可以很好地处理小事情,但正如你所看到的那样,对于大文件来说很糟糕......

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

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

这意味着巨大的内存分配,大量使用交换文件系统,模拟硬盘驱动器的部分就像RAM内存,但硬盘驱动器非常慢。

对于将系统用作单用户的人来说,StringBuilder选项看起来很好,但是当你有两个或更多用户同时阅读大文件时,你会遇到问题。

答案 5 :(得分: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();
    }
}

答案 6 :(得分:4)

查看以下代码段。你提到过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);
        }
    }
}

Original Article

答案 7 :(得分:3)

你可能最好使用处理here的内存映射文件。内存映射文件支持将在.NET 4中使用(我想...我听说通过其他人谈论它)因此这个包装器使用p / invokes来完成同样的工作..

编辑:在MSDN上查看其工作原理,这里是blog条目,说明在即将发布的.NET 4中它是如何完成的发布。我之前给出的链接是pinvoke周围的包装来实现这一点。您可以将整个文件映射到内存中,并在滚动文件时将其视为滑动窗口。

答案 8 :(得分:2)

虽然最受欢迎的答案是正确的,但它缺乏多核处理的使用。就我而言,我使用 PLink 有 12 个内核:

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

<头>
方法 平均 错误 StdDev Gen 0 Gen 1 第 2 代 已分配
GetTopWordsSync 33.03 秒 0.175 秒 0.155 秒 1194000 314000 7000 7.06 GB
GetTopWordsParallel 10.89 秒 0.121 秒 0.113 秒 1225000 354000 8000 7.18 GB

正如您所见,性能提高了 75%。

答案 9 :(得分: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等时正确排列)。

这一切都是在不使用后台任务,线程或复杂的自定义状态机的情况下完成的。

答案 10 :(得分:1)

我的文件超过13 GB: enter image description here

波纹管链接包含轻松读取文件的代码:

Read a large text file

More information

答案 11 :(得分:0)

所有优秀答案!然而,对于寻找答案的人来说,这些似乎有些不完整。

作为标准字符串,只能是大小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);
}
}

有些人可能想要在处理时进行Tokenise和拆分。字符串列表现在可以包含非常大量的文本。