将多个文件合并为单个文件

22

代码:

static void MultipleFilesToSingleFile(string dirPath, string filePattern, string destFile)
{
    string[] fileAry = Directory.GetFiles(dirPath, filePattern);

    Console.WriteLine("Total File Count : " + fileAry.Length);

    using (TextWriter tw = new StreamWriter(destFile, true))
    {
        foreach (string filePath in fileAry)
        {
            using (TextReader tr = new StreamReader(filePath))
            {
                tw.WriteLine(tr.ReadToEnd());
                tr.Close();
                tr.Dispose();
            }
            Console.WriteLine("File Processed : " + filePath);
        }

        tw.Close();
        tw.Dispose();
    }
}

这个需要优化,因为速度非常慢:处理平均大小为40-50MB的45个XML文件需要3分钟。

请注意:45个平均为45MB的文件只是一个例子,可能有n个大小为m的文件,其中n在数千个范围内,m的平均值可以为128KB。简而言之,它可能会有所不同。

请问您对优化有任何看法吗?


3
45个平均大小为45MB的文件总共超过2GB。您期望需要多长时间?磁盘 I/O 将占用大部分时间。 - Ken White
3
在已经使用了using块的情况下,调用Dispose方法是多余的,因为你要处理的对象已经在using块中(它将替你处理Dispose)。 - Tim
1
你正在将每个文件加载到内存中。这样的大字符串将进入大对象堆,为什么不读取较小的数据块(重用缓冲区)?由于使用语句,关闭/处理是无用的。原始流足够了,因为您不处理/更改任何编码。完成所有这些后...您会发现性能不会有太大变化,因为可能大部分时间都花在I/O上。如果输出文件不在与输入相同的磁盘上,则甚至可以尝试使读取和写入异步(在写入时预读下一个文件/块)。 - Adriano Repetti
1
@Pratik 最后一点提示:如果你可能有1000个以上的文件,建议使用Directory.EnumerateFiles而不是Directory.GetFiles。出于同样的原因,我建议你检查文件大小以决定哪种复制方法更好(一次读取一个大文件,还是多次读取小块)。最后不要使用_helper_函数AppendAllText:它为每次写入打开和关闭文件。 - Adriano Repetti
1
@Pratik 不,大部分时间都花在(缓慢的)磁盘 I/O 上,使用不安全的代码不会带来任何好处。最好重新设计代码,避免浪费内存/CPU,并改进算法(好吧,即使多线程 I/O 也是有经验的)。嗯,你可以考虑重写代码以使用 ReadFileScatter 和 WriteFileGather,但坦率地说,我不知道相对于使用它们所需的努力而言,你将获得多少性能提升(至少直到非常高速的 SSD 变得足够普遍)。 - Adriano Repetti
显示剩余8条评论
6个回答

53

一般回答

为什么不直接使用Stream.CopyTo(Stream destination)方法

private static void CombineMultipleFilesIntoSingleFile(string inputDirectoryPath, string inputFileNamePattern, string outputFilePath)
{
    string[] inputFilePaths = Directory.GetFiles(inputDirectoryPath, inputFileNamePattern);
    Console.WriteLine("Number of files: {0}.", inputFilePaths.Length);
    using (var outputStream = File.Create(outputFilePath))
    {
        foreach (var inputFilePath in inputFilePaths)
        {
            using (var inputStream = File.OpenRead(inputFilePath))
            {
                // Buffer size can be passed as the second argument.
                inputStream.CopyTo(outputStream);
            }
            Console.WriteLine("The file {0} has been processed.", inputFilePath);
        }
    }
}

缓冲区大小调整

请注意,所提到的方法是重载的。

有两种方法重载:

  1. CopyTo(Stream destination)
  2. CopyTo(Stream destination, int bufferSize)

第二个方法重载通过bufferSize参数提供缓冲区大小调整功能。


