.NET系统在将大小为120 MB的CSV文件使用String.Split()方法分割时出现了OutOfMemoryException异常。

6

我正在使用C#读取一个大约120 MB的纯文本CSV文件。最初,我通过逐行读取来解析它,但最近发现先将整个文件内容读入内存会快多次。由于CSV中有嵌在引号内的逗号,这意味着我必须使用正则表达式进行分割,因此解析已经相当慢了。下面是唯一一个可靠的正则表达式:

string[] fields = Regex.Split(line, 
@",(?!(?<=(?:^|,)\s*\x22(?:[^\x22]|\x22\x22|\\\x22)*,)
(?:[^\x22]|\x22\x22|\\\x22)*\x22\s*(?:,|$))");
// from http://regexlib.com/REDetails.aspx?regexp_id=621

为了在将整个内容读入内存后进行解析,我使用换行符对字符串进行拆分,以获取包含每行的数组。然而,当我在120MB文件上执行此操作时,会出现System.OutOfMemoryException。为什么我的计算机有4GB RAM,但它很快就会耗尽内存?有没有更好的方法来快速解析复杂的CSV文件?

9个回答

8

如果不是必须的话,不要自己编写解析器。我曾经使用过这个来解析CSV文件:

快速CSV读取器

至少你可以研究一下别人是怎么做的。


1
我也给一个赞。根据我的经验,Sébastien Lorion的CSV阅读器高效、灵活且稳定。它可以在短时间内处理掉一个120MB的文件。 - LukeH

7
基本上,无论分配的大小如何,都可能会出现OutOfMemoryException。当您分配一段内存时,实际上是在请求所需大小的连续内存块。如果无法满足该请求,则会出现OutOfMemoryException。
您还应该知道,除非您运行64位Windows,否则您的4 GB RAM将分为2 GB内核空间和2 GB用户空间,因此您的.NET应用程序默认情况下无法访问超过2 GB的内存。
在.NET中进行字符串操作时,由于.NET字符串是不可变的,因此您可能会冒着创建大量临时字符串的风险。因此,您可能会看到内存使用量急剧上升。

字符串是计算机科学中的私生子。它们是必要的邪恶,但我仍然希望有人能够找出更好的方法! - Darren Kopp

5
如果您已将整个文件读入字符串,则应该使用StringReader
StringReader reader = new StringReader(fileContents);
string line;
while ((line = reader.ReadLine()) != null) {
    // Process line
}

这应该与从文件流式传输大致相同,唯一的区别在于内容已经在内存中。

测试后编辑

我尝试了上述方法处理一个140MB的文件,其中处理过程包括将行长度变量递增line.Length。这在我的电脑上耗时约1.6秒。此后,我尝试了以下操作:

System.IO.StreamReader reader = new StreamReader("D:\\test.txt");
long length = 0;
string line;
while ((line = reader.ReadLine()) != null)
    length += line.Length;

结果大约为1秒钟。

当然,你的情况可能会有所不同,特别是如果你从网络驱动器读取或者处理时间足够长以至于硬盘需要寻找其他地方。但是,如果你正在使用FileStream读取文件并且没有缓冲,则也可能会有所不同。StreamReader提供了缓冲,这可以极大地增强读取能力。


如果他能够将文件读入字符串中,那么这就是一个相当不错的答案,至少目前看起来是这样。我不会感到惊讶,如果许多机器在尝试加载120MB文件时立即失败(或有时失败,有时工作)。 - mqp

4

您可能无法分配如此大的连续内存块,也不应该期望能够这样做。流式传输是处理此类问题的常规方式,但您说得对,它可能会慢一些(虽然我认为通常不应该慢那么多)。

为了达成折中方案,您可以尝试使用像StreamReader.ReadBlock()这样的功能一次读取文件的较大部分(但仍不是整个文件),并依次处理每个部分。


1
正如其他帖子所说,OutOfMemory是因为找不到所请求大小的连续内存块。
但是,您说逐行解析比一次性读取并进行处理要快几倍。只有在您追求阻塞读取的朴素方法时,这才有意义,例如(伪代码):
while(! file.eof() )
{
    string line = file.ReadLine();
    ProcessLine(line);
}

你应该使用流式处理,其中你的流是由另一个线程通过Write()调用从文件中读取填充的,这样文件读取就不会被ProcessLine()所阻塞,反之亦然。这应该与一次性读取整个文件然后进行处理的性能相当。

你能给一个多线程方法的代码示例吗?我之前一直用的是天真的方式,现在我明白了为什么那样做可能会成为一个重大问题。 - Craig W
.Net具有内置的异步文件读写功能,一个很好的起点是BeginRead()调用。以下Google搜索结果有许多示例:http://www.google.com/search?q=.net+asynchronous+file - Not Sure

0

你应该将一块数据读入缓冲区并对其进行处理。然后再读取另一块数据,以此类推。

有许多库可以高效地为您完成这项工作。我维护了一个名为CsvHelper的库。您需要处理很多边缘情况,例如当逗号或行结束符在字段中间时。


0

你应该尝试使用CLR分析器来确定你的实际内存使用情况。可能存在除了系统RAM之外的内存限制。例如,如果这是一个IIS应用程序,则你的内存受应用程序池的限制。

通过这个分析信息,你可能会发现需要使用更可扩展的技术,比如最初尝试的CSV文件流式传输。


0

你的内存不足是在栈上,而不是堆上。

你可以尝试重新设计你的应用程序,使得你可以将输入数据分成更易处理的“块”,而不是一次性处理120MB。


字符串被分配在堆上,而不是栈上。只有int/byte/double等原始类型才会被分配在栈上。 - Not Sure
@不确定:你是正确的。然而,在程序堆栈可能会填满的各种非明显情况下,这种情况是很常见的。鉴于所讨论的系统具有充足的物理内存,我认为这可能是其中之一。=) - Garrett
堆栈填满会导致 StackOverflowException,而不是 OutOfMemoryException;后者总是用于指示 GC 堆上的内存不足。 - Not Sure

0

我同意这里的大多数人,你需要使用流式处理。

我不知道是否有人已经说过了,但你应该看一下扩展方法。

而且我知道,毫无疑问,.NET / CLR 上最好的 CSV 分割技术是this one

那种技术让我从输入的 CSV 中生成了超过 10GB 的 XML 输出,包括广泛的输入过滤器等等,比我见过的任何其他技术都要快。


没错,而且,无论如何,流式传输 > 缓冲到你的 RAM 中。 想一想,如果你有 4GIG,然后加载了 2GIG 的输入,仅仅是加载时间和 VM 子系统重新定位页面以及页面表的巨大大小就会耗尽你的 CPU 缓存等等...在一个小而易于管理的工作空间内进行输入/输出,可以保持你的缓存“热”,并且你所有的 CPU 时间都专注于手头的任务,而不是系统负载的巨大波动... - RandomNickName42

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