Самый эффективный способ поиска последних x строк файла в python

У меня есть файл, и я не знаю, насколько он будет большой (он может быть довольно большим, но размер будет сильно отличаться). Я хочу найти последние 10 строк или около того, чтобы увидеть, соответствует ли какая-либо из них строка. Мне нужно сделать это как можно быстрее и эффективнее, и мне было интересно, есть ли что-то лучше:

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

Ответ 1

# 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

Ответ 2

Здесь ответ, подобный MizardX, но без его очевидной проблемы с квадратичным временем в худшем случае от повторного сканирования рабочей строки для строк новой строки в виде кусков.

По сравнению с решением activestate (которое также представляется квадратичным), это не взрывается с учетом пустого файла, и каждый ищет для чтения блока вместо двух.

По сравнению с нерестом "хвост" это самодостаточно. (Но "хвост" лучше всего, если он у вас есть.)

По сравнению с захватом нескольких 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 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

Изменить: изменена карта() на itertools.imap() в head(). Изменить 2: упростить reverseedblocks(). Изменить 3: избежать повторного сканирования хвоста для строк новой строки. Изменить 4: переписал reverseedlines(), потому что str.splitlines() игнорирует окончательный '\n', как заметил Брайан (спасибо).

Обратите внимание, что в очень старых версиях Python конкатенация строк в цикле здесь займет квадратичное время. CPython, по крайней мере, последние несколько лет, автоматически избегает этой проблемы.

Ответ 3

Если вы используете Python в системе POSIX, вы можете использовать "tail -10" для извлечения последних нескольких строк. Это может быть быстрее, чем писать собственный код Python, чтобы получить последние 10 строк. Вместо того, чтобы открывать файл напрямую, откройте канал из команды "tail -10 filename". Если вы уверены в выходе журнала (например, вы знаете, что существует никогда любые длинные строки длиной до сотни или тысячи символов), то с использованием одного из "read the last 2KB", Подходы, перечисленные, были бы точными.

Ответ 4

Я думаю, что чтение последних 2 КБ или около того файла должно гарантировать, что вы получите 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)

Ответ 5

Вот версия, использующая 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]

Ответ 7

Если вы находитесь в коробке unix, os.popen("tail -10 " + filepath).readlines(), вероятно, будет самым быстрым способом. В противном случае это зависит от того, насколько вы надежны. Предлагаемые до сих пор методы все равно будут падать, так или иначе. Для надежности и скорости в наиболее распространенном случае вы, вероятно, хотите что-то вроде логарифмического поиска: используйте file.seek для перехода к концу файла минус 1000 символов, прочитайте его, проверьте, сколько строк оно содержит, затем EOF минус 3000 символов, читать в 2000 символов, подсчитывать строки, затем EOF минус 7000, читать 4000 символов, подсчитывать строки и т.д., пока у вас не будет столько строк, сколько вам нужно. Но если вы точно знаете, что это всегда будет работать с файлами с разумной длиной строки, вам может и не понадобиться.

Вы также можете найти вдохновение в исходный код для команды unix tail.

Ответ 8

Я столкнулся с этой проблемой, проанализировав последний час LARGE syslog файлов и использовал эту функцию, чтобы активировать сайт рецептов... (http://code.activestate.com/recipes/439045/)

!/usr/bin/env python
# -*-mode: python; coding: iso-8859-1 -*-
#
# Copyright (c) Peter Astrand <[email protected]>

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 читать весь файл в памяти, а затем отрубает последние десять строк.

Ответ 9

Вы можете прочитать куски размером 1000 байт или около того из конца файла в буфер до 10 строк.

Ответ 10

Вы также можете подсчитывать строки при обратном просмотре файла, а не гадать при смещении байта.

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(). Вы также можете кэшировать строки, которые вы читаете в первый раз, чтобы дважды очистить одну и ту же часть файла.

Ответ 11

Я принял предложение 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)

Ответ 12

прочитайте последние несколько Ks файла и разделите его на строки, чтобы вернуть только последние 10.

довольно маловероятно, чтобы начало этого куска падало на границу линии, но вы все равно отбросите первые строки.

Ответ 13

Лично у меня возникнет соблазн выйти в оболочку и вызвать tail -n10 для загрузки файла. Но тогда я не программист на Python;)

Ответ 14

Во-первых, функция, которая возвращает список:

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 строк.

Ответ 15

Это решение будет читать файл только один раз, но с использованием 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"

Ответ 16

Возможно, это может быть полезно:

import os.path

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