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

2024-04-19 11:16:34 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个100GB的文本文件,它是来自数据库的BCP转储文件。当我试图用BULK INSERT导入它时,在第219506324行出现一个隐藏错误。在解决这个问题之前,我想看看这句台词,可惜我最喜欢的方法是

import linecache
print linecache.getline(filename, linenumber)

正在抛出一个MemoryError。有趣的是the manual says“这个函数永远不会抛出异常。”在这个大文件上,当我试图读取第1行时,它抛出了一个异常,而我有大约6GB的可用内存。。。

我想知道到那条无法到达的线的最优雅的方法是什么。可用的工具有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文件中查找第1000万行,然后使用Alex建议的方法,我将其松散地转换为C#。。。我唯一要做的是,首先将一个300 MB的文件读入内存,以便清除HDD缓存。

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);

所以除非亚历克斯愿意发表评论,否则我会把加布的解决方案标记为被接受。


Tags: 文件the方法newvarbuffermethod建议
3条回答

好吧,内存可以在任何时间异步和不可预测地耗尽——这就是为什么“从不例外”的承诺并没有真正应用于此(就像,比如说,在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个行尾(其中Nlinenum之间的区别,这里是基于1的,与之前运行的行结束总数不同),如果N+1st行结束不在缓冲区中(因为这是行结束的点),则读取更多内容,提取相关子字符串。不仅仅是两行with的网络,而且返回异常情况…;-)。

编辑:由于操作注释怀疑按缓冲区而不是按行读取会影响性能,所以我打开了一段旧代码,在这里我正在测量两种方法,以执行一些相关的任务——用缓冲区方法计算行数,行上循环,或者一口气读取内存中的整个文件(通过readlines)。目标文件是kjv.txt,詹姆斯国王版本圣经的标准英文文本,每节一行,ASCII:

$ wc kjv.txt 
  114150  821108 4834378 kjv.txt

平台是MacOS Pro笔记本电脑,OSX 10.5.8,Intel Core 2 Duo,2.4 GHz,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只是为了确认课程的正确性(和do;-)。

当然,在经过几次测试之后,为了确保每个人都能从操作系统、磁盘控制器和文件系统的预读功能(如果有的话)中获得平等的收益,需要进行以下测量:

$ 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

而且,正如所预测的,按缓冲区的方法几乎是线性的。外推(当然,这总是一项冒险的工作;-),每秒小于200MB似乎是可预测的性能——称之为每GB 6秒,或者对于100GB可能10分钟。

当然,这个小程序所做的只是行计数,但是(一旦有足够的I/O来分摊恒定的开销;-)一个读取特定行的程序应该有类似的性能(即使它在找到感兴趣的“缓冲区”后需要更多的处理,对于给定大小——假设重复对半缓冲区以识别足够小的一部分,然后一点点努力线性地乘以对半“缓冲区余数”的大小。

优雅?不是真的。。。但是,为了速度,很难打败!-)

您可以尝试使用sed一行:sed '42q;d'来获取第42行。它不是用Python或C#编写的,但我想您已经在Mac上使用了sed。

这是我的优雅版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的RAM!手册应该说的是“如果这个函数不能访问文件,它将永远不会抛出异常

相关问题 更多 >