读取多个非常大的文件的最佳方法

4
我需要帮助找出一种最快的方法来读取大约80个文件,每个文件中有超过500,000行,并将每个输入文件中的每行作为主文件中的一列写入。主文件必须写入像记事本这样的文本编辑器,而不是微软产品,因为它们无法处理如此多的行数。
例如,主文件应该像这样:
File1_Row1,File2_Row1,File3_Row1,...

File1_Row2,File2_Row2,File3_Row2,...

File1_Row3,File2_Row3,File3_Row3,...

我目前尝试了两种解决方案:

  1. 创建一个交错数组来将每个文件的内容存储到数组中,然后在读取所有文件的所有行之后,写入主文件。这种解决方案的问题是Windows操作系统内存会抛出错误,提示虚拟内存使用过多。
  2. 为每个80个文件动态创建一个读取特定行号的读取器线程,一旦所有线程完成读取行,就将这些值组合并写入文件,并重复所有文件的每一行。这种解决方案的问题是非常慢。

有没有更好的快速读取如此多大文件的解决方案?


8
抱歉让您失望了,但是“记事本(Notepad)”是微软公司的产品。 - Damien_The_Unbeliever
1
......而文本文件就是文本文件。换句话说,没有“NotePadFileStream”。 - ChiefTwoPencils
4个回答

5

最好的方法是使用StreamReader为每个输入文件打开一个,并为输出文件使用StreamWriter。然后,循环遍历每个读取器并读取一行并将其写入主文件。这样,您每次只加载一行,因此内存压力应该最小。我能够在37秒内复制80个至500,000行的文件。以下是示例:

using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;

class MainClass
{
    static string[] fileNames = Enumerable.Range(1, 80).Select(i => string.Format("file{0}.txt", i)).ToArray();

    public static void Main(string[] args)
    {
        var stopwatch = Stopwatch.StartNew();
        List<StreamReader> readers = fileNames.Select(f => new StreamReader(f)).ToList();

        try
        {
            using (StreamWriter writer = new StreamWriter("master.txt"))
            {
                string line = null;
                do
                {
                    for(int i = 0; i < readers.Count; i++)
                    {
                        if ((line = readers[i].ReadLine()) != null)
                        {
                            writer.Write(line);
                        }
                        if (i < readers.Count - 1)
                            writer.Write(",");
                    }
                    writer.WriteLine();
                } while (line != null);
            }
        }
        finally
        {
            foreach(var reader in readers)
            {
                reader.Close();
            }
        }
        Console.WriteLine("Elapsed {0} ms", stopwatch.ElapsedMilliseconds);
    }
}

我假设所有的输入文件都有相同数量的行,但你应该添加逻辑以在至少一个文件给出数据时继续读取。


3

如果您需要处理IO操作,但又不想对应用程序的内存造成压力,可以考虑使用内存映射文件。它能够同时保持良好的性能。

这里是完整的文档:内存映射文件


作为一个答案(而不是评论),这有点像回答问题“你怎么建造一座房子?”用“使用砖块。这个链接上有一些好的砖块指南”。正如mike z的答案所展示的,至少有一种方法可以使用OP已经知道的工具来回答这个问题;问题更多地是关于如何使用工具而不是使用哪些工具。 - anton.burger
1
@shambulator:我没有看到任何无效的内容。我们正在讨论的技术不是可以用简短明了的答案来解释的。我可以复制/粘贴文档中提供的代码,但我认为文档在详细解释我们正在讨论的功能方面做得更好。 - Tigran
但是我理解 MSDN 上的文章,内存映射文件仍然需要开发人员处理内存溢出,对吗? - Kai Hartmann
@KaiHartmann 没错;通过用MMF API代替文件IO API,OP不可能神奇地解决内存管理问题。这个问题揭示了OP的某种程度的理解; 像这样的回答-尽管大体上是正确的-假设了更多的背景知识。如果OP已经足够了解如何从MSDN文档中吸收所需的所有信息,他们就不会提出这个问题。 - anton.burger

1
如果您的计算机内存足够,我建议使用Parallel.Invoke结构,并将每个文件读入预先分配的数组中,例如:
string[] file1lines = new string[some value];
string[] file2lines = new string[some value];
string[] file3lines = new string[some value];

Parallel.Invoke(
() =>
{
   ReadMyFile(file1,file1lines);
},
() =>
{
   ReadMyFile(file2,file2lines);
},
() =>
{
   ReadMyFile(file3,file3lines);
}
);

每个ReadMyFile方法都应该只使用以下示例代码,根据这些基准测试,这是读取文本文件的最快方式:(参见链接)

int x = 0;
using (StreamReader sr = File.OpenText(fileName))
{
        while ((file1lines[x] = sr.ReadLine()) != null)
        {
               x += 1;
        }
}

如果您需要在编写最终输出之前操作每个文件的数据,可以 阅读此文章 了解最快的方法。
然后,您只需要一个方法将内容按照所需的方式写入每个 string[] 到输出中。

0

有一个打开文件句柄的数组。循环遍历该数组,并从每个文件中读取一行到字符串数组中。然后将此数组合并到主文件中,在末尾添加换行符。

这与您的第二种方法不同,它是单线程的,不会读取特定的行,而总是读取下一行。

当然,如果有比其他文件少的行,则需要进行错误处理。


我认为这种方法不会比多线程方法快很多。虽然它确实消除了多个线程竞争HDD访问,但仍具有大量随机I/O。 - Chris
可以通过使用缓冲区或显式的BufferedStream打开文件来改进此代码,以达到更好的性能。顺便说一下,我认为原帖中的线程方法没有读取下一行,而是总是读取到某一行号。 - JeffRSon
1
@jeffrson 如果我没记错的话,StreamReader/Writer 默认是缓冲的。 - Mike Zboray

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