Obtener las últimas n líneas de un archivo, similar a la cola


181

Estoy escribiendo un visor de archivos de registro para una aplicación web y para eso quiero paginar a través de las líneas del archivo de registro. Los elementos del archivo están basados ​​en líneas con el elemento más nuevo en la parte inferior.

Por lo tanto, necesito un tail()método que pueda leer nlíneas desde la parte inferior y que admita un desplazamiento. Lo que se me ocurrió se ve así:

def tail(f, n, offset=0):
    """Reads a n lines from f with an offset of offset lines."""
    avg_line_length = 74
    to_read = n + offset
    while 1:
        try:
            f.seek(-(avg_line_length * to_read), 2)
        except IOError:
            # woops.  apparently file is smaller than what we want
            # to step back, go to the beginning instead
            f.seek(0)
        pos = f.tell()
        lines = f.read().splitlines()
        if len(lines) >= to_read or pos == 0:
            return lines[-to_read:offset and -offset or None]
        avg_line_length *= 1.3

¿Es este un enfoque razonable? ¿Cuál es la forma recomendada de seguir archivos de registro con compensaciones?


En mi sistema (Linux SLES 10), la búsqueda relativa al final genera un IOError "no puede hacer búsquedas relativas al final distintas de cero". Me gusta esta solución, pero la modifiqué para obtener la longitud del archivo ( seek(0,2)entonces tell()), y uso ese valor para buscar en relación con el comienzo.
Anne

2
Felicidades - esta pregunta llegó al código fuente de Kippo
Miles

Los parámetros del opencomando que se utiliza para generar el fobjeto de archivo deben especificarse, ya que dependiendo si f=open(..., 'rb')o f=open(..., 'rt')el fdebe ser procesado de manera diferente
Igor Fobia

Respuestas:


123

Esto puede ser más rápido que el tuyo. No hace suposiciones sobre la longitud de la línea. Retrocede el archivo un bloque a la vez hasta que se encuentra el número correcto de caracteres '\ n'.

def tail( f, lines=20 ):
    total_lines_wanted = lines

    BLOCK_SIZE = 1024
    f.seek(0, 2)
    block_end_byte = f.tell()
    lines_to_go = total_lines_wanted
    block_number = -1
    blocks = [] # blocks of size BLOCK_SIZE, in reverse order starting
                # from the end of the file
    while lines_to_go > 0 and block_end_byte > 0:
        if (block_end_byte - BLOCK_SIZE > 0):
            # read the last block we haven't yet read
            f.seek(block_number*BLOCK_SIZE, 2)
            blocks.append(f.read(BLOCK_SIZE))
        else:
            # file too small, start from begining
            f.seek(0,0)
            # only read what was not read
            blocks.append(f.read(block_end_byte))
        lines_found = blocks[-1].count('\n')
        lines_to_go -= lines_found
        block_end_byte -= BLOCK_SIZE
        block_number -= 1
    all_read_text = ''.join(reversed(blocks))
    return '\n'.join(all_read_text.splitlines()[-total_lines_wanted:])

No me gustan los supuestos engañosos sobre la longitud de la línea cuando, como cuestión práctica, nunca se pueden saber cosas así.

Generalmente, esto ubicará las últimas 20 líneas en la primera o segunda pasada a través del bucle. Si su cosa de 74 caracteres es realmente precisa, crea el tamaño de bloque 2048 y seguirá 20 líneas casi de inmediato.

Además, no quemo muchas calorías cerebrales tratando de alinear con los bloques físicos del sistema operativo. Al usar estos paquetes de E / S de alto nivel, dudo que vea alguna consecuencia de rendimiento al tratar de alinearse en los límites de bloqueo del sistema operativo. Si usa E / S de nivel inferior, es posible que vea una aceleración.


ACTUALIZAR

para Python 3.2 y versiones posteriores, siga el proceso en bytes, ya que en los archivos de texto (los que se abren sin una "b" en la cadena de modo), solo se permiten búsquedas relativas al comienzo del archivo (la excepción es buscar hasta el final del archivo) con búsqueda (0, 2)) .:

