这个小程序找到文件中十大最常用的单词.您将如何通过逐行流式处理文件来优化此方法,还是将它保持为现在的功能样式?
static void Main(string[] args) { string path = @"C:\tools\copying.txt"; File.ReadAllText(path) .Split(' ') .Where(s => !string.IsNullOrEmpty(s)) .GroupBy(s => s) .OrderByDescending(g => g.Count()) .Take(10) .ToList() .ForEach(g => Console.WriteLine("{0}\t{1}", g.Key, g.Count())); Console.ReadLine(); }
这是我想使用的行读者:
static IEnumerableReadLinesFromFile(this string filename) { using (StreamReader reader = new StreamReader(filename)) { while (true) { string s = reader.ReadLine(); if (s == null) break; yield return s; } } }
编辑:
我意识到顶级单词的实现没有考虑到标点符号和所有其他细微差别,我并不太担心.
澄清:
我对解决方案感兴趣,它不会立即将整个文件加载到内存中.我想你需要一个数据结构,可以在运行中获取一系列单词和"组" - 就像一个trie.然后以某种方式以懒惰的方式完成它,以便线路阅读器可以逐行进行业务.我现在意识到这要求很多,并且比我上面给出的简单例子复杂得多.也许我会试一试,看看我是否可以像上面那样清楚地获得代码(带有一堆新的lib支持).
所以你要说的是你想要的:
full text -> sequence of words -> rest of query
至
sequence of lines -> sequence of words -> rest of query
是?
这似乎很简单.
var words = from line in GetLines() from word in line.Split(' ') select word; and then words.Where( ... blah blah blah
或者,如果您希望始终使用"流畅"样式,则可以使用SelectMany()方法.
我个人不会一气呵成.我会进行查询,然后编写一个foreach循环.这样,查询就没有副作用,并且副作用位于它们所属的循环中.但是有些人似乎更喜欢将他们的副作用放入ForEach方法中.
更新:有一个问题是这个查询是多么"懒惰".
你是对的,你最终得到的是文件中每个单词的内存表示; 但是,通过我对它的轻微重组,你至少不必创建一个包含整个文本的大字符串; 你可以逐行完成.
有很多方法可以减少这里有多少重复,我们将在一分钟内完成.但是,我想继续谈谈如何推理懒惰.
考虑这些事情的一个好方法是由于Jon Skeet,我将无耻地从他那里偷走.
想象一下有一线人的舞台.他们穿着衬衫,上面写着GetLines,Split,Where,GroupBy,OrderByDescending,Take,ToList和ForEach.
ToList pokes Take.做一些事情然后用手来列出一张带有一系列单词的卡片.ToList继续戳戳直到Take说"我已经完成".此时,ToList会从已经交出的所有牌中列出一个列表,然后将第一张牌交给ForEach.下一次被戳,它会分发下一张牌.
Take做什么?每次被戳时,它会向另一张卡询问OrderByDescending,并立即将该卡交给ToList.发出十张牌后,它告诉ToList"我已经完成了".
OrderByDescending做什么?当它第一次被戳时,它捅了GroupBy.GroupBy递给他一张卡片.它继续戳GroupBy,直到GroupBy说"我已经完成".然后OrderByDescending对牌进行排序,并将第一张牌交给Take.每次接下来都会被戳,它会拿一张新牌拿走,直到Take停止询问.
GetLines,Split,Where,GroupBy,OrderByDescending,Take,ToList和ForEach
等等.你看这是怎么回事.查询运算符GetLines,Split,Where,GroupBy,OrderByDescending,Take是懒惰的,因为它们在戳之前不会执行.他们中的一些人(OrderByDescending,ToList,GroupBy)需要多次捅他们的卡提供商,然后才能回应那些戳他们的人.他们中的一些人(GetLines,Split,Where,Take)只在他们自己被戳时才戳他们的提供者.
完成ToList后,ForEach会调用ToList.ToList hands ForEach从其列表中取出一张卡片.Foreach计算单词,然后在白板上写下单词和计数.ForEach继续戳ToList,直到ToList说"不再".
(请注意,在查询中完全没有ToList;它所做的就是将前十名的结果累积到一个列表中.ForEach可以直接与Take交谈.)
现在,至于你是否可以进一步减少内存占用的问题:是的,你可以.假设文件是"foo bar foo blah".您的代码构建了一组组:
{ { key: foo, contents: { foo, foo } }, { key: bar, contents: { bar } }, { key: blah, contents: { blah } } }
然后按内容列表的长度排序,然后进入前十.您不必在内容列表中存储那么多内容,以便计算您想要的答案.你真正想要存储的是:
{ { key: foo, value: 2 }, { key: bar, value: 1 }, { key: blah, value: 1 } }
然后按值排序.
或者,您可以建立向后映射
{ { key: 2, value: { foo } }, { key: 1, value: { bar, blah }} }
按键排序,然后在列表上执行select-many,直到您提取了前十个单词.
您要查看的任何一个概念都是"累加器".累加器是在迭代数据结构的同时有效地"累积"关于数据结构的信息的对象."Sum"是一系列数字的累加器."StringBuilder"通常用作字符串序列的累加器.您可以编写一个累加器,当单词列表被移过时累积单词的计数.
要了解如何执行此操作,您要学习的功能是Aggregate:
http://msdn.microsoft.com/en-us/library/system.linq.enumerable.aggregate.aspx
祝好运!