首页 文章

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

提问于
浏览
81

我是've got the lovely task of working out how to handle large files being loaded into our application'的脚本编辑器(对于我们用于快速宏的内部产品,它就像VBA) . 大多数文件大约300-400 KB,这是很好的加载 . 但是当它们超过100 MB时,这个过程很难(正如你所期望的那样) .

发生的事情是将文件读取并推入RichTextBox然后导航 - 不要过于担心这部分 .

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

[Reader].ReadToEnd()

这可能需要很长时间才能完成 .

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

一些假设:

  • 大多数文件将是30-40 MB

  • 文件的内容是文本(非二进制),有些是Unix格式,有些是DOS .

  • 检索完内容后,我们计算出使用的终结符 .

  • 没有人's concerned once it'加载了在richtextbox中渲染所需的时间 . 这只是文本的初始加载 .

现在提问:

  • 我可以简单地使用StreamReader,然后检查Length属性(so ProgressMax)并发出Read for set buffer size并在后台worker中的while循环WHILST中迭代,所以它没有完成.2602446_ .

  • 内容将转到StringBuilder . 如果长度可用,我可以用流的大小初始化StringBuilder吗?

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

11 回答

  • 159

    您可以使用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)
        {
    
        }
    }
    

    March 2013 UPDATE

    我最近编写了用于读取和处理(搜索文本)1 GB-ish文本文件的代码(比这里涉及的文件大得多),并通过使用 生产环境 者/消费者模式实现了显着的性能提升 . 生产环境 者任务使用 BufferedStream 以文本行读取,并将它们交给执行搜索的单独的消费者任务 .

    我用它来学习TPL数据流,这非常适合快速编码这种模式 .

    Why BufferedStream is faster

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

    December 2014 UPDATE: Your Mileage May Vary

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

    Related

    我遇到了一个案例,从ASP.Net MVC操作将大量生成的CSV文件传输到Response流非常慢 . 在这种情况下,添加BufferedStream可将性能提高100倍 . 欲了解更多,请参阅Unbuffered Output Very Slow

  • 14

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

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

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

  • 0

    如果您阅读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种不同的方法都是基准标记,但是大多数时候这个方法似乎都提前出来,甚至像其他读者所提到的那样执行缓冲读取器 .

  • 5

    对于二进制文件,我发现最快的阅读方式就是这个 .

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

    在我的测试中它快了几百倍 .

  • 3

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

    并尝试永远不要使用ReadToEnd() . 它's one of the functions that you think 2602459 ; it'是一个很好的小东西,但正如你所看到的那样,大文件很糟糕......

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

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

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

    StringBuilder选项看起来很适合将系统用作单用户的用户,但是如果有两个或更多用户同时读取大文件,则会出现问题 .

  • 14

    这应该足以让你入门 .

    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();
        }
    }
    
  • 1

    看看下面的代码片段 . 你提到了 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

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

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

  • 6

    迭代器可能适合这种类型的工作:

    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等)时正确排列 .

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

  • 4

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

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

    Read a large text file

    More information

  • 1

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

    由于标准字符串只能是大小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和拆分 . 字符串列表现在可以包含非常大量的文本 .

相关问题