【问题标题】:Combine multiple files into single file将多个文件合并为一个文件
【发布时间】:2013-01-25 15:27:24
【问题描述】:

代码:

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

我需要对其进行优化,因为它非常慢:45 个平均大小为 40 - 50 Mb XML 文件的文件需要 3 分钟。

请注意:平均 45 MB 的 45 个文件只是一个示例,它可以是 n 大小为 m 的文件数,其中 n 以千为单位,m 平均为 128 Kb .简而言之,它可能会有所不同。

您能否提供一些关于优化的意见?

【问题讨论】:

  • 45 个文件,每个文件平均 45MB,总共刚好超过 2GB。你预计这需要多长时间?磁盘 I/O 将占用大量时间。
  • 调用 Dispose 是多余的,因为您要处理的对象已经在 using 块中(它将为您处理 Dispose)。
  • 您正在将每个文件加载到内存中。这么大的字符串会进入大对象堆,为什么不读取较小的数据块(重用缓冲区)?由于 using 语句,关闭/处置是无用的。原始流就足够了,因为您不处理/更改任何编码。完成所有这些之后……您会看到性能不会有太大变化,因为可能大部分时间都花在了 I/O 上。如果输出文件与输入文件不在同一个磁盘上,那么您甚至可以尝试异步读写(在写入时预读下一个文件/块)。
  • @Pratik 最后一点:如果您可能有 1000 多个文件,您可以考虑使用 Directory.EnumerateFiles 而不是 Directory.GetFiles。出于同样的原因,我建议您检查文件大小以确定哪种复制方法更好(一次大读取或多个小块)。最后不要使用 helper 函数 AppendAllText:它会在每次写入时打开和关闭文件。
  • @Pratik 不,大部分时间都花在(慢速)磁盘 I/O 上,使用不安全代码不会有任何收获。最好只重构代码以不浪费内存/CPU 并改进算法(好吧,即使 I/O 的多线程在某种程度上也是经验性的)。好吧,您可能会考虑重写您的代码以使用 ReadFileScatter 和 WriteFileGather 但坦率地说,我不知道您将获得多少性能提升(与使用它们相比,至少在非常高速的 SSD 足够普遍之前)。

标签: c# .net file-io copy


【解决方案1】:

一般回答

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

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,您能否创建适当的问题并提供问题的链接?
  • @SergeyBrunov 如何分离这个“单个文件”以取回文件?
  • @mrid,请随时在 Stack Overflow 上创建一个单独的问题。长话短说,您需要将元数据存储在某处。元数据可以表示为目录:每个组合文件在结果(单个)文件中的偏移量。
  • 它不适用于视频(webm 扩展)文件。而且也没有给出任何错误
【解决方案2】:

一种选择是利用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);
}

【讨论】:

  • 只需添加/b 开关以强制copy 将它们视为二进制 文件(然后它会将它们附加)。如果您需要一个命令行解决方案,这很好(从性能的角度来看,它不是 最佳 解决方案,但要做到这一点需要付出很大的努力)。
  • @Eren:我的立场是正确的。这一定是cmd.exe 的变化,我没有发现。我将删除我的 cmets - 幸运的是我没有投反对票。 :-) 感谢您的更正;我总是喜欢学习东西,即使我在这个过程中被证明是错误的。 (和 +1,当我在它的时候。)
  • 启动命令行实用程序以使用 C# 组合文件的内容?你在开玩笑吗?
  • 这是一种 LAME 方法。我怀疑它会比 OP 的代码更好,它涉及启动一个可能有开销的新进程,没有像样的错误处理选项(退出代码不是一个好的选择)。除此之外,它看起来很古老。跛脚。
  • 我永远不会启动一个将未经处理的输入作为参数的进程。
【解决方案3】:

我会使用 BlockingCollection 进行读取,以便您可以同时读取和写入。
显然应该写入单独的物理磁盘以避免硬件争用。 此代码将保留顺序。
读取将比写入快,因此无需并行读取。
同样,由于读取速度会更快,因此限制了集合的大小,因此读取不会比写入更远。
在写入电流的同时并行读取单个 next 的简单任务存在文件大小不同的问题 - 写入小文件比读取大文件快。

我使用这种模式在 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

【讨论】:

  • 如果输入和输出来自同一个磁盘,那么每次读取都必须等待(或者会因为写入而变慢)...
  • 代码太多,任务太少。多线程不会帮助将磁盘 RW 磁头一分为二
  • @StenPetrov “显然应该写入单独的物理磁盘以避免硬件争用”的哪一部分不清楚?
  • @Blam 所以除了你在这里写的东西之外,我们还得写另一篇写到单个磁盘的文章?
  • @StenPetrov 代码不会在单个磁盘上失败。通过读写缓存,它甚至可能会得到一些并行。我不会针对单个磁盘进行不同的优化。所以你会以不同的方式做 - 从你的回答中可以清楚地看到。
【解决方案4】:

你可以做的几件事:

  • 根据我的经验,默认缓冲区大小可以增加到大约 120K,带来显着的好处,我怀疑在所有流上设置一个大缓冲区将是最简单和最显着的性能提升:

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

  • 将内容读入大缓冲区,立即将它们转储到输出流中 — 这将加快小文件操作。
  • 不需要多余的关闭/处置:您有using 语句。

【讨论】:

    【解决方案5】:

    sergey-brunov 发布的用于合并 2GB 文件的尝试解决方案。系统为此工作占用了大约 2 GB 的 RAM。我进行了一些更改以进行更多优化,现在合并 2GB 文件需要 350MB RAM。

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

    【讨论】:

      【解决方案6】:
          // 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;
              }
          }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-10-15
        • 2013-01-09
        • 2017-08-17
        • 2021-05-28
        • 2014-11-06
        • 2015-01-22
        相关资源
        最近更新 更多