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

112
我得承担起处理大文件被加载到我们应用程序脚本编辑器(它类似于我们内部产品的VBA,用于快速宏)的可爱任务。大多数文件大小在300-400 KB左右,这样加载是没有问题的。但当它们超过100 MB时,这个过程就会变得困难(正如你所预料的那样)。
发生的情况是,文件被读取并推入一个RichTextBox中,然后进行导航 - 不要太担心这部分内容。
最初编写代码的开发人员只是使用StreamReader并执行以下操作:
[Reader].ReadToEnd()

这可能需要相当长的时间才能完成。

我的任务是将这段代码分解,将其分块读入缓冲区并显示进度条,同时提供取消选项。

一些假设:

  • 大多数文件的大小为30-40 MB
  • 文件内容为文本(而非二进制),有些是Unix格式,有些是DOS格式。
  • 一旦获取到内容,我们会确定使用的终止符。
  • 加载到richtextbox中后的渲染时间不是问题,只是初始文本加载时间较长。

现在是问题:

  • 我可以简单地使用StreamReader,然后检查Length属性(因此ProgressMax),发出一次针对固定缓冲区大小的读取,并在while循环内迭代,同时在后台工作器中进行,以便不阻塞主UI线程?完成后将stringbuilder返回给主线程。
  • 内容将被放入StringBuilder。如果长度可用,我可以使用流的大小初始化StringBuilder吗?

在您的专业意见中,这些是好主意吗?过去我曾遇到从流中读取内容时的一些问题,因为它总是会错过最后几个字节或其他东西,但如果是这种情况,我会问另一个问题。


32
30-40MB的脚本文件?天哪!我可不想审核那个…… - dthorpe
我知道这个问题有点老了,但是我前几天发现了它,并测试了MemoryMappedFile的建议,这无疑是最快的方法。通过readline方法读取一个7,616,939行345MB文件的比较,在我的机器上需要12个小时以上,而通过MemoryMappedFile执行相同的加载和读取只需要3秒钟。 - csonon
这只是几行代码而已。看看我正在使用的库,可以读取25GB及以上的大文件。https://github.com/Agenty/FileReader/ - Vikash Rathee
@VikashRathee 那个库使用 foreach (string line in File.ReadLines(path).Skip(skip))。那太糟糕了。 - mafu
13个回答

2

距离上一次回答已经过去10多年了,这是我阅读超过10Gb文本文件并返回符合您要求长度的结果的解决方案。我在这里分享,以便有需要的人寻求帮助 :)

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

MemoryMapped对于随机访问更好,而StreamReader在顺序读取方面快10倍。 - Stuart Dobson

2
一个迭代器可能非常适合这种工作:
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等)时,您的缓冲区正确对齐。
所有这些都是在不使用后台任务、线程或复杂的自定义状态机的情况下完成的。

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接