p.ej: f = open('C:/.../../apache_logs.txt', 'rb')

 def tail(f, lines=20):
    total_lines_wanted = lines

    BLOCK_SIZE = 1024
    f.seek(0, 2)
    block_end_byte = f.tell()
    lines_to_go = total_lines_wanted
    block_number = -1
    blocks = []
    while lines_to_go > 0 and block_end_byte > 0:
        if (block_end_byte - BLOCK_SIZE > 0):
            f.seek(block_number*BLOCK_SIZE, 2)
            blocks.append(f.read(BLOCK_SIZE))
        else:
            f.seek(0,0)
            blocks.append(f.read(block_end_byte))
        lines_found = blocks[-1].count(b'\n')
        lines_to_go -= lines_found
        block_end_byte -= BLOCK_SIZE
        block_number -= 1
    all_read_text = b''.join(reversed(blocks))
    return b'\n'.join(all_read_text.splitlines()[-total_lines_wanted:])

13
Esto falla en pequeños archivos de registro - IOError: argumento no válido - f.seek (bloque * 1024, 2)
ohnoes

1
Muy buen enfoque de hecho. Utilicé una versión ligeramente modificada del código anterior y se me ocurrió esta receta: code.activestate.com/recipes/577968-log-watcher-tail-f-log
Giampaolo Rodolà

66
Ya no funciona en python 3.2. Estoy obteniendo io.UnsupportedOperation: can't do nonzero end-relative seeksque puedo cambiar el desplazamiento a 0, pero eso anula el propósito de la función.
Falacia lógica

44
@DavidEnglund Reason está aquí . En resumen: buscar en relación con el final del archivo no está permitido en modo texto, presumiblemente porque el contenido del archivo tiene que ser descodificado y, en general, buscar una posición arbitraria dentro de una secuencia de bytes codificados puede tener resultados indefinidos cuando Intente decodificar a Unicode a partir de esa posición. La sugerencia que se ofrece en el enlace es intentar abrir el archivo en modo binario y decodificar usted mismo, detectando las excepciones DecodeError.
max

66
NO USE ESTE CÓDIGO. Daña las líneas en algunos casos de borde en Python 2.7. La respuesta de @papercrane a continuación lo arregla.
xApple

88

Asume un sistema similar a Unix en Python 2 que puede hacer:

import os
def tail(f, n, offset=0):
  stdin,stdout = os.popen2("tail -n "+n+offset+" "+f)
  stdin.close()
  lines = stdout.readlines(); stdout.close()
  return lines[:,-offset]

Para python 3 puedes hacer:

import subprocess
def tail(f, n, offset=0):
    proc = subprocess.Popen(['tail', '-n', n + offset, f], stdout=subprocess.PIPE)
    lines = proc.stdout.readlines()
    return lines[:, -offset]

55
Debe ser independiente de la plataforma. Además, si lees la pregunta, verás que f es un archivo como objeto.
Armin Ronacher

40
La pregunta no dice que la dependencia de la plataforma es inaceptable. No entiendo por qué esto merece dos votos negativos cuando proporciona una manera muy unixy (puede ser lo que estás buscando ... ciertamente fue para mí) de hacer exactamente lo que la pregunta hace.
Shabbyrobe

3
Gracias, estaba pensando que tenía que resolver esto en Python puro, pero no hay razón para no usar las utilidades de UNIX cuando están a la mano, así que decidí hacerlo. FWIW en Python moderno, subprocess.check_output es probablemente preferible a os.popen2; simplifica un poco las cosas, ya que solo devuelve la salida como una cadena y aumenta en un código de salida distinto de cero.
mrooney

3
Aunque esto depende de la plataforma, es una forma muy eficiente de hacer lo que se le ha pedido, además de ser una forma extremadamente rápida de hacerlo (no es necesario cargar todo el archivo en la memoria). @Shabbyrobe
earthmeLon

66
Es posible que desee offset_total = str(n+offset)stdin,stdout = os.popen2("tail -n "+offset_total+" "+f)TypeErrors (cannot concatenate int+str)
calcular previamente

32

Aquí está mi respuesta. Pitón pura Usando el tiempo, parece bastante rápido. Seguir 100 líneas de un archivo de registro que tiene 100,000 líneas:

>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=10)
0.0014600753784179688
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=100)
0.00899195671081543
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=1000)
0.05842900276184082
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=10000)
0.5394978523254395
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=100000)
5.377126932144165

Aquí está el código:

import os


