【问题标题】:Efficiently finding the last line in a text file [duplicate]有效地查找文本文件中的最后一行 [重复]
【发布时间】:2011-11-02 06:42:29
【问题描述】:

我需要从许多非常大(数百兆字节)的文本文件中提取最后一行以获取某些数据。目前,我正在使用 python 循环遍历所有行,直到文件为空,然后处理返回的最后一行,但我确信有一种更有效的方法可以做到这一点。

使用 python 仅检索文本文件的最后一行的最佳方法是什么?

【问题讨论】:

  • 你可以看看:stackoverflow.com/questions/136168/… 真的很接近你的需要。
  • 这是一个 Python 问题,还是使用 awk 或 sed 的答案也一样好?
  • 您需要提供一条重要信息(许多答案完全忽略了它):文件的编码。
  • 只有多字节编码(例如 UTF-16 或 UTF-32)会破坏给定的算法。
  • 谢谢,这让我非常接近,一点点调整就得到了我所需要的。

标签: python text


【解决方案1】:
lines = file.readlines()
fileHandle.close()
last_line = lines[-1]

【讨论】:

  • 啊!永远不要这样做lines[len(lines) -1]。这是一个O(n) 操作。 lines[-1] 会得到最后一个。此外,这并不比他已经使用的方法更好。
  • 糟糕,我的错误!不过这种方法实际上效率更高。
  • @gddc: lines[len(lines)-1] 不是 O(n) (除非 lines 是用户定义的类型,具有 __len__ 的 O(n) 实现,但这里不是这种情况) .虽然它的风格很糟糕,但lines[len(lines)-1] 的运行时间成本与lines[-1] 几乎相同;唯一的区别是索引计算是在脚本中显式完成还是由运行时隐式完成。
  • 然而,这听起来内存效率很低,因为在执行上述O(1) 操作之前,您必须将一个可能很大的文件读入内存。
【解决方案2】:

不是直接的方式,但可能比简单的 Python 实现快得多:

line = subprocess.check_output(['tail', '-1', filename])

【讨论】:

  • 你会想在末尾添加一个 [0:-1],不知何故它在末尾添加一个 '\n'...
  • 这不是一个非常python的解决方案
  • 我非常喜欢这个,但在共享代码中使用它并发现人们在 Windows 中使用它时的缺陷 - 没有尾部功能。所以我的偏好(python 3.7,无格式)是 ... with open(filename, 'r') as f: line = f.readlines()[-1]
  • @John9631,您的解决方案非常慢,因为 readlines() 正在读取 RAM 中的所有行,如果文件大小以 GB 为单位,则会出现 MEMORY 错误!
  • windows 支持tail 吗?
【解决方案3】:

使用带有负偏移量的文件的seek 方法和whence=os.SEEK_END 从文件末尾读取块。在该块中搜索最后一行结束字符并抓取它之后的所有字符。如果没有线端,则向后备份并重复该过程。

def last_line(in_file, block_size=1024, ignore_ending_newline=False):
    suffix = ""
    in_file.seek(0, os.SEEK_END)
    in_file_length = in_file.tell()
    seek_offset = 0

    while(-seek_offset < in_file_length):
        # Read from end.
        seek_offset -= block_size
        if -seek_offset > in_file_length:
            # Limit if we ran out of file (can't seek backward from start).
            block_size -= -seek_offset - in_file_length
            if block_size == 0:
                break
            seek_offset = -in_file_length
        in_file.seek(seek_offset, os.SEEK_END)
        buf = in_file.read(block_size)

        # Search for line end.
        if ignore_ending_newline and seek_offset == -block_size and buf[-1] == '\n':
            buf = buf[:-1]
        pos = buf.rfind('\n')
        if pos != -1:
            # Found line end.
            return buf[pos+1:] + suffix

        suffix = buf + suffix

    # One-line file.
    return suffix

