Publicar una respuesta a instancias de los comentaristas en mi respuesta a una pregunta similar en la que se utilizó la misma técnica para mutar la última línea de un archivo, no solo para obtenerla.
Para un archivo de tamaño significativo, mmap
es la mejor manera de hacerlo. Para mejorar la mmap
respuesta existente , esta versión es portátil entre Windows y Linux, y debería ejecutarse más rápido (aunque no funcionará sin algunas modificaciones en Python de 32 bits con archivos en el rango GB, consulte la otra respuesta para obtener sugerencias sobre cómo manejar esto , y para modificar para trabajar en Python 2 ).
import io # Gets consistent version of open for both Py2.7 and Py3.x
import itertools
import mmap
def skip_back_lines(mm, numlines, startidx):
'''Factored out to simplify handling of n and offset'''
for _ in itertools.repeat(None, numlines):
startidx = mm.rfind(b'\n', 0, startidx)
if startidx < 0:
break
return startidx
def tail(f, n, offset=0):
# Reopen file in binary mode
with io.open(f.name, 'rb') as binf, mmap.mmap(binf.fileno(), 0, access=mmap.ACCESS_READ) as mm:
# len(mm) - 1 handles files ending w/newline by getting the prior line
startofline = skip_back_lines(mm, offset, len(mm) - 1)
if startofline < 0:
return [] # Offset lines consumed whole file, nothing to return
# If using a generator function (yield-ing, see below),
# this should be a plain return, no empty list
endoflines = startofline + 1 # Slice end to omit offset lines
# Find start of lines to capture (add 1 to move from newline to beginning of following line)
startofline = skip_back_lines(mm, n, startofline) + 1
# Passing True to splitlines makes it return the list of lines without
# removing the trailing newline (if any), so list mimics f.readlines()
return mm[startofline:endoflines].splitlines(True)
# If Windows style \r\n newlines need to be normalized to \n, and input
# is ASCII compatible, can normalize newlines with:
# return mm[startofline:endoflines].replace(os.linesep.encode('ascii'), b'\n').splitlines(True)
Esto supone que el número de líneas con cola es lo suficientemente pequeño como para que pueda leerlas todas en la memoria de una vez; También puede hacer de esto una función de generador y leer manualmente una línea a la vez reemplazando la línea final con:
mm.seek(startofline)
# Call mm.readline n times, or until EOF, whichever comes first
# Python 3.2 and earlier:
for line in itertools.islice(iter(mm.readline, b''), n):
yield line
# 3.3+:
yield from itertools.islice(iter(mm.readline, b''), n)
Por último, esta lectura en modo binario (necesaria para usar mmap
) por lo que da str
líneas (Py2) y bytes
líneas (Py3); si desea unicode
(Py2) o str
(Py3), el enfoque iterativo podría modificarse para decodificarlo y / o corregir nuevas líneas:
lines = itertools.islice(iter(mm.readline, b''), n)
if f.encoding: # Decode if the passed file was opened with a specific encoding
lines = (line.decode(f.encoding) for line in lines)
if 'b' not in f.mode: # Fix line breaks if passed file opened in text mode
lines = (line.replace(os.linesep, '\n') for line in lines)
# Python 3.2 and earlier:
for line in lines:
yield line
# 3.3+:
yield from lines
Nota: Escribí todo esto en una máquina donde no tengo acceso a Python para probar. Avísame si escribí algo; esto fue lo suficientemente similar a mi otra respuesta que yo creo que debería funcionar, pero los ajustes (por ejemplo, manejo de un offset
) podrían dar lugar a errores sutiles. Por favor, avíseme en los comentarios si hay algún error.
seek(0,2)
entoncestell()
), y uso ese valor para buscar en relación con el comienzo.