def tail(f, lines=1, _buffer=4098):
    """Tail a file and get X lines from the end"""
    # place holder for the lines found
    lines_found = []

    # block counter will be multiplied by buffer
    # to get the block size from the end
    block_counter = -1

    # loop until we find X lines
    while len(lines_found) < lines:
        try:
            f.seek(block_counter * _buffer, os.SEEK_END)
        except IOError:  # either file is too small, or too many lines requested
            f.seek(0)
            lines_found = f.readlines()
            break

        lines_found = f.readlines()

        # we found enough lines, get out
        # Removed this line because it was redundant the while will catch
        # it, I left it for history
        # if len(lines_found) > lines:
        #    break

        # decrement the block counter to get the
        # next X bytes
        block_counter -= 1

    return lines_found[-lines:]

3
Solución elegante! ¿Es if len(lines_found) > lines:realmente necesario? ¿La loopcondición no lo atraparía también?
Maximilian Peters

Una pregunta para mi comprensión: ¿se os.SEEK_ENDusa simplemente por claridad? Por lo que he encontrado, su valor es constante (= 2). Me preguntaba si dejarlo fuera para poder dejarlo fuera import os. ¡Gracias por la gran solución!
n1k31t4

2
@MaximilianPeters sí. No es necesario. Lo comenté.
glenbot

@DexterMorgan puede reemplazar os.SEEK_ENDcon su equivalente entero. Estaba principalmente allí para facilitar la lectura.
glenbot

1
Voté, pero tengo una pequeña liendre. Después de la búsqueda, la lectura de la primera línea puede estar incompleta, por lo que para obtener N _completo_líneas cambié while len(lines_found) < linesa while len(lines_found) <= linesen mi copia. ¡Gracias!
Graham Klyne

30

Si leer el archivo completo es aceptable, use una deque.

from collections import deque
deque(f, maxlen=n)

Antes de 2.6, los deques no tenían una opción maxlen, pero es bastante fácil de implementar.

import itertools
def maxque(items, size):
    items = iter(items)
    q = deque(itertools.islice(items, size))
    for item in items:
        del q[0]
        q.append(item)
    return q

Si es un requisito leer el archivo desde el final, use una búsqueda de galope (también conocido como exponencial).

def tail(f, n):
    assert n >= 0
    pos, lines = n+1, []
    while len(lines) <= n:
        try:
            f.seek(-pos, 2)
        except IOError:
            f.seek(0)
            break
        finally:
            lines = list(f)
        pos *= 2
    return lines[-n:]

¿Por qué funciona esa función inferior? pos *= 2Parece completamente arbitrario. ¿Cuál es su significado?
2mac

1
@ 2mac Búsqueda exponencial . Se lee desde el final del archivo de forma iterativa, duplicando la cantidad leída cada vez, hasta que se encuentran suficientes líneas.
A. Coady

Creo que la solución para leer desde el final no admitirá archivos codificados con UTF-8, ya que la longitud de los caracteres es variable y podría (probablemente lo hará) aterrizar en algún desplazamiento impar que no se puede interpretar correctamente.
Mike

desafortunadamente su solución de búsqueda al galope no funciona para Python 3. Como f.seek () no tiene un desplazamiento negativo. He actualizado su código para que funcione para el enlace
itsjwala

25

La respuesta anterior de S.Lott casi funciona para mí, pero termina dándome líneas parciales. Resulta que corrompe los datos en los límites de los bloques porque los datos retienen los bloques de lectura en orden inverso. Cuando se llama '' .join (datos), los bloques están en el orden incorrecto. Esto arregla eso.

def tail(f, window=20):
    """
    Returns the last `window` lines of file `f` as a list.
    f - a byte file-like object
    """
    if window == 0:
        return []
    BUFSIZ = 1024
    f.seek(0, 2)
    bytes = f.tell()
    size = window + 1
    block = -1
    data = []
    while size > 0 and bytes > 0:
        if bytes - BUFSIZ > 0:
            # Seek back one whole BUFSIZ
            f.seek(block * BUFSIZ, 2)
            # read BUFFER
            data.insert(0, f.read(BUFSIZ))
        else:
            # file too small, start from begining
            f.seek(0,0)
            # only read what was not read
            data.insert(0, f.read(bytes))
        linesFound = data[0].count('\n')
        size -= linesFound
        bytes -= BUFSIZ
        block -= 1
    return ''.join(data).splitlines()[-window:]

1
Insertar al principio de la lista es una mala idea. ¿Por qué no usar la estructura deque?
Sergey11g

1
Lamentablemente no es compatible con Python 3 ... tratando de averiguar por qué.
Sherlock70

