如何将一个巨大的文件分割成单词?

5

如何从文本文件中读取非常长的字符串,然后处理它(分割为单词)?

我尝试使用StreamReader.ReadLine()方法,但是我得到了一个OutOfMemory异常。显然,我的行非常长。 这是我用于读取文件的代码:

using (var streamReader = File.OpenText(_filePath))
    {

        int lineNumber = 1;
        string currentString = String.Empty;
        while ((currentString = streamReader.ReadLine()) != null)
        {

            ProcessString(currentString, lineNumber);
            Console.WriteLine("Line {0}", lineNumber);
            lineNumber++;
        }
    }

将行分成单词的代码:

var wordPattern = @"\w+";
var matchCollection = Regex.Matches(text, wordPattern);
var words = (from Match word in matchCollection
             select word.Value.ToLowerInvariant()).ToList();

你使用了什么算法/方法进行拆分操作? - byako
1
请添加您的用例,例如您要对这些单词做什么,您想要计算出现次数,获取唯一单词列表等等?这将提供进一步的优化可能性。 - Bas
@Bas,好的,我需要将行拆分为单词,并将这些单词写入另一个文件中,该文件包含此单词出现的行列表。 - Ihor Korotenko
那么我将把修改读取方法的任务交给你,让它逐行读取。 - CodeCaster
我会将 ToList()GetLowercasedWords 中移除。我看不出它除了占用内存、减慢速度和增加内存错误的风险之外还有什么作用。 - Jon Hanna
显示剩余5条评论
3个回答

5
你可以逐个字符读取,同时建立单词,使用yield使其延迟读取,这样你就不必一次性读取整个文件:
private static IEnumerable<string> ReadWords(string filename)
{
    using (var reader = new StreamReader(filename))
    {
        var builder = new StringBuilder();

        while (!reader.EndOfStream)
        {
            char c = (char)reader.Read();

            // Mimics regex /w/ - almost.
            if (char.IsLetterOrDigit(c) || c == '_')
            {
                builder.Append(c);
            }
            else
            {
                if (builder.Length > 0)
                {
                    yield return builder.ToString();
                    builder.Clear();
                }
            }
        }

        yield return builder.ToString();
    }
}

代码按字符读取文件,当遇到非单词字符时,会使用 yield return 返回此前已构建的单词(仅对于第一个非字母字符)。代码使用 StringBuilder 构建单词字符串。 Char.IsLetterOrDigit() 对于字符的行为与 正则表达式中的单词字符 w 相同,但下划线(以及其他字符)也包含在后者类别中。如果您的输入包含更多要包括的字符,则需要更改 if() 语句。

2
这种情况下,使用 StringBuilder 是否更好呢? - Brian Rasmussen
2
小心!这不等同于\w+,因为它除了空格(例如破折号、标点符号)之外什么都不处理。 - Bas
\w 还包括数字和下划线。 - Bas
在这种情况下,“word”是由[A-Za-z0-9]序列组成的。 - Ihor Korotenko
2
这个可以运行,但是CPU负载会非常大。逐字符处理真的很昂贵。更新:它每秒可以处理30MB,比我预期的要多得多。 - usr

0
将其分成适当的小节。这样,您就不必试图读取4GB的数据,我相信这大约是一个页面的大小,而是尝试读取8个500MB的块,这应该会有所帮助。

我应该说你不会将它分成精确的块,但是相对接近的块。我这么说的原因是,如果你有一个500 mb的截止点,你会想在单词的结尾或开头拆分文件,而不是中间。所以不要只是随意切割文件。要用更聪明的方式来做。 - trinityalps
你说得对。这就是我遇到困难的主要原因。 - Ihor Korotenko

0

垃圾回收可能是一个解决方案。我不确定它是否是问题的根源。但如果是这种情况,简单的GC.Collect通常是不够的,并且出于性能原因,只有在真正需要时才应该调用它。尝试以下过程,当可用内存过低(低于作为过程参数提供的阈值)时调用垃圾回收。

int charReadSinceLastMemCheck = 0 ;
using (var streamReader = File.OpenText(_filePath))
{

    int lineNumber = 1;
    string currentString = String.Empty;
    while ((currentString = streamReader.ReadLine()) != null)
    {

        ProcessString(currentString, lineNumber);
        Console.WriteLine("Line {0}", lineNumber);
        lineNumber++;
        totalRead+=currentString.Length ;
        if (charReadSinceLastMemCheck>1000000) 
        { // Check memory left every Mb read, and collect garbage if required
          CollectGarbage(100) ;
          charReadSinceLastMemCheck=0 ;
        } 
    }
}


internal static void CollectGarbage(int SizeToAllocateInMo)
{
       long [,] TheArray ;
       try { TheArray =new long[SizeToAllocateInMo,125000]; }low function 
       catch { TheArray=null ; GC.Collect() ; GC.WaitForPendingFinalizers() ; GC.Collect() ; }
       TheArray=null ;
}

@CodeCaster:你在已删除的评论中写道:“你也不应该在一些法国论坛上找到巫术代码并进行跨贴。”如果在其他论坛上发布过的代码重复发布违反了StackOverflow的原则,我会很快离开这个论坛。如果你看一下法国论坛回复的作者,你会发现他的名字也是“Graffito”。但你肯定认为这是另一个人。 - Graffito
1
Graffito,你的CollectGarbage方法真是魔鬼。我从你之前的回答中就认出它了。 - usr

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