我有一个100 GB的文本文件,这是一个来自数据库的BCP转储.当我尝试导入它时BULK INSERT
,我在行号219506324上得到了一个神秘的错误.在解决这个问题之前我想看看这行,但是我最喜欢的方法是
import linecache print linecache.getline(filename, linenumber)
扔了一个MemoryError
.有趣的是手册上说的是"此功能将永远不会抛出异常." 在这个大文件中,当我尝试读取第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内存!本手册应该说的是" 如果无法访问文件,该函数将永远不会抛出异常".
好吧,内存可以在任何时间,异步和不可预测地耗尽 - 这就是为什么"从不例外"的承诺并不真正适用于那里(就像在Java中一样,每个方法必须指定它可以引发哪些异常,一些例外可以免除此规则,因为几乎任何方法都可以在任何时候由于资源稀缺或其他系统问题而无法预测.
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')
对缓冲区字符串对象的调用); 一旦线端的运行总数超过你正在寻找的亚麻,找到当前在缓冲区中的第N个线端(这里N
是区别的linenum
,这里是从1开始的,和之前运行的线端的总数),读取如果N+1
st line-end也不在缓冲区中(因为这是你的行结束的点),则更多一点,提取相关的子字符串.with
对于异常情况而言,不仅仅是几行并且返回... ;-).
编辑:由于OP评论怀疑按缓冲区而不是按行读取可能会产生性能差异,因此我将一段旧代码无根据,我正在测量两种方法来处理有些相关的任务 - 计算行数使用缓冲区方法,在线上循环,或在一个gulp(by 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
s为只是为了确认过程的正确性(做;-).
当然,在几次干运行之后进行测量,以确保每个人都能从操作系统,磁盘控制器和文件系统的预读功能(如果有)中获益:
$ 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来分摊常量开销;-)读取特定行的程序应具有相似的性能(即使它一旦找到它就需要更多处理)对于给定大小的缓冲区,"感兴趣的缓冲区,它是一个大致恒定的处理量 - 可能是重复减半缓冲区以识别它的足够小的部分,然后在一定程度上线性地调整大小乘以二的"缓冲余数").
优雅?不是真的...但是,为了速度,很难被击败! - )