代码之家  ›  专栏  ›  技术社区  ›  Gleno

读取文件特定行号的有效方法。(奖励:python手册印刷错误)

  •  6
  • Gleno  · 技术社区  · 15 年前

    我有一个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建议的方法和alex建议的方法,在100GB文件中使用第1000万行,然后将其松散地翻译成c…我从自己身上添加的唯一一件事是,首先将一个300MB的文件读取到内存中,以清除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);
    

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

    5 回复  |  直到 13 年前
        1
  •  8
  •   Gabe Timothy Khouri    15 年前

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

        2
  •  6
  •   Alex Martelli    15 年前

    嗯,记忆 可以 在任何时候,异步和不可预测地运行——这就是为什么“从不例外”的承诺并不真正适用于此(就像在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') 调用buffer string对象);一旦行尾的运行总数超过您要查找的linenum,则查找当前在缓冲区中的第n个行尾(其中 N 两者的区别是 行号 ,这里是从1开始的,前面的行尾总数),如果 N+1 ST行尾也不在缓冲区中(因为这是行尾所在的点),所以提取相关的子字符串。不仅仅是几行 with 并返回异常情况…;-)。

    编辑 :由于OP注释怀疑通过缓冲区而不是通过行来读取可能会造成性能差异,因此我解开了一段旧代码,在其中我测量了两种方法以执行某种相关任务——使用缓冲区方法计算行数、在行上执行循环,或者一次性读取内存中的整个文件(通过 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 s只是为了确认正确性,当然(和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
    

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

    当然,这个小程序所做的只是行计数,但是(一旦有足够的I/O来摊销常量开销;-)读取特定行的程序应该具有类似的性能(即使一旦找到了“兴趣缓冲区”,它就需要更多的处理),这大概是 常数 对给定大小的缓冲区的处理量——大概是重复地将缓冲区减半,以识别其中足够小的部分,然后再对减半后的“缓冲区余数”的大小进行一点线性处理。

    优雅的?不是真的…但是,对于速度来说,很难打败!-)

        3
  •  1
  •   Adam Schmideg    14 年前

    你可以试试这个SED一行程序: sed '42q;d' 取第42行。它不在python或c中,但我想您已经在Mac上使用过了。

        4
  •  0
  •   VinayC    15 年前

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

        5
  •  0
  •   Matt Razza    15 年前

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

    通过浏览整个文件一次并记录行开始的位置(例如在sqlite数据库中),可以创建索引。然后,当您需要转到一个特定的行时,您查询该行的索引,找到该位置并读取该行。