2008-11-03 18 views
31

Tengo un archivo y no sé qué tan grande será (podría ser bastante grande, pero el tamaño variará mucho). Quiero buscar las últimas 10 líneas para ver si alguno de ellos coincide con una cadena. Necesito hacer esto lo más rápido y eficientemente como sea posible y se preguntaba si hay algo mejor que:La forma más eficiente de buscar las últimas x líneas de un archivo en python

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

duplicado exacto de http://stackoverflow.com/questions/136168/tail-a-file-with-python. –

Respuesta

32
# 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 
+1

El "if len (l) <10" es redundante. "print l [: - 10]" maneja ese caso. –

+0

@Darius: realmente quise decir que len (l)> 10, fijo – PabloG

+2

líneas [: - 10] cae las últimas 10 líneas. Lo que quieres es líneas [-10:]. –

0

leer los últimos pocos Ks del archivo y de división que en líneas para volver sólo el último 10.

es bastante improbable que el comienzo de ese trozo caiga en un límite de línea, pero descartarás las primeras líneas de todos modos.

1

Puede leer fragmentos de 1.000 bytes aproximadamente desde el final del archivo en un búfer hasta que tenga 10 líneas.

7

Creo que al leer los últimos 2 KB o más del archivo debería asegurarse de obtener 10 líneas, y no debería ser demasiado de un cerdo de recursos.

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) 
+0

debe comprobar que el archivo es> = 2 KB aunque – Jake

+0

Verdadero: comentario agregado sobre la naturaleza rápida y sucia del código –

0

Personalmente estaría tentado de salir a la cáscara y la llamada del extremo -n10 para cargar el archivo. Pero entonces no soy realmente un programador de Python;)

0

En primer lugar, una función que devuelve una lista:

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:] 

En segundo lugar, una función que itera sobre las líneas en orden inverso:

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 

por su ejemplo:

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

Edit: Ahora obtiene el tamaño de archivo automáticamente
Edit2: Ahora solo itera para 10 líneas.

+0

Una nit: no dejas de buscar después de verificar 10 líneas sin éxito. –

+0

Sí, eso es genial. Vea mi respuesta para otra forma de hacerlo, más reutilizable. –

+0

Aunque, mirándolo ahora, me doy cuenta de que mi función head() pasa por las 10 líneas, incluso si la clave se encuentra antes; Debería haber usado itertools.imap() en lugar de map(). –

8

Si está ejecutando Python en un sistema POSIX, puede usar 'tail -10' para recuperar las últimas líneas. Esto puede ser más rápido que escribir su propio código de Python para obtener las últimas 10 líneas. En lugar de abrir el archivo directamente, abra un conducto desde el comando 'cola -10 nombre de archivo'. Sin embargo, si está seguro del resultado del registro (por ejemplo, sabe que hay nunca líneas muy largas que tienen cientos o miles de caracteres), entonces usar uno de los enfoques "leer los últimos 2KB" indicados estaría bien .

+0

Sería cauteloso con esto, porque las llamadas shell tienen mucho más sobrecarga que un acceso directo. – Svante

+1

Esto es bastante viejo, pero en realidad no abogaba por una llamada shell. Recomiendo llamar al script con la salida de la cola, en lugar de llamar al script para leer todo el archivo. –

2

Si está en una caja de Unix, os.popen("tail -10 " + filepath).readlines() será probablemente la forma más rápida. De lo contrario, depende de qué tan robusto quieras que sea. Los métodos propuestos hasta ahora caerán, de una forma u otra. Para mayor robustez y velocidad en el caso más común, probablemente desee algo así como una búsqueda logarítmica: use file.seek para ir al final del archivo menos 1000 caracteres, léalo, verifique cuántas líneas contiene, luego a EOF menos 3000 caracteres , lea en 2000 caracteres, cuente las líneas, luego EOF menos 7000, lea en 4000 caracteres, cuente las líneas, etc. hasta que tenga tantas líneas como necesite. Pero si está seguro de que siempre se ejecutará en archivos con longitudes de línea razonables, es posible que no lo necesite.