20

El código que terminé usando. Creo que este es el mejor hasta ahora:

def tail(f, n, offset=None):
    """Reads a n lines from f with an offset of offset lines.  The return
    value is a tuple in the form ``(lines, has_more)`` where `has_more` is
    an indicator that is `True` if there are more lines in the file.
    """
    avg_line_length = 74
    to_read = n + (offset or 0)

    while 1:
        try:
            f.seek(-(avg_line_length * to_read), 2)
        except IOError:
            # woops.  apparently file is smaller than what we want
            # to step back, go to the beginning instead
            f.seek(0)
        pos = f.tell()
        lines = f.read().splitlines()
        if len(lines) >= to_read or pos == 0:
            return lines[-to_read:offset and -offset or None], \
                   len(lines) > to_read or pos > 0
        avg_line_length *= 1.3

55
no responde exactamente la pregunta
sheki

13

Solución simple y rápida con mmap:

import mmap
import os

def tail(filename, n):
    """Returns last n lines from the filename. No exception handling"""
    size = os.path.getsize(filename)
    with open(filename, "rb") as f:
        # for Windows the mmap parameters are different
        fm = mmap.mmap(f.fileno(), 0, mmap.MAP_SHARED, mmap.PROT_READ)
        try:
            for i in xrange(size - 1, -1, -1):
                if fm[i] == '\n':
                    n -= 1
                    if n == -1:
                        break
            return fm[i + 1 if i else 0:].splitlines()
        finally:
            fm.close()

1
Esta es probablemente la respuesta más rápida cuando la entrada podría ser enorme (o lo sería, si usara el .rfindmétodo para escanear hacia atrás en busca de nuevas líneas, en lugar de realizar comprobaciones de byte a nivel a nivel de Python; en CPython, reemplazando el código de nivel de Python con Las llamadas incorporadas en C generalmente ganan mucho). Para entradas más pequeñas, el dequecon a maxlenes más simple y probablemente igualmente rápido.
ShadowRanger

4

Una versión compatible con Python3 aún más limpia que no se inserta, sino que agrega e invierte:

def tail(f, window=1):
    """
    Returns the last `window` lines of file `f` as a list of bytes.
    """
    if window == 0:
        return b''
    BUFSIZE = 1024
    f.seek(0, 2)
    end = f.tell()
    nlines = window + 1
    data = []
    while nlines > 0 and end > 0:
        i = max(0, end - BUFSIZE)
        nread = min(end, BUFSIZE)

        f.seek(i)
        chunk = f.read(nread)
        data.append(chunk)
        nlines -= chunk.count(b'\n')
        end -= nread
    return b'\n'.join(b''.join(reversed(data)).splitlines()[-window:])

úsalo así:

with open(path, 'rb') as f:
    last_lines = tail(f, 3).decode('utf-8')

No está mal, pero en general aconsejaría que no agregue una respuesta a una pregunta de 10 años con muchas respuestas. Pero ayúdame: ¿qué es específico de Python 3 en tu código?
usr2564301

Las otras respuestas no estaban funcionando exactamente bien :-) py3: ver stackoverflow.com/questions/136168/…
Hauke ​​Rehfeld

3

Actualice la solución @papercrane a python3. Abra el archivo con open(filename, 'rb')y:

def tail(f, window=20):
    """Returns the last `window` lines of file `f` as a list.
    """
    if window == 0:
        return []

    BUFSIZ = 1024
    f.seek(0, 2)
    remaining_bytes = f.tell()
    size = window + 1
    block = -1
    data = []

    while size > 0 and remaining_bytes > 0:
        if remaining_bytes - BUFSIZ > 0:
            # Seek back one whole BUFSIZ
            f.seek(block * BUFSIZ, 2)
            # read BUFFER
            bunch = f.read(BUFSIZ)
        else:
            # file too small, start from beginning
            f.seek(0, 0)
            # only read what was not read
            bunch = f.read(remaining_bytes)

        bunch = bunch.decode('utf-8')
        data.insert(0, bunch)
        size -= bunch.count('\n')
        remaining_bytes -= BUFSIZ
        block -= 1

    return ''.join(data).splitlines()[-window:]

3

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, mmapes la mejor manera de hacerlo. Para mejorar la mmaprespuesta 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 strlíneas (Py2) y byteslí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.


3

Encontré el Popen anterior para ser la mejor solución. Es rápido y sucio y funciona Para Python 2.6 en la máquina Unix, utilicé lo siguiente

