如何使用C#高效合并巨大的文件

8
我有超过125个大小约为100Mb的TSV文件需要合并。合并操作可以销毁这125个文件,但不能销毁数据。重要的是最终我能够得到一个大文件,其中包含所有文件内容(没有特定顺序)。
有没有一种有效的方法来完成这个操作?我想知道Windows是否提供了API来简单地将所有文件进行“联合”?否则,我将不得不读取所有文件并写入一个大文件。
谢谢!

请看这里(可能是重复的):https://dev59.com/S3RB5IYBdhLWcg3w-8Lo - Abel
4个回答

17

所以"合并"实际上只是将文件一个接一个地写入吗?这很简单 - 只需打开一个输出流,然后重复打开一个输入流,复制数据,关闭即可。例如:

static void ConcatenateFiles(string outputFile, params string[] inputFiles)
{
    using (Stream output = File.OpenWrite(outputFile))
    {
        foreach (string inputFile in inputFiles)
        {
            using (Stream input = File.OpenRead(inputFile))
            {
                input.CopyTo(output);
            }
        }
    }
}

这是使用.NET 4中新增的Stream.CopyTo方法。如果您不使用.NET 4,则另一个辅助方法会很有用:

private static void CopyStream(Stream input, Stream output)
{
    byte[] buffer = new byte[8192];
    int bytesRead;
    while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, bytesRead);
    }
}

据我所知,没有比这更有效率的方法了... 但是重要的是,这不会在你的系统上占用太多内存。它不像是反复将整个文件读入内存,然后再全部写出来。

编辑:正如评论中指出的那样,有一些可以调整文件选项的方法,可能会让它在文件系统处理数据方面稍微更加有效率。但本质上你还是需要逐个缓冲区地读取和写入数据。


你猜测的答案是否是否定的? - Marcus Johansson
非常高兴听到关于CopyTo的消息,现在我可以删除我的答案;-) - Abel
Copystream 方法看起来很像 CopyTo 的实现,这是故意的吗? - dada686
从内核层面来看,这可能并不是最有效的方法。你会花费相当多的时间在内存中复制数据。将FILE_FLAG_NO_BUFFERING传递给底层的CreateFile可以避免这种情况。 - MSalters
@MSalters:当你说“相当长的时间”时,这不是在物理读取时花费的时间会被大大压缩吗?在创建输入流时使用FileOptions.SequentialScan可能有所帮助,但我通常会选择最简单的方法,直到发现实际问题为止。 - Jon Skeet
显示剩余4条评论

2

您说的“合并”,是指您想使用一些自定义逻辑来决定哪些行放在哪里吗?还是主要想将文件连接成一个大文件?

如果是后者,可能根本不需要以编程方式进行操作,只需使用以下命令生成一个批处理文件即可(如果不需要,请删除/b):

copy /b "file 1.tsv" + "file 2.tsv" "destination file.tsv"

使用C#,我会采取以下方法。编写一个简单的函数来复制两个流:
void CopyStreamToStream(Stream dest, Stream src)
{
    int bytesRead;

    // experiment with the best buffer size, often 65536 is very performant
    byte[] buffer = new byte[GOOD_BUFFER_SIZE];

    // copy everything
    while((bytesRead = src.Read(buffer, 0, buffer.Length)) > 0)
    {
        dest.Write(buffer, 0, bytesRead);
    }
}

// then use as follows (do in a loop, don't forget to use using-blocks)
CopStreamtoStream(yourOutputStream, yourInputStream);

@Aaronaught:我提交时已经完成了一半,然后写了第二部分。但是,请注意第二段中的小提示:“只需生成一个批处理文件”。通过生成,我的意思是:自动生成。但后来我决定添加C#代码 :) - Abel

2

从命令行执行:

copy 1.txt+2.txt+3.txt combined.txt

或者

copy *.txt combined.txt

1
你知道他说了125个文件吗? 这将非常冗长和繁琐。如果你提供一个C#程序来生成复制字符串,那可能是一个部分答案。 - Aaronaught
6
兄弟,那就使用第二个选项,使用文件掩码。或者运行dir命令(例如 dir /b 只获取文件名),将文件名保存到文件中,在好的文本编辑器中构建命令。有很多方法可以避免手动输入125个文件名。 - Gabriel Magana
重点是,你甚至没有接近回答问题。你对问题领域做了很多假设,而这些假设你不可能知道。询问领域的更多细节是可以的,但不能简单地假设问题作者选择了错误的解决方法。你的可能无关的解决方案和争论的语气都会被扣分,“伙计”。 - Aaronaught
1
LOL,一定要喜欢自封的版主。冷静点。你读太多东西了(巧合的是,这正是你指责我的事情;谈论投射自己)。OP问如何合并文件,我给出了一个可行的答案。它可能完美地解决问题,也可能不是。OP知道是否是这种情况,但你不知道。虽然我不想参与争吵,所以这是我对你的最后回应。 - Gabriel Magana

0

使用一个包含100MB文本文件的文件夹,总计约12GB,我发现通过使用File.ReadAllBytes然后将其写入流中,可以比接受的答案节省一些时间。

        [Test]
        public void RaceFileMerges()
        {
            var inputFilesPath = @"D:\InputFiles";
            var inputFiles = Directory.EnumerateFiles(inputFilesPath).ToArray();

            var sw = new Stopwatch();
            sw.Start();

            ConcatenateFilesUsingReadAllBytes(@"D:\ReadAllBytesResult", inputFiles);

            Console.WriteLine($"ReadAllBytes method in {sw.Elapsed}");

            sw.Reset();
            sw.Start();

            ConcatenateFiles(@"D:\CopyToResult", inputFiles);

            Console.WriteLine($"CopyTo method in {sw.Elapsed}");
        }

        private static void ConcatenateFiles(string outputFile, params string[] inputFiles)
        {
            using (var output = File.OpenWrite(outputFile))
            {
                foreach (var inputFile in inputFiles)
                {
                    using (var input = File.OpenRead(inputFile))
                    {
                        input.CopyTo(output);
                    }
                }
            }
        }

        private static void ConcatenateFilesUsingReadAllBytes(string outputFile, params string[] inputFiles)
        {
            using (var stream = File.OpenWrite(outputFile))
            {
                foreach (var inputFile in inputFiles)
                {
                    var currentBytes = File.ReadAllBytes(inputFile);
                    stream.Write(currentBytes, 0, currentBytes.Length);
                }
            }
        }

ReadAllBytes方法在00:01:22.2753300内完成。 CopyTo方法在00:01:30.3122215内完成。
我多次重复这个操作,结果都差不多。

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