如何在C#中高效地编写大型文本文件?

51

我正在用C#创建一个方法,用于生成Google产品数据源的文本文件。该数据源将包含超过30,000条记录,而文本文件目前的大小约为7Mb。

以下是我目前正在使用的代码(为了简洁起见,已删除某些行)。

public static void GenerateTextFile(string filePath) {

  var sb = new StringBuilder(1000);
  sb.Append("availability").Append("\t");
  sb.Append("condition").Append("\t");
  sb.Append("description").Append("\t");
  // repetitive code hidden for brevity ...
  sb.Append(Environment.NewLine);

  var items = inventoryRepo.GetItemsForSale();

  foreach (var p in items) {
    sb.Append("in stock").Append("\t");
    sb.Append("used").Append("\t");
    sb.Append(p.Description).Append("\t");
    // repetitive code hidden for brevity ...
    sb.AppendLine();
  }

  using (StreamWriter outfile = new StreamWriter(filePath)) {
      result.Append("Writing text file to disk.").AppendLine();
      outfile.Write(sb.ToString());
  }
}

我想知道StringBuilder是否是适合这项工作的正确工具。如果使用TextWriter,是否会有性能提升?

我对IO性能不是很了解,所以任何帮助或改进意见都将不胜感激。谢谢。


自从我写下这个问题以来,Linq2Csv项目已经诞生了。它是处理我所编写的代码的更好方式。http://nuget.org/packages/LinqToCsv - jessegavin
有没有带解决方案的完整源代码? - Kiquenet
抱歉,这是为我的一个客户编写的。你真的应该研究一下 Linq2Csv。它会让这种事情变得更容易。 - jessegavin
距离我上次在这个问题上发表评论已经将近5年了,我强烈推荐CsvHelper。https://joshclose.github.io/CsvHelper/ - jessegavin
4个回答

81

现代操作系统通常对文件 I/O 操作进行了良好的优化。您不应该尝试在内存中组装整个文件字符串,而是逐个部分地将其写出。 FileStream 会负责缓冲和其他性能考虑。

您可以通过移动来轻松进行此更改:

using (StreamWriter outfile = new StreamWriter(filePath)) {

将字符串直接写入文件,避免使用 StringBuilder 在内存中大量构建字符串有几个原因:

  1. StringBuilder 需要不断增加其容量以适应写入操作,导致内存重新分配和复制,实际上性能可能更差。
  2. 可能需要比您可以物理分配的内存更多的内存,因此可能需要使用虚拟内存(交换文件),这比 RAM 慢得多。
  3. 对于真正大的文件(> 2Gb),在 32 位平台上会耗尽地址空间,从而无法完成。
  4. 要将 StringBuilder 的内容写入文件,必须使用 ToString(),这实际上会使进程的内存消耗翻倍,因为两个副本必须在内存中一段时间。如果地址空间足够分散,无法分配单个连续的内存块,则此操作也可能失败。

不错的答案。可以尝试使用StreamWriter构造函数重载来调整,该重载允许您定义缓冲区大小... - João
嘿,谢谢你的回答!我很感激你花时间进一步解释如何处理这种情况。 - jessegavin
1
5年后... FileStream类仍然是编写7MB文本文件的最佳方法吗? - n00dles

27

using语句移至整个代码范围并直接写入文件。我认为没有必要先将其全部存储在内存中。


13

使用StreamWriter.Write逐个写入字符串而不是将所有内容缓存在StringBuilder中。


4
我非常希望你不是指让他一次只写一个 - JSBձոգչ
虽然这是一个很好的答案。我有一个大约20MB大小的文件,我面临的问题是StreamWriter实际上在末尾放置了回车符/换行符。我正在尝试删除最后的额外回车符,正如已经指出的那样,StringBuilder对于性能或大小来说并不是一个很好的解决方案。 我尝试使用StreamReader.Peek()在到达末尾之前窥视一下该行。 有什么想法吗? - petrosmm
2
@MaximusPeters,您可能已经找到了解决方法,但是也许您当时使用的是 WriteLine() 方法而不是 Write() 方法? - Stéphane Gourichon

4
这可能有些陈旧,但我有一个需要写入约1700万行的文件,所以最终我将每个批次的写入限制为每10,000行,类似于这些行。
for (i6 = 1; i6 <= ball; i6++) 
{ //this is middle of 6 deep nest ..
  counter++;
  // modus to get a value at every so often 10k lines
  divtrue = counter % 10000; // remainder operator % for 10k
  //  build the string of fields with \n at the end 
  lineout = lineout + whatever 
  // the magic 10k block here
  if (divtrue.Equals(0))  
  {
     using (StreamWriter outFile = new StreamWriter(@filepath, true))
     { 
         //  write the 10k lines with .write NOT writeline..
         outFile.Write(lineout); 
     } 
     // reset the string so we dont do silly like memory overflow
     lineout = ""; 
  }
}

在我的情况下,它比逐行处理快得多。

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