def GetLastNLines(self, n, fileName):
    """
    Name:           Get LastNLines
    Description:        Gets last n lines using Unix tail
    Output:         returns last n lines of a file
    Keyword argument:
    n -- number of last lines to return
    filename -- Name of the file you need to tail into
    """
    p = subprocess.Popen(['tail','-n',str(n),self.__fileName], stdout=subprocess.PIPE)
    soutput, sinput = p.communicate()
    return soutput

soutput tendrá contendrá las últimas n líneas del código. para iterar a través de la línea sur por línea:

for line in GetLastNLines(50,'myfile.log').split('\n'):
    print line

2

basado en la respuesta más votada de S.Lott (25 de septiembre de 08 a las 21:43), pero corregido para archivos pequeños.

def tail(the_file, lines_2find=20):  
    the_file.seek(0, 2)                         #go to end of file
    bytes_in_file = the_file.tell()             
    lines_found, total_bytes_scanned = 0, 0
    while lines_2find+1 > lines_found and bytes_in_file > total_bytes_scanned: 
        byte_block = min(1024, bytes_in_file-total_bytes_scanned)
        the_file.seek(-(byte_block+total_bytes_scanned), 2)
        total_bytes_scanned += byte_block
        lines_found += the_file.read(1024).count('\n')
    the_file.seek(-total_bytes_scanned, 2)
    line_list = list(the_file.readlines())
    return line_list[-lines_2find:]

    #we read at least 21 line breaks from the bottom, block by block for speed
    #21 to ensure we don't get a half line

Espero que esto sea útil.


2

Hay algunas implementaciones existentes de tail en pypi que puedes instalar usando pip:

  • mtFileUtil
  • multitail
  • log4tailer
  • ...

Dependiendo de su situación, puede haber ventajas al usar una de estas herramientas existentes.


¿Conoces algún módulo que funcione en Windows? Lo intenté tailhead, tailerpero no funcionaron. También lo intenté mtFileUtil. Inicialmente arrojó un error porque las printdeclaraciones no tenían paréntesis (estoy en Python 3.6). Agregué esos reverse.pyy los mensajes de error desaparecieron, pero cuando mi script llama al módulo ( mtFileUtil.tail(open(logfile_path), 5)), no imprime nada.
Technext

2

Sencillo :

