【问题标题】:Most efficient way to search the last X lines of a file?搜索文件最后 X 行的最有效方法?
【发布时间】:2010-09-20 14:10:00
【问题描述】:

我有一个文件,但我不知道它会有多大(它可能非常大,但大小会有很大差异)。我想搜索最后 10 行左右,看看它们是否与字符串匹配。我需要尽可能快速有效地执行此操作,并且想知道是否有比以下更好的方法:

s = "foo"
last_bit = fileObj.readlines()[-10:]
for line in last_bit:
    if line == s:
        print "FOUND"

【问题讨论】:

标签: python file search


【解决方案1】:

这会将最后 10 行作为列表返回,然后您可以轻松搜索您的行。 (兼容 Python 3)

def read_last_n_lines_new(lines_need=10):

    with open('Log.txt', 'rb') as f:
        f.seek(0, 2)
        data = []
        lines_found = 0
        while True:
            try:
                f.seek(-1, 1)
            except:
                break
            finally:
                c = f.read(1)
                f.seek(-1, 1)
            if c == b'\n':
                lines_found = lines_found+1
            if lines_found > lines_need or not c:
                break
            data.insert(0, c.decode('utf-8'))
            
        
        lines = []
        cur = ""
        for l in data:
            if(l == '\n'):
                lines.append(cur)
                cur = ''
            else:
                cur = cur + l
        return lines

【讨论】:

  • 这个问题已经有很多答案了,这个有用和/或比其他更好吗?
  • @MarcMush 大多数答案都是分块读取文件。我一次读取文件 1 个字节,并且函数在读取最后 n 行时停止,所以我想它应该很快。
【解决方案2】:

这里有一个类似于 MizardX 的答案,但没有明显的问题,即在添加块时重复重新扫描工作字符串以寻找换行符,在最坏的情况下花费二次时间。

与 Active State 解决方案(也似乎是二次的)相比,这不会在给定一个空文件的情况下崩溃,并且每个块读取一次而不是两次。

与生成“尾巴”相比,这是独立的。 (但如果你有'尾巴'是最好的。)

与从末尾抓取几 kB 并希望它足够,这适用于任何行长。

import os

def reversed_lines(file):
    "Generate the lines of file in reverse order."
    part = ''
    for block in reversed_blocks(file):
        for c in reversed(block):
            if c == '\n' and part:
                yield part[::-1]
                part = ''
            part += c
    if part: yield part[::-1]

def reversed_blocks(file, blocksize=4096):
    "Generate blocks of file's contents in reverse order."
    file.seek(0, os.SEEK_END)
    here = file.tell()
    while 0 < here:
        delta = min(blocksize, here)
        here -= delta
        file.seek(here, os.SEEK_SET)
        yield file.read(delta)

按要求使用:

from itertools import islice

def check_last_10_lines(file, key):
    for line in islice(reversed_lines(file), 10):
        if line.rstrip('\n') == key:
            print 'FOUND'
            break

编辑: 将 head() 中的 map() 更改为 itertools.imap()。 编辑 2: 简化了 reversed_blocks()。 编辑 3: 避免重新扫描尾部以查找换行符。 编辑 4: 重写了 reversed_lines(),因为 str.splitlines() 忽略了结尾的 '\n',正如 BrianB 注意到的那样(谢谢)。

请注意,在非常旧的 Python 版本中,此处循环中的字符串连接将花费二次时间。至少最近几年的 CPython 自动避免了这个问题。

【讨论】:

  • 非常好——我仔细阅读了答案列表,直到我来到这里,知道最好的答案是任何一个足够精明的可以使用 yield 指令
  • 为你修复了一个角落案例——有时一个块以换行符结尾,所以尾部是它自己的条目。
  • @BrianB,谢谢——你能给出一个我的代码中断的测试用例吗?我已恢复您的更改,因为它在我尝试的第一件事上失败了,'\nhello\n\nworld\n'(blocksize 设置为 2)。 (我的感谢并不讽刺,因为我希望您注意到我的代码失败的真实案例。)
  • @BrianB,我想我明白了你所看到的,而且重写整个函数似乎最好,唉。完成。
  • 有效,但它的速度大约是以前版本的一半(已更正)。
【解决方案3】:

感谢 18 Darius Bacon 的解决方案,但实现速度提高了 30%,并包装到 io.BaseIO 类中。

