读取文件特定行号的有效方法。 (奖金:Python手册错误打印)

我有一个100 GB的文本文件,这是一个来自数据库的BCP转储。 当我尝试用BULK INSERT导入它时,我在行号219506324上得到了一个神秘的错误。在解决这个问题之前我想看看这行,但是我最喜欢的方法是

 import linecache print linecache.getline(filename, linenumber) 

抛出MemoryError 。 有趣的是,手册说 “这个函数永远不会抛出exception。” 在这个大文件中,当我尝试读取第1行时,它会抛出一个,我有大约6GB的可用RAM …

我想知道到达那条无法到达的线路最优雅的方法是什么。 可用的工具是Python 2,Python 3和C#4(Visual Studio 2010)。 是的,我明白我总能做点什么

 var line = 0; using (var stream = new StreamReader(File.OpenRead(@"s:\source\transactions.dat"))) { while (++line < 219506324) stream.ReadLine(); //waste some cycles Console.WriteLine(stream.ReadLine()); } 

哪个会奏效,但我怀疑这是优雅的方式。

编辑:我正在等待关闭此线程,因为包含该文件的硬盘正在被另一个进程使用。 我将测试建议的方法和报告时间。 谢谢大家的建议和意见。

结果是我实施了Gabes和Alexes方法来查看哪一个更快。 如果我做错了什么,请告诉我。 我正在使用Gabe建议的方法在我的100GB文件中使用第10百万行,然后使用Alex建议的方法,我将其松散地翻译成C#…我自己添加的唯一内容是,首先阅读300 MB文件到内存只是为了清除硬盘缓存。

 const string file = @"x:\....dat"; // 100 GB file const string otherFile = @"x:\....dat"; // 300 MB file const int linenumber = 10000000; ClearHDDCache(otherFile); GabeMethod(file, linenumber); //Gabe's method ClearHDDCache(otherFile); AlexMethod(file, linenumber); //Alex's method // Results // Gabe's method: 8290 (ms) // Alex's method: 13455 (ms) 

gabe方法的实现如下:

 var gabe = new Stopwatch(); gabe.Start(); var data = File.ReadLines(file).ElementAt(linenumber - 1); gabe.Stop(); Console.WriteLine("Gabe's method: {0} (ms)", gabe.ElapsedMilliseconds); 

虽然亚历克斯的方法略显诡计:

 var alex = new Stopwatch(); alex.Start(); const int buffersize = 100 * 1024; //bytes var buffer = new byte[buffersize]; var counter = 0; using (var filestream = File.OpenRead(file)) { while (true) // Cutting corners here... { filestream.Read(buffer, 0, buffersize); //At this point we could probably launch an async read into the next chunk... var linesread = buffer.Count(b => b == 10); //10 is ASCII linebreak. if (counter + linesread >= linenumber) break; counter += linesread; } } //The downside of this method is that we have to assume that the line fit into the buffer, or do something clever...er var data = new ASCIIEncoding().GetString(buffer).Split('\n').ElementAt(linenumber - counter - 1); alex.Stop(); Console.WriteLine("Alex's method: {0} (ms)", alex.ElapsedMilliseconds); 

因此,除非亚历克斯关注评论,否则我会将Gabe的解决方案标记为已接受。

这是我在C#中的优雅版本:

 Console.Write(File.ReadLines(@"s:\source\transactions.dat").ElementAt(219506323)); 

或更一般:

 Console.Write(File.ReadLines(filename).ElementAt(linenumber - 1)); 

当然,您可能希望在给定行之前和之后显示一些上下文:

 Console.Write(string.Join("\n", File.ReadLines(filename).Skip(linenumber - 5).Take(10))); 

或者更流利:

 File .ReadLines(filename) .Skip(linenumber - 5) .Take(10) .AsObservable() .Do(Console.WriteLine); 

顺便说一句, linecache模块对大文件没有做任何巧妙的事情。 它只是读取整个内容,将其全部保存在内存中。 它捕获的唯一例外是I / O相关(无法访问文件,找不到文件等)。 这是代码的重要部分:

  fp = open(fullname, 'rU') lines = fp.readlines() fp.close() 

换句话说,它试图将整个100GB文件装入6GB内存! 本手册应该说的是“ 如果无法访问文件,该函数将永远不会抛出exception”。

好吧,内存可以在任何时间,异步和不可预测地耗尽 – 这就是为什么“永不exception”的承诺并不真正适用于那里(就像在Java中一样,每个方法必须指定它可以引发哪些exception,一些例外是免于此规则的,因为几乎任何方法,由于资源稀缺或其他系统范围的问题,不可预测地可以随时提出它们。

linecache尝试读取整个文件。 你唯一的简单选择(希望你不赶时间)是从一开始就读一行…:

 def readoneline(filepath, linenum): if linenum < 0: return '' with open(filepath) as f: for i, line in enumerate(filepath): if i == linenum: return line return '' 

在这里, linenum是从0开始的(如果你不喜欢它,你的Python是2.6或更好,将起始值1传递给enumerate ),返回值是无效行号的空字符串。

稍微快一点(也就是更复杂)就是一次读取100 MB(二进制模式)到缓冲区; 计算缓冲区中的行.count('\n') (只对缓冲区字符串对象进行.count('\n')调用); 一旦线端的运行总数超过你正在寻找的亚麻,找到当前在缓冲区中的第N个线端(其中Nlinenum之间的差异,这里是从1开始的,以及之前运行的线端的总数),如果N+1 st行结束也不在缓冲区中(因为这是行结束的点),则读取更多内容,提取相关的子字符串。 不只是几行with并且为exception情况返回... ;-)。

编辑 :由于OP评论怀疑按缓冲区而不是按行读取可能会产生性能差异,因此我将一段旧代码连根拔起,我正在测量两种方法来完成一些相关的任务 - 计算行数使用缓冲区方法,在线上循环,或在一个gulp(通过readlines )读取内存中的整个文件。 目标文件是kjv.txt ,这是King James'圣经版本的标准英文文本,每节一行,ASCII:

 $ wc kjv.txt 114150 821108 4834378 kjv.txt 

Platform是MacOS Pro笔记本电脑,OSX 10.5.8,2.4 GHz的Intel Core 2 Duo,Python 2.6.5。

测试模块, readkjv.py

 def byline(fn='kjv.txt'): with open(fn) as f: for i, _ in enumerate(f): pass return i +1 def byall(fn='kjv.txt'): with open(fn) as f: return len(f.readlines()) def bybuf(fn='kjv.txt', BS=100*1024): with open(fn, 'rb') as f: tot = 0 while True: blk = f.read(BS) if not blk: return tot tot += blk.count('\n') if __name__ == '__main__': print bybuf() print byline() print byall() 

print只是为了确认当然的正确性(并且做;-)。

当然,在几次干运行之后进行测量,以确保每个人都能从操作系统,磁盘控制器和文件系统的预读function(如果有)中获益:

 $ py26 -mtimeit -s'import readkjv' 'readkjv.byall()' 10 loops, best of 3: 40.3 msec per loop $ py26 -mtimeit -s'import readkjv' 'readkjv.byline()' 10 loops, best of 3: 39 msec per loop $ py26 -mtimeit -s'import readkjv' 'readkjv.bybuf()' 10 loops, best of 3: 25.5 msec per loop 

这些数字非常可重复。 如你所见,即使在如此小的文件(小于5 MB!)上,在线方法比基于缓冲区的方法慢 - 只是浪费了太多精力!

为了检查可伸缩性,我接下来使用了一个4倍大的文件,如下所示:

 $ cat kjv.txt kjv.txt kjv.txt kjv.txt >k4.txt $ wc k4.txt 456600 3284432 19337512 k4.txt $ py26 -mtimeit -s'import readkjv' 'readkjv.bybuf()' 10 loops, best of 3: 25.4 msec per loop $ py26 -mtimeit -s'import readkjv' 'readkjv.bybuf("k4.txt")' 10 loops, best of 3: 102 msec per loop 

并且,正如预测的那样,缓冲区方法几乎完全线性地扩展。 推断(当然总是风险很大的努力;-),每秒小于200 MB似乎是可预测的性能 - 称其为每GB 6秒,或者100 GB可能为10分钟。

当然,这个小程序所做的只是行计数,但是(一旦有足够的I / O来分摊常量开销;-)读取特定行的程序应具有相似的性能(即使它找到后需要更多处理)对于给定大小的缓冲区,“感兴趣的缓冲区,它是一个大致恒定的处理量 - 可能是重复减半缓冲区以识别它的足够小部分,然后在一定程度上线性化多重的“缓冲余数”)。

优雅? 不是真的...但是,为了速度,很难被击败! - )

你可以试试这个sed one-liner: sed '42q;d'来获取第42行。它不是在Python或C#中,但我认为你已经在你的Mac上了。

不是优雅但更快的解决方案是使用多个线程(或.NET 4.0中的任务)同时读取和处理文件的多个块。

如果您希望在同一个文件上经常需要此操作,那么创建索引是有意义的。

您可以通过遍历整个文件并记录行开头的位置来创建索引,例如在sqlite数据库中。 然后,当您需要转到特定行时,查询索引,寻找该位置并读取该行。