with open("test.txt") as f:
data = f.readlines()
tail = data[-2:]
print(''.join(tail)

Esta es una implementación totalmente mala. Considere manejar archivos enormes, y donde n también es una operación enorme y demasiado costosa
Nivesh Krishna

1

Para una mayor eficiencia con archivos muy grandes (comunes en situaciones de archivos de registro en los que es posible que desee utilizar la cola), generalmente desea evitar leer el archivo completo (incluso si lo hace sin leer todo el archivo en la memoria de una vez) Sin embargo, sí lo hace necesita de alguna manera resolver el desplazamiento en líneas en lugar de caracteres. Una posibilidad es leer hacia atrás con seek () char by char, pero esto es muy lento. En cambio, es mejor procesar en bloques más grandes.

Tengo una función de utilidad que escribí hace un tiempo para leer archivos al revés que se pueden usar aquí.

import os, itertools

def rblocks(f, blocksize=4096):
    """Read file as series of blocks from end of file to start.

    The data itself is in normal order, only the order of the blocks is reversed.
    ie. "hello world" -> ["ld","wor", "lo ", "hel"]
    Note that the file must be opened in binary mode.
    """
    if 'b' not in f.mode.lower():
        raise Exception("File must be opened using binary mode.")
    size = os.stat(f.name).st_size
    fullblocks, lastblock = divmod(size, blocksize)

    # The first(end of file) block will be short, since this leaves 
    # the rest aligned on a blocksize boundary.  This may be more 
    # efficient than having the last (first in file) block be short
    f.seek(-lastblock,2)
    yield f.read(lastblock)

    for i in range(fullblocks-1,-1, -1):
        f.seek(i * blocksize)
        yield f.read(blocksize)

def tail(f, nlines):
    buf = ''
    result = []
    for block in rblocks(f):
        buf = block + buf
        lines = buf.splitlines()

        # Return all lines except the first (since may be partial)
        if lines:
            result.extend(lines[1:]) # First line may not be complete
            if(len(result) >= nlines):
                return result[-nlines:]

            buf = lines[0]

    return ([buf]+result)[-nlines:]


f=open('file_to_tail.txt','rb')
for line in tail(f, 20):
    print line

[Editar] Se agregó una versión más específica (evita la necesidad de revertir dos veces)


Una prueba rápida muestra que esto funciona mucho peor que mi versión anterior. Probablemente debido a su almacenamiento en búfer.
Armin Ronacher

Sospecho que es porque estoy haciendo múltiples búsquedas hacia atrás, por lo que no estoy haciendo un buen uso del búfer de lectura anticipada. Sin embargo, creo que puede ser mejor cuando su conjetura sobre la longitud de la línea no es precisa (por ejemplo, líneas muy grandes), ya que evita tener que volver a leer los datos en este caso.
Brian

1

puede ir al final de su archivo con f.seek (0, 2) y luego leer las líneas una por una con el siguiente reemplazo para readline ():

def readline_backwards(self, f):
    backline = ''
    last = ''
    while not last == '\n':
        backline = last + backline
        if f.tell() <= 0:
            return backline
        f.seek(-1, 1)
        last = f.read(1)
        f.seek(-1, 1)
    backline = last
    last = ''
    while not last == '\n':
        backline = last + backline
        if f.tell() <= 0:
            return backline
        f.seek(-1, 1)
        last = f.read(1)
        f.seek(-1, 1)
    f.seek(1, 1)
    return backline

1

Basado en la respuesta de Eyecue (10 de junio de 2010 a las 21:28): esta clase agrega el método head () y tail () para archivar el objeto.

class File(file):
    def head(self, lines_2find=1):
        self.seek(0)                            #Rewind file
        return [self.next() for x in xrange(lines_2find)]

    def tail(self, lines_2find=1):  
        self.seek(0, 2)                         #go to end of file
        bytes_in_file = self.tell()             
        lines_found, total_bytes_scanned = 0, 0
        while (lines_2find+1 > lines_found and
               bytes_in_file > total_bytes_scanned): 
            byte_block = min(1024, bytes_in_file-total_bytes_scanned)
            self.seek(-(byte_block+total_bytes_scanned), 2)
            total_bytes_scanned += byte_block
            lines_found += self.read(1024).count('\n')
        self.seek(-total_bytes_scanned, 2)
        line_list = list(self.readlines())
        return line_list[-lines_2find:]

Uso:

f = File('path/to/file', 'r')
f.head(3)
f.tail(3)

1

Varias de estas soluciones tienen problemas si el archivo no termina en \ n o para garantizar que se lea la primera línea completa.

def tail(file, n=1, bs=1024):
    f = open(file)
    f.seek(-1,2)
    l = 1-f.read(1).count('\n') # If file doesn't end in \n, count it anyway.
    B = f.tell()
    while n >= l and B > 0:
            block = min(bs, B)
            B -= block
            f.seek(B, 0)
            l += f.read(block).count('\n')
    f.seek(B, 0)
    l = min(l,n) # discard first (incomplete) line if l > n
    lines = f.readlines()[-l:]
    f.close()
    return lines

1

Aquí hay una implementación bastante simple:

with open('/etc/passwd', 'r') as f:
  try:
    f.seek(0,2)
    s = ''
    while s.count('\n') < 11:
      cur = f.tell()
      f.seek((cur - 10))
      s = f.read(10) + s
      f.seek((cur - 10))
    print s
  except Exception as e:
    f.readlines()

Gran ejemplo! ¿Podría explicar el uso de try antes de f.seek? ¿Por qué no antes de la with open? Además, ¿por exceptqué haces un f.readlines()??

Honestamente, el intento probablemente debería ir primero. No recuerdo haber tenido una razón para no detectar open () que no sea en un sistema Linux estándar saludable, / etc / passwd siempre debe ser legible. intente, entonces con es el orden más común.
GL2014

1

Hay un módulo muy útil que puede hacer esto:

from file_read_backwards import FileReadBackwards

with FileReadBackwards("/tmp/file", encoding="utf-8") as frb:

# getting lines by lines starting from the last line up
for l in frb:
    print(l)

1

Otra solución

si su archivo txt se ve así: ratón serpiente gato lagarto lobo perro

podría revertir este archivo simplemente usando la indexación de matriz en python '' '

contents=[]
def tail(contents,n):
    with open('file.txt') as file:
        for i in file.readlines():
            contents.append(i)

    for i in contents[:n:-1]:
        print(i)

tail(contents,-5)

resultado: perro lobo gato lagarto


1

La forma más simple es usar deque:

from collections import deque

def tail(filename, n=10):
    with open(filename) as f:
        return deque(f, n)

0

Tuve que leer un valor específico de la última línea de un archivo, y me topé con este hilo. En lugar de reinventar la rueda en Python, terminé con un pequeño script de shell, guardado como / usr / local / bin / get_last_netp:

#! /bin/bash
tail -n1 /home/leif/projects/transfer/export.log | awk {'print $14'}

Y en el programa Python:

from subprocess import check_output

last_netp = int(check_output("/usr/local/bin/get_last_netp"))

0

No es el primer ejemplo que usa un deque, sino uno más simple. Este es general: funciona en cualquier objeto iterable, no solo en un archivo.

#!/usr/bin/env python
import sys
import collections
def tail(iterable, N):
    deq = collections.deque()
    for thing in iterable:
        if len(deq) >= N:
            deq.popleft()
        deq.append(thing)
    for thing in deq:
        yield thing
if __name__ == '__main__':
    for line in tail(sys.stdin,10):
        sys.stdout.write(line)

0
This is my version of tailf

import sys, time, os

filename = 'path to file'

try:
    with open(filename) as f:
        size = os.path.getsize(filename)
        if size < 1024:
            s = size
        else:
            s = 999
        f.seek(-s, 2)
        l = f.read()
        print l
        while True:
            line = f.readline()
            if not line:
                time.sleep(1)
                continue
            print line
except IOError:
    pass

0
import time

attemps = 600
wait_sec = 5
fname = "YOUR_PATH"

with open(fname, "r") as f:
    where = f.tell()
    for i in range(attemps):
        line = f.readline()
        if not line:
            time.sleep(wait_sec)
            f.seek(where)
        else:
            print line, # already has newline

0
import itertools
fname = 'log.txt'
offset = 5
n = 10
with open(fname) as f:
    n_last_lines = list(reversed([x for x in itertools.islice(f, None)][-(offset+1):-(offset+n+1):-1]))

0
abc = "2018-06-16 04:45:18.68"
filename = "abc.txt"
with open(filename) as myFile:
    for num, line in enumerate(myFile, 1):
        if abc in line:
            lastline = num
print "last occurance of work at file is in "+str(lastline) 

0

Actualización para la respuesta dada por A.Coady

Funciona con python 3 .

Esto usa la búsqueda exponencial y almacenará solo las Nlíneas desde atrás y es muy eficiente.

import time
import os
import sys

def tail(f, n):
    assert n >= 0
    pos, lines = n+1, []

    # set file pointer to end

    f.seek(0, os.SEEK_END)

    isFileSmall = False

    while len(lines) <= n:
        try:
            f.seek(f.tell() - pos, os.SEEK_SET)
        except ValueError as e:
            # lines greater than file seeking size
            # seek to start
            f.seek(0,os.SEEK_SET)
            isFileSmall = True
        except IOError:
            print("Some problem reading/seeking the file")
            sys.exit(-1)
        finally:
            lines = f.readlines()
            if isFileSmall:
                break

        pos *= 2

    print(lines)

    return lines[-n:]




with open("stream_logs.txt") as f:
    while(True):
        time.sleep(0.5)
        print(tail(f,2))

-1

Pensándolo bien, esto es probablemente tan rápido como cualquier cosa aquí.

def tail( f, window=20 ):
    lines= ['']*window
    count= 0
    for l in f:
        lines[count%window]= l
        count += 1
    print lines[count%window:], lines[:count%window]

Es mucho mas simple. Y parece ir a buen ritmo.


Debido a que casi todo aquí no funciona con archivos de registro con más de 30 MB sin cargar la misma cantidad de memoria en la RAM;) Su primera versión es mucho mejor, pero para los archivos de prueba aquí funciona un poco peor que la mía y no funciona con diferentes personajes de nueva línea.
Armin Ronacher

3
Estaba equivocado. La versión 1 tomó 0.00248908996582 para 10 colas a través del diccionario. La versión 2 tomó 1.2963051796 por 10 colas a través del diccionario. Casi me votaría a mí mismo.
S.Lott

"no funciona con diferentes caracteres de nueva línea". Reemplace datacount ('\ n') con len (data.splitlines ()) si es importante.
S.Lott
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.