我们如何将不同的值写入文件中?假设textFile1.text有以下行:"test, test, test"和"abc, pqr, xyz",而textFile2.text有以下行:"test, test, test"和"pqr, xyz, abcde",那么在textFile3.text中应该有以下行:"test, test, test","abc, pqr, xyz","pqr, xyz, abcde"。 - Rocky
@Rocky,你能否创建相应的问题并提供问题链接? - Sergey Vyacheslavovich Brunov
@SergeyBrunov,我该如何将这个“单一文件”分离以获取文件? - mrid
@mrid,请随意在Stack Overflow上创建一个单独的问题。长话短说,您需要将元数据存储在某个地方。元数据可以表示为目录表:每个组合文件在结果(单个)文件中的偏移量。 - Sergey Vyacheslavovich Brunov
它无法使用视频(webm扩展名)文件。而且也没有给出任何错误。 - Aamir Nakhwa
@AamirNakhwa,这是因为在这里,“组合”意味着将[输入]文件简单(直接)连接起来,即不考虑特定的文件格式(文件格式的细节)。 - Sergey Vyacheslavovich Brunov

3
一种选择是利用copy命令,并让它发挥其所擅长的功能。
类似这样:
static void MultipleFilesToSingleFile(string dirPath, string filePattern, string destFile)
{
    var cmd = new ProcessStartInfo("cmd.exe", 
        String.Format("/c copy {0} {1}", filePattern, destFile));
    cmd.WorkingDirectory = dirPath;
    cmd.UseShellExecute = false;
    Process.Start(cmd);
}

1
只需添加/b开关,强制copy将它们视为二进制文件(然后它会追加它们)。如果您需要一个命令行解决方案,这是不错的选择(从性能角度来看,这并不是最佳解决方案,但使其良好的努力相当高)。 - Adriano Repetti
1
@Eren:我认错了。这一定是我没有注意到的cmd.exe的变化。我会删除我的评论 - 幸运的是我没有投反对票。 :-) 感谢您的纠正;我总是喜欢学习新东西,即使在这个过程中被证明是错误的。 (而且+1,顺便说一下。) - Ken White
1
启动命令行实用程序使用C#组合文件内容?你在开玩笑吗? - Sergey Vyacheslavovich Brunov
3
这是一种糟糕的方法。我怀疑它能否比OP的代码表现更好,因为它涉及启动一个可能有额外开销的新进程,而且没有良好的错误处理选项(退出代码并不是一个好选项)。此外,它看起来过时了。糟糕。 - Sten Petrov
2
我永远不会使用未经过净化的输入作为参数来启动进程。 - Eric J.
显示剩余4条评论

2
我会使用BlockingCollection来读取,这样你就可以同时读写。
显然应该将写入到单独的物理磁盘中以避免硬件争用。
此代码将保留顺序。
读取速度比写入速度快,因此不需要并行读取。
同样,由于读取速度较快,限制集合大小,使读取不会比必要的更远地超前于写入。
一个简单的任务是在T1上并行读取下一个单一的文本,然后在T2上插入到SQL中,但存在文件大小不同的问题 - 写入小文件比读取大文件更快。
public void WriteFiles()
{
    using (BlockingCollection<string> bc = new BlockingCollection<string>(10))
    {
        // play with 10 if you have several small files then a big file
        // write can get ahead of read if not enough are queued

        TextWriter tw = new StreamWriter(@"c:\temp\alltext.text", true);
        // clearly you want to write to a different phyical disk 
        // ideally write to solid state even if you move the files to regular disk when done
        // Spin up a Task to populate the BlockingCollection
        using (Task t1 = Task.Factory.StartNew(() =>
        {
            string dir = @"c:\temp\";
            string fileText;      
            int minSize = 100000; // play with this
            StringBuilder sb = new StringBuilder(minSize);
            string[] fileAry = Directory.GetFiles(dir, @"*.txt");
            foreach (string fi in fileAry)
            {
                Debug.WriteLine("Add " + fi);
                fileText = File.ReadAllText(fi);
                //bc.Add(fi);  for testing just add filepath
                if (fileText.Length > minSize)
                {
                    if (sb.Length > 0)
                    { 
                       bc.Add(sb.ToString());
                       sb.Clear();
                    }
                    bc.Add(fileText);  // could be really big so don't hit sb
                }
                else
                {
                    sb.Append(fileText);
                    if (sb.Length > minSize)
                    {
                        bc.Add(sb.ToString());
                        sb.Clear();
                    }
                }
            }
            if (sb.Length > 0)
            {
                bc.Add(sb.ToString());
                sb.Clear();
            }
            bc.CompleteAdding();
        }))
        {

            // Spin up a Task to consume the BlockingCollection
            using (Task t2 = Task.Factory.StartNew(() =>
            {
                string text;
                try
                {
                    while (true)
                    {
                        text = bc.Take();
                        Debug.WriteLine("Take " + text);
                        tw.WriteLine(text);                  
                    }
                }
                catch (InvalidOperationException)
                {
                    // An InvalidOperationException means that Take() was called on a completed collection
                    Debug.WriteLine("That's All!");
                    tw.Close();
                    tw.Dispose();
                }
            }))

                Task.WaitAll(t1, t2);
        }
    }
}

