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

时间:2010-08-28 03:17:55

标签: c# .net python file

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

import linecache
print linecache.getline(filename, linenumber)

投掷MemoryError。有趣的是the manual says “这个函数永远不会抛出异常。”在这个大文件中,当我尝试读取第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);

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

5 个答案:

答案 0 :(得分:8)

这是我在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内存!手册应该说的是“如果无法访问文件,该函数将永远不会抛出异常。”

答案 1 :(得分:6)

好吧,内存可以在任何时候,异步和不可预测地耗尽 - 这就是为什么“从不例外”的承诺并不真正适用于那里(就像在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),并且返回值是无效行号的空字符串。

稍微快一点(而且 lot 更复杂)就是一次读取100 MB(二进制模式)到缓冲区;计算缓冲区中的行尾数(只对缓冲区字符串对象进行.count('\n')调用);一旦线端的运行总数超过您正在寻找的亚麻,请找到当前位于缓冲区中的第N个线端(其中Nlinenum之间的差异,此处基于1,并且如果N+1 st line-end不在缓冲区中(因为这是你的行结束的那一点),读取更多一点,读取更多一点,提取相关的子字符串。不只是几行with并返回异常情况......; - )。

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

$ wc kjv.txt 
  114150  821108 4834378 kjv.txt

平台是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只是为了确认当然的正确性(并做; - )。

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

$ 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来分摊常量开销;-)读取特定行的程序应该具有相似的性能(即使它需要更多的处理一次它找到了感兴趣的缓冲区,对于给定大小的缓冲区来说,它是一个大致常量处理量 - 可能是重复减半缓冲区以识别它的足够小部分,然后在乘法减半的“缓冲区余数”的大小上线性的一点努力。)

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

答案 2 :(得分:1)

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

答案 3 :(得分:0)

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

答案 4 :(得分:0)

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

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