请注意,这不适用于不支持seek 的东西,例如标准输入或套接字。在这些情况下,您会被困在阅读全文中(就像 tail 命令所做的那样)。

【讨论】:

  • 我认为这个答案只能在 Python 2 中正常工作。至少,它在 Python 3 中对我不起作用,因为你不能在 Python 3 中从文本文件的末尾寻找相对(引发 io 异常)。要将其更新到 Python 3:使用二进制文件,然后您必须使用字节数组而不是字符串来表示 buf(确保比较 buf[-1:] == b'\n')。如果您确定它是 utf-8 编码的,您可以使用 suffix.decode('utf-8') 返回一个字符串。
【解决方案4】:

如果你知道一条线的最大长度,你可以这样做

def getLastLine(fname, maxLineLength=80):
    fp=file(fname, "rb")
    fp.seek(-maxLineLength-1, 2) # 2 means "from the end of the file"
    return fp.readlines()[-1]

这适用于我的 Windows 机器。但是我不知道如果您以二进制模式打开文本文件,在其他平台上会发生什么。如果要使用 seek(),则需要二进制模式。

【讨论】:

  • 如果你不知道最大行长?
  • 这和迈克的回答都是“正确的方法”,但是除了简单(单字节,例如 ASCII)文本编码之外的任何问题。 unicode 可以有多字节字符,因此在这种情况下(1)您不知道给定最大字符长度的字节相对偏移量,并且(2)您可能会寻找字符的“中间”。跨度>
  • @andrew,即使您从字符中间开始,UTF-8 中的行尾字节码仍然是唯一的。这是 UTF-8 的优点之一。
  • @andrew:UTF-8 可以在中间同步,因为代码点 >= U+80 表示中的字节都设置了高位。因此,如果高位被清除,它就是一个低位 ASCII 字符。这让我们的解析器编写者很高兴。另一方面,有 Shift-JIS 等格式,将非低位 ASCII 字符编码为两个字节,但只有第一个字节保证设置为高位。幸运的是,他们没有对第二个字节使用控制字符。
  • file() 在 Python 3 中不受支持,请改用 open();
【解决方案5】:

查找到文件末尾减去 100 个字节左右。阅读并搜索换行符。如果这里没有换行符,则再寻回 100 个字节左右。起泡,冲洗,重复。最终你会找到一个换行符。最后一行在该换行符之后立即开始。

最好的情况是您只读取 100 个字节。