class ReverseFile(io.IOBase):
    def __init__ (self, filename, headers=1):
        self.fp = open(filename)
        self.headers = headers
        self.reverse = self.reversed_lines()
        self.end_position = -1
        self.current_position = -1

    def readline(self, size=-1):
        if self.headers > 0:
            self.headers -= 1
            raw = self.fp.readline(size)
            self.end_position = self.fp.tell()
            return raw

        raw = next(self.reverse)
        if self.current_position > self.end_position:
            return raw

        raise StopIteration

    def reversed_lines(self):
        """Generate the lines of file in reverse order.
        """
        part = ''
        for block in self.reversed_blocks():
            block = block + part
            block = block.split('\n')
            block.reverse()
            part = block.pop()
            if block[0] == '':
                block.pop(0)

            for line in block:
                yield line + '\n'

        if part:
            yield part

    def reversed_blocks(self, blocksize=0xFFFF):
        "Generate blocks of file's contents in reverse order."
        file = self.fp
        file.seek(0, os.SEEK_END)
        here = file.tell()
        while 0 < here:
            delta = min(blocksize, here)
            here -= delta
            file.seek(here, os.SEEK_SET)
            self.current_position = file.tell()
            yield file.read(delta)

一个例子

rev = ReverseFile(filename)
for i, line in enumerate(rev):
        print("{0}: {1}".format(i, line.strip()))

【讨论】:

    【解决方案4】:

    我采纳了mhawke的建议使用mmap,并写了一个使用rfind的版本:

    from mmap import mmap
    import sys
    
    def reverse_file(f):
        mm = mmap(f.fileno(), 0)
        nl = mm.size() - 1
        prev_nl = mm.size()
        while nl > -1:
            nl = mm.rfind('\n', 0, nl)
            yield mm[nl + 1:prev_nl]
            prev_nl = nl + 1
    
    def main():
        # Example usage
        with open('test.txt', 'r+') as infile:
            for line in reverse_file(infile):
                sys.stdout.write(line)
    

    【讨论】:

      【解决方案5】:

      也许这可能有用:

      import os.path
      
      path = 'path_to_file'
      os.system('tail -n1 ' + path)
      

      【讨论】:

      • 可能是一种方式,但它不便携。
      【解决方案6】:
      # Tail
      from __future__ import with_statement
      
      find_str = "FIREFOX"                    # String to find
      fname = "g:/autoIt/ActiveWin.log_2"     # File to check
      
      with open(fname, "r") as f:
          f.seek (0, 2)           # Seek @ EOF
          fsize = f.tell()        # Get Size
          f.seek (max (fsize-1024, 0), 0) # Set pos @ last n chars
          lines = f.readlines()       # Read to end
      
      lines = lines[-10:]    # Get last 10 lines
      
      # This returns True if any line is exactly find_str + "\n"
      print find_str + "\n" in lines
      
      # If you're searching for a substring
      for line in lines:
          if find_str in line:
              print True
              break
      

      【讨论】:

      • “if len(l)
      • @Darius: 我的意思是如果 len(l) > 10, 固定
      • lines[:-10] 删除最后 10 行。你想要的是行[-10:]。
      • @MizardX / @ΤZΩΤZΙΟΥ:你是对的,当然。感谢错误修复/评论
      • 如果文件有很长的行,这将失败。该代码假定最后 10 行属于最后 1k 数据。应该检查至少有 11 行,或者继续向后搜索直到条件为真。
      【解决方案7】:

      您还可以在反向浏览文件时计算行数,而不是猜测字节偏移量。

      lines = 0
      chunk_size = 1024
      
      f = file('filename')
      f.seek(0, 2)
      f.seek(f.tell() - chunk_size)
      
      while True:
          s = f.read(chunk_size)
          lines += s.count('\n')
          if lines > NUM_OF_LINES:
              break
          f.seek(f.tell() - chunk_size*2)
      

      现在文件处于运行readlines() 的好位置。您还可以缓存您第一次读取的字符串,以消除两次​​读取文件的相同部分。

      【讨论】:

        【解决方案8】:

        此解决方案将只读取一次文件,但使用 2 个文件对象指针无需重新读取即可获得文件的最后 N 行:

        def getLastLines (path, n):
            # return the las N lines from the file indicated in path
        
            fp = open(path)
            for i in range(n):
                line = fp.readline()
                if line == '':
                    return []
        
            back = open(path)
            for each in fp:
                back.readline()
        
            result = []
            for line in back:
                result.append(line[:-1])
        
            return result
        
        
        
        
        s = "foo"
        last_bit = getLastLines(r'C:\Documents and Settings\ricardo.m.reyes\My Documents\desarrollo\tail.py', 10)
        for line in last_bit:
            if line == s:
                print "FOUND"
        

        【讨论】:

          【解决方案9】:

          我认为阅读文件的最后 2 KB 左右应该可以确保你得到 10 行,并且不应该占用太多资源。

          file_handle = open("somefile")
          file_size = file_handle.tell()
          file_handle.seek(max(file_size - 2*1024, 0))
          
          # this will get rid of trailing newlines, unlike readlines()
          last_10 = file_handle.read().splitlines()[-10:]
          
          assert len(last_10) == 10, "Only read %d lines" % len(last_10)
          

          【讨论】:

          • 您应该仔细检查文件是否 >= 2KB
          【解决方案10】:

          这是一个使用mmap 的版本,看起来非常有效。最大的优点是mmap 会自动为您处理文件到内存的分页要求。

          import os
          from mmap import mmap
          
          def lastn(filename, n):
              # open the file and mmap it
              f = open(filename, 'r+')
              m = mmap(f.fileno(), os.path.getsize(f.name))
          
              nlcount = 0
              i = m.size() - 1 
              if m[i] == '\n': n += 1
              while nlcount < n and i > 0:
                  if m[i] == '\n': nlcount += 1
                  i -= 1
              if i > 0: i += 2
          
              return m[i:].splitlines()
          
          target = "target string"
          print [l for l in lastn('somefile', 10) if l == target]
          

          【讨论】:

          • 不错!我应该想到mmap。在我对一个非常大的 1 行文件的测试中,这比我的要慢一个数量级,不过,我猜是因为它在 Python 代码中逐个字符地检查。
          • 是的,我也担心“纯 Python”循环。循环可能比我提供的代码更有效。如果 mmap 对象有一个 rfind() 方法,那就更好了!
          • 仅供参考:Python v2.6.5 的 mmap 对象有一个 rfind() 方法。
          • tail -r 也将mmap 用于常规文件(请参阅r_reg() function
          【解决方案11】:

          我遇到了这个问题,解析了最后一小时的 LARGE syslog 文件,并从 activestate 的配方站点使用了这个函数... (http://code.activestate.com/recipes/439045/)

          !/usr/bin/env python
          # -*-mode: python; coding: iso-8859-1 -*-
          #
          # Copyright (c) Peter Astrand <astrand@cendio.se>
          
          import os
          import string
          
          class BackwardsReader:
              """Read a file line by line, backwards"""
              BLKSIZE = 4096
          
              def readline(self):
                  while 1:
                      newline_pos = string.rfind(self.buf, "\n")
                      pos = self.file.tell()
                      if newline_pos != -1:
                          # Found a newline
                          line = self.buf[newline_pos+1:]
                          self.buf = self.buf[:newline_pos]
                          if pos != 0 or newline_pos != 0 or self.trailing_newline:
                              line += "\n"
                          return line
                      else:
                          if pos == 0:
                              # Start-of-file
                              return ""
                          else:
                              # Need to fill buffer
                              toread = min(self.BLKSIZE, pos)
                              self.file.seek(-toread, 1)
                              self.buf = self.file.read(toread) + self.buf
                              self.file.seek(-toread, 1)
                              if pos - toread == 0:
                                  self.buf = "\n" + self.buf
          
              def __init__(self, file):
                  self.file = file
                  self.buf = ""
                  self.file.seek(-1, 2)
                  self.trailing_newline = 0
                  lastchar = self.file.read(1)
                  if lastchar == "\n":
                      self.trailing_newline = 1
                      self.file.seek(-1, 2)
          
          # Example usage
          br = BackwardsReader(open('bar'))
          
          while 1:
              line = br.readline()
              if not line:
                  break
              print repr(line)
          

          它工作得非常好,比 fileObj.readlines()[-10:] 之类的任何东西都要高效得多,它使 python 将整个文件读入内存,然后将最后十行删除。

          【讨论】:

          • 如果 bar 为空则失败。我也遇到了一些麻烦。
          【解决方案12】:

          首先是一个返回列表的函数:

          def lastNLines(file, N=10, chunksize=1024):
              lines = None
              file.seek(0,2) # go to eof
              size = file.tell()
              for pos in xrange(chunksize,size-1,chunksize):
                  # read a chunk
                  file.seek(pos,2)
                  chunk = file.read(chunksize)
                  if lines is None:
                      # first time
                      lines = chunk.splitlines()
                  else:
                      # other times, update the 'first' line with
                      # the new data, and re-split
                      lines[0:1] = (chunk + lines[0]).splitlines()
                  if len(lines) > N:
                      return lines[-N:]
              file.seek(0)
              chunk = file.read(size-pos)
              lines[0:1] = (chunk + lines[0]).splitlines()
              return lines[-N:]
          

          其次,一个以倒序遍历行的函数:

          def iter_lines_reversed(file, chunksize=1024):
              file.seek(0,2)
              size = file.tell()
              last_line = ""
              for pos in xrange(chunksize,size-1,chunksize):
                  # read a chunk
                  file.seek(pos,2)
                  chunk = file.read(chunksize) + last_line
                  # split into lines
                  lines = chunk.splitlines()
                  last_line = lines[0]
                  # iterate in reverse order
                  for index,line in enumerate(reversed(lines)):
                      if index > 0:
                          yield line
              # handle the remaining data at the beginning of the file
              file.seek(0)
              chunk = file.read(size-pos) + last_line
              lines = chunk.splitlines()
              for line in reversed(lines):
                  yield line
          

          你的例子:

          s = "foo"
          for index, line in enumerate(iter_lines_reversed(fileObj)):
              if line == s:
                  print "FOUND"
                  break
              elif index+1 >= 10:
                  break
          

          编辑:现在自动获取文件大小
          Edit2:现在只迭代 10 行。

          【讨论】:

          • 小窍门:查了 10 行不成功,你不会停止搜索。
          • 是的,这很酷。请参阅我的答案以了解另一种更可重用的方法。
          • 虽然,现在看,我意识到我的 head() 函数会遍历所有 10 行,即使更快地找到密钥;我应该使用 itertools.imap() 而不是 map()。
          【解决方案13】:

          如果您使用的是 unix 机器,os.popen("tail -10 " + filepath).readlines() 可能是最快的方法。否则,这取决于您希望它有多强大。到目前为止提出的方法都会以一种或另一种方式失败。对于最常见情况下的稳健性和速度,您可能需要类似对数搜索:使用 file.seek 转到文件末尾减去 1000 个字符,读入,检查它包含多少行,然后到 EOF 减去 3000 个字符,读入 2000 个字符,计算行数,然后 EOF 减去 7000,读入 4000 个字符,计算行数,等等,直到你拥有所需的行数。但是,如果您确定它总是会在具有合理行长的文件上运行,那么您可能不需要它。

          您还可以在 source code 中找到一些灵感,用于 unix tail 命令。

          【讨论】:

            【解决方案14】:

            如果您在 POSIX 系统上运行 Python,则可以使用“tail -10”检索最后几行。这可能比编写自己的 Python 代码来获取最后 10 行要快。不是直接打开文件,而是从命令“tail -10 文件名”打开管道。但是,如果您确定日志输出(例如,您知道 从不 任何非常长的数百或数千个字符的行)然后使用“读取最后 2KB”之一列出的方法就可以了。

            【讨论】:

            • 我会谨慎处理这一点,因为 shell 调用的开销比直接访问要大得多。
            • 这已经很老了,但我实际上并不是在提倡使用 shell 调用。我建议使用 tail 的管道输出调用脚本,而不是调用脚本来读取整个文件本身。
            【解决方案15】:

            当我不得不做类似的事情时,我想我记得改编 this blog post from Manu Garg 的代码。

            【讨论】:

              【解决方案16】:

              就我个人而言,我很想突破到 shell 并调用 tail -n10 来加载文件。但是我并不是真正的 Python 程序员 ;)

              【讨论】:

                【解决方案17】:

                您可以从文件末尾读取大约 1,000 字节的块到缓冲区中,直到有 10 行。

                【讨论】:

                  【解决方案18】:

                  读取文件的最后几 Ks,并将其分成几行以仅返回最后 10 个。

                  该块的开头不太可能落在行边界上,但无论如何您都会丢弃第一行。

                  【讨论】:

                    猜你喜欢
                    • 2019-08-24
                    • 1970-01-01
                    • 1970-01-01
                    • 2018-03-29
                    • 2014-01-22
                    • 1970-01-01
                    • 2011-10-23
                    • 2012-01-11
                    • 1970-01-01
                    相关资源
                    最近更新 更多