BlockingCollection Class


1
如果输入和输出来自同一磁盘,则每次读取都必须等待(否则由于写入而变慢)... - Adriano Repetti
太多的代码却完成了很少的任务。多线程也无法将磁盘读写头分成两个。 - Sten Petrov
@StenPetrov,“明显应该写入单独的物理磁盘以避免硬件争用”的哪一部分不清楚? - paparazzo
@Blam,除了你在这里写的内容之外,我们还需要编写另一段代码来将数据写入单个磁盘吗? - Sten Petrov
@StenPetrov 代码不会在单个硬盘上失败。通过读写缓存,它甚至可能会实现一些并行优化。我不会为单个硬盘进行不同的优化。因此,你会以不同的方式来完成 - 这从你的回答中很清楚。 - paparazzo

2

尝试了由sergey-brunov发布的合并2GB文件的解决方案。系统在此过程中占用了约2GB的RAM。我进行了一些优化,并使其现在只需要350MB RAM即可合并2GB文件。


private static void CombineMultipleFilesIntoSingleFile(string inputDirectoryPath, string inputFileNamePattern, string outputFilePath)
        {
            string[] inputFilePaths = Directory.GetFiles(inputDirectoryPath, inputFileNamePattern);
            Console.WriteLine("Number of files: {0}.", inputFilePaths.Length);
            foreach (var inputFilePath in inputFilePaths)
            {
                using (var outputStream = File.AppendText(outputFilePath))
                {
                    // Buffer size can be passed as the second argument.
                    outputStream.WriteLine(File.ReadAllText(inputFilePath));
                    Console.WriteLine("The file {0} has been processed.", inputFilePath);

                }
            }
        }

1

你可以做几件事情:

  • 在我的经验中,默认缓冲区大小可以增加到约120K,我怀疑在所有流上设置大缓冲区将是最容易且最明显的性能提升器:

    new System.IO.FileStream("File.txt", System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read, 150000);
    
  • 使用Stream类,而不是StreamReader类。

  • 将内容读入大缓冲区,一次性将其转储到输出流中——这将加快小文件操作的速度。
  • 不需要多余的关闭/释放:您可以使用using语句。

0
    // Binary File Copy
    public static void mergeFiles(string strFileIn1, string strFileIn2, string strFileOut, out string strError)
    {
        strError = String.Empty;
        try
        {
            using (FileStream streamIn1 = File.OpenRead(strFileIn1))
            using (FileStream streamIn2 = File.OpenRead(strFileIn2))
            using (FileStream writeStream = File.OpenWrite(strFileOut))
            {
                BinaryReader reader = new BinaryReader(streamIn1);
                BinaryWriter writer = new BinaryWriter(writeStream);

                // create a buffer to hold the bytes. Might be bigger.
                byte[] buffer = new Byte[1024];
                int bytesRead;

                // while the read method returns bytes keep writing them to the output stream
                while ((bytesRead =
                        streamIn1.Read(buffer, 0, 1024)) > 0)
                {
                    writeStream.Write(buffer, 0, bytesRead);
                }
                while ((bytesRead =
                        streamIn2.Read(buffer, 0, 1024)) > 0)
                {
                    writeStream.Write(buffer, 0, bytesRead);
                }
            }
        }
        catch (Exception ex)
        {
            strError = ex.Message;
        }
    }

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