【讨论】:

    【解决方案6】:

    如果您可以选择一个合理的最大行长度,您可以在开始阅读之前搜索到文件的几乎末尾。

    myfile.seek(-max_line_length, os.SEEK_END)
    line = myfile.readlines()[-1]
    

    【讨论】:

    • 我认为你必须在寻找时再走一个字节,因为 readlines() 包含行终止符。
    【解决方案7】:

    能否将文件加载到mmap,然后使用 mmap.rfind(string[, start[, end]]) 查找文件中倒数第二个 EOL 字符?在文件中查找该点应该会指向我认为的最后一行。

    【讨论】:

      【解决方案8】:

      这里的效率低下并不是因为 Python,而是因为文件读取方式的性质。找到最后一行的唯一方法是读入文件并找到行尾。但是,查找操作可用于跳转到文件中的任何字节偏移。因此,您可以从非常接近文件末尾的地方开始,并根据需要抓取越来越大的块,直到找到最后一行结尾:

      from os import SEEK_END
      
      def get_last_line(file):
        CHUNK_SIZE = 1024 # Would be good to make this the chunk size of the filesystem
      
        last_line = ""
      
        while True:
          # We grab chunks from the end of the file towards the beginning until we 
          # get a new line
          file.seek(-len(last_line) - CHUNK_SIZE, SEEK_END)
          chunk = file.read(CHUNK_SIZE)
      
          if not chunk:
            # The whole file is one big line
            return last_line
      
          if not last_line and chunk.endswith('\n'):
            # Ignore the trailing newline at the end of the file (but include it 
            # in the output).
            last_line = '\n'
            chunk = chunk[:-1]
      
          nl_pos = chunk.rfind('\n')
          # What's being searched for will have to be modified if you are searching
          # files with non-unix line endings.
      
          last_line = chunk[nl_pos + 1:] + last_line
      
          if nl_pos == -1:
            # The whole chunk is part of the last line.
            continue
      
          return last_line
      

      【讨论】:

      • 如果n 大于文件大小,file.seek(-n, os.SEEK_END) 将引发IOError: [Errno 22] Invalid argument
      【解决方案9】:

      这里有一个稍微不同的解决方案。我没有多行,而是只关注最后一行,而不是恒定的块大小,我有一个动态(加倍)的块大小。有关详细信息,请参阅 cmets。

      # Get last line of a text file using seek method.  Works with non-constant block size.  
      # IDK if that speed things up, but it's good enough for us, 
      # especially with constant line lengths in the file (provided by len_guess), 
      # in which case the block size doubling is not performed much if at all.  Currently,
      # we're using this on a textfile format with constant line lengths.
      # Requires that the file is opened up in binary mode.  No nonzero end-rel seeks in text mode.
      REL_FILE_END = 2
      def lastTextFileLine(file, len_guess=1):
          file.seek(-1, REL_FILE_END)      # 1 => go back to position 0;  -1 => 1 char back from end of file
          text = file.read(1)
          tot_sz = 1              # store total size so we know where to seek to next rel file end
          if text != b'\n':        # if newline is the last character, we want the text right before it
              file.seek(0, REL_FILE_END)    # else, consider the text all the way at the end (after last newline)
              tot_sz = 0
          blocks = []           # For storing succesive search blocks, so that we don't end up searching in the already searched
          j = file.tell()          # j = end pos
          not_done = True
          block_sz = len_guess
          while not_done:
              if j < block_sz:   # in case our block doubling takes us past the start of the file (here j also = length of file remainder)
                  block_sz = j
                  not_done = False
              tot_sz += block_sz
              file.seek(-tot_sz, REL_FILE_END)         # Yes, seek() works with negative numbers for seeking backward from file end
              text = file.read(block_sz)
              i = text.rfind(b'\n')
              if i != -1:
                  text = text[i+1:].join(reversed(blocks))
                  return str(text)
              else:
                  blocks.append(text)
                  block_sz <<= 1    # double block size (converge with open ended binary search-like strategy)
                  j = j - block_sz      # if this doesn't work, try using tmp j1 = file.tell() above
          return str(b''.join(reversed(blocks)))      # if newline was never found, return everything read
      

      理想情况下,您可以将其包装在 LastTextFileLine 类中,并跟踪行长的移动平均值。这可能会给你一个很好的 len_guess。

      【讨论】:

        【解决方案10】:
        #!/usr/bin/python
        
        count = 0
        
        f = open('last_line1','r')
        
        for line in f.readlines():
        
            line = line.strip()
        
            count = count + 1
        
            print line
        
        print count
        
        f.close()
        
        count1 = 0
        
        h = open('last_line1','r')
        
        for line in h.readlines():
        
            line = line.strip()
        
            count1 = count1 + 1
        
            if count1 == count:
        
               print line         #-------------------- this is the last line
        
        h.close()
        

        【讨论】:

          【解决方案11】:
          with open('output.txt', 'r') as f:
              lines = f.read().splitlines()
              last_line = lines[-1]
              print last_line
          

          【讨论】:

          • 最佳解决方案和快速解决方案
          • 在处理 GB 文本文件时效果不佳,您只需要最后一行检查。
          • 我认为这在处理非常大的文本文件时效率不高。
          • IndexError: list index out of range is any way to store more data
          • 在访问索引-1之前,您应该检查if lines:以防文件为空
          猜你喜欢
          • 1970-01-01
          • 2012-05-11
          • 1970-01-01
          • 2020-05-31
          • 2021-03-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多