También puede encontrar algo de inspiración en el source code para el comando unix tail.

31

Aquí hay una respuesta como la de MizardX, pero sin su aparente problema de tomar el tiempo cuadrático en el peor de los casos, al volver a escanear la cadena de trabajo repetidas veces para nuevas líneas a medida que se agregan trozos.

En comparación con la solución de estado activo (que también parece ser cuadrática), esto no explota dado un archivo vacío, y uno busca por lectura de bloque en lugar de dos.

Comparado con el spawning 'tail', es autocontenido. (Pero 'cola' es mejor si lo tiene.)

En comparación con agarrar unos kB del extremo y con la esperanza de que sea suficiente, esto funciona para cualquier longitud de línea.

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) 

utilizarlo como solicitada:

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 

Editar: mapa cambiada() para itertools.imap() en la cabeza(). Editar 2: simplified reversed_blocks(). Editar 3: evitar volver a explorar la cola para nuevas líneas. Editar 4: reescribió reversed_lines() porque str.splitlines() ignora una '\ n' final, como notó BrianB (gracias).

Tenga en cuenta que en las versiones muy antiguas de Python, la concatenación de cadenas en un bucle aquí tomará un tiempo cuadrático. CPython desde al menos los últimos años evita este problema automáticamente.

+0

Muy bien - Leí la lista de respuestas hasta que llegué aquí, sabiendo que la mejor sería la que fuera lo suficientemente inteligente como para usar la directiva 'yield' –

+0

Se arregló una esquina para usted - a veces un bloque termina en una nueva línea, por lo que la cola es su propia entrada. –

+0

@BrianB, gracias. ¿Pueden dar un caso de prueba donde se rompe mi código? He revertido el cambio porque falló en la primera cosa que probé, '\ nhello \ n \ nworld \ n' (con blocksize establecido en 2). (Mi agradecimiento no es irónico porque espero que hayas notado un caso real en el que mi código falló). –

2

me encontré con ese problema, el análisis de la última hora de los archivos de registro del sistema grande, y utiliza esta función desde el sitio de la receta de ActiveState ... (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) 

Funciona muy bien y es mucho más eficiente que algo como fileObj.readlines() [- 10:], que hace que python lea el archivo completo en la memoria y luego corta las últimas diez líneas de él.

+1

No funciona si la barra está vacía. También tengo problemas para seguir el código. –

5

Aquí hay una versión que usa mmap que parece bastante eficiente. La gran ventaja es que mmap se encargará automáticamente de los requisitos de búsqueda de memoria de archivos para usted.

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] 
+1

¡Agradable! Debería haber pensado en mmap. Sin embargo, esto va un orden de magnitud más lento que el mío en mi prueba de un archivo de 1 línea realmente grande, supongo, porque verifica char por char en código Python. –

+0

Sí, también me preocupaba el ciclo "Python puro". El bucle posiblemente podría hacerse más eficiente que el código que he proporcionado. Si el objeto mmap tiene un método rfind(), ¡podría haber sido mucho mejor! – mhawke

+0

FYI: los objetos mmap de Python v2.6.5 tienen un método 'rfind()'. – RobM

0

Esta solución va a leer el archivo sólo una vez, pero utilizando 2 punteros a objetos de archivo para poder obtener las últimas N líneas de archivo sin tener que volver a leerlo:

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" 
1

También podría contar el líneas a medida que retrocede en el archivo, en lugar de adivinar una compensación de bytes.

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) 

Ahora el archivo está en una buena posición para ejecutar readlines(). También podría almacenar en caché las cadenas que leyó la primera vez, para eliminar la lectura de la misma porción del archivo dos veces.

0

Tal vez esto podría ser útil:

import os.path 

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

podría ser de una sola manera, pero no es portátil. – ghostdog74

1

Tomé la sugerencia de utilizar mhawke mmap y escribió una versión que utiliza 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) 
Cuestiones relacionadas