Obter as últimas n linhas de um arquivo, semelhante ao tail


181

Estou escrevendo um visualizador de arquivo de log para um aplicativo Web e para isso quero paginar pelas linhas do arquivo de log. Os itens no arquivo são baseados em linhas com o item mais recente na parte inferior.

Então, eu preciso de um tail()método que possa ler nlinhas da parte inferior e suporte um deslocamento. O que eu criei se parece com isso:

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

Essa é uma abordagem razoável? Qual é a maneira recomendada de ajustar os arquivos de log com as compensações?


No meu sistema (Linux SLES 10), a busca em relação ao final gera um IOError "não é possível fazer buscas relativas a zero que não sejam de zero". Eu gosto dessa solução, mas a modifiquei para obter o tamanho do arquivo ( seek(0,2)então tell()) e use esse valor para procurar em relação ao início.
Anne

2
Parabéns - esta pergunta tornou no código-fonte Kippo
Miles

Os parâmetros do opencomando usado para gerar o fobjeto de arquivo deve ser especificado, porque dependendo se f=open(..., 'rb')ou f=open(..., 'rt')o fdevem ser processadas de forma diferente
Igor Fobia

Respostas:


123

Isso pode ser mais rápido que o seu. Não faz suposições sobre o comprimento da linha. Faz o backup do arquivo um bloco de cada vez, até encontrar o número certo 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:])

Não gosto de suposições complicadas sobre o comprimento da linha quando - por uma questão prática - você nunca pode saber coisas assim.

Geralmente, isso localizará as últimas 20 linhas na primeira ou na segunda passagem pelo loop. Se sua coisa de 74 caracteres é realmente precisa, você cria o tamanho do bloco 2048 e segue 20 linhas quase imediatamente.

Além disso, eu não queimo muitas calorias do cérebro tentando refinar o alinhamento com os blocos físicos do sistema operacional. Usando esses pacotes de E / S de alto nível, duvido que você veja qualquer consequência de desempenho ao tentar alinhar nos limites do bloco do SO. Se você usar E / S de nível inferior, poderá ver uma aceleração.


ATUALIZAR

para Python 3.2 e superior, siga o processo em bytes, como nos arquivos de texto (aqueles abertos sem um "b" na string de modo), apenas buscas relativas ao início do arquivo são permitidas (a exceção é procurar até o final do arquivo) com busca (0, 2)) .:

por exemplo: 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
Isso falha em arquivos de log pequenos - IOError: argumento inválido - f.seek (bloco * 1024, 2)
ohnoes

1
Abordagem muito agradável mesmo. Eu usei uma versão ligeiramente modificada do código acima e veio com esta receita: code.activestate.com/recipes/577968-log-watcher-tail-f-log
Giampaolo Rodolà

6
Não funciona mais no python 3.2. Estou conseguindo io.UnsupportedOperation: can't do nonzero end-relative seeksalterar o deslocamento para 0, mas isso anula o objetivo da função.
Falácia lógica

4
@DavidEnglund A razão está aqui . Em resumo: a busca em relação ao final do arquivo não é permitida no modo de texto, presumivelmente porque o conteúdo do arquivo precisa ser decodificado e, em geral, a busca de uma posição arbitrária em uma sequência de bytes codificados pode ter resultados indefinidos quando você tente decodificar para Unicode a partir dessa posição. A sugestão oferecida no link é tentar abrir o arquivo no modo binário e decodificar você mesmo, capturando as exceções DecodeError.
max

6
NÃO USE ESTE CÓDIGO. Ele corrompe linhas em alguns casos de borda no python 2.7. A resposta de @papercrane abaixo a corrige.
XApple

88

Assume um sistema unix no Python 2 que você pode fazer:

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, você pode fazer:

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]

5
Deve ser independente da plataforma. Além disso, se você ler a pergunta, verá que f é um arquivo como um objeto.
Armin Ronacher

40
a questão não diz que a dependência da plataforma é inaceitável. Não vejo por que isso merece duas votações negativas quando fornece uma maneira muito unix (pode ser o que você está procurando ... certamente foi para mim) de fazer exatamente o que a pergunta faz.
Shabbyrobe

3
Obrigado, eu estava pensando que tinha que resolver isso em Python puro, mas não há razão para não usar os utilitários UNIX quando eles estão à mão, então fui com isso. FWIW no Python moderno, subprocess.check_output é provavelmente preferível ao os.popen2; simplifica um pouco as coisas, pois apenas retorna a saída como uma string e gera um código de saída diferente de zero.
Mroney # 30/13

3
Embora isso dependa da plataforma, é uma maneira muito eficiente de fazer o que foi solicitado, além de ser uma maneira extremamente rápida de fazer (você não precisa carregar o arquivo inteiro na memória). @Shabbyrobe
earthmeLon

6
Você pode querer precalculate o deslocamento como: offset_total = str(n+offset)e substituir esta linha stdin,stdout = os.popen2("tail -n "+offset_total+" "+f)para evitarTypeErrors (cannot concatenate int+str)
AddingColor

32

Aqui está a minha resposta. Pitão puro. Usando o timeit, parece bem rápido. Seguindo 100 linhas de um arquivo de log que possui 100.000 linhas:

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

Aqui está o 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
Solução elegante! É if len(lines_found) > lines:realmente necessário? A loopcondição não a pegaria também?
Maximilian Peters

Uma pergunta para minha compreensão: é os.SEEK_ENDusada simplesmente para maior clareza? Tanto quanto eu descobri, seu valor é constante (= 2). Eu estava pensando em deixá-lo de fora para poder deixar de fora o import os. Obrigado pela ótima solução!
N1k31t4 5/05

2
@MaximilianPeters yes. Não é necessário. Eu comentei.
glenbot

@DexterMorgan você pode substituir os.SEEK_ENDpor seu equivalente inteiro. Estava principalmente lá para facilitar a leitura.
glenbot

1
Eu votei, mas tenho um pequeno nit. Após a busca, a primeira linha lida pode estar incompleta; portanto, para obter N _complete_lines, alterei o while len(lines_found) < linespara while len(lines_found) <= linesna minha cópia. Obrigado!
Graham Klyne

30

Se a leitura do arquivo inteiro for aceitável, use um deque.

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

Antes do 2.6, o deques não tinha uma opção maxlen, mas é 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

Se for necessário ler o arquivo a partir do final, use uma pesquisa a galope (também conhecida 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 que essa função inferior funciona? pos *= 2parece completamente arbitrário. Qual é o seu significado?
2mac

1
@ 2mac Pesquisa Exponencial . Ele lê do final do arquivo iterativamente, dobrando a quantidade lida a cada vez, até que sejam encontradas linhas suficientes.
A.Coady

Eu acho que a solução para ler a partir do final não suportará arquivos codificados com UTF-8, já que o tamanho dos caracteres é variável e você pode (provavelmente irá) pousar em algum deslocamento estranho que não pode ser interpretado corretamente.
1755 Mike

infelizmente, sua solução de pesquisa galopante não funciona no python 3. Como o f.seek () não é compensado pelo negativo. Eu atualizei o código de fazer o trabalho para python 3 ligação
itsjwala

25

A resposta de S.Lott acima quase funciona para mim, mas acaba me dando linhas parciais. Acontece que ele corrompe os dados nos limites do bloco, porque os dados retêm os blocos de leitura na ordem inversa. Quando '' .join (data) é chamado, os blocos estão na ordem errada. Isso corrige isso.

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
Inserir no início da lista é uma má ideia. Por que não usar a estrutura deque?
usar o seguinte código

1
Infelizmente não é compatível com Python 3 ... tentando descobrir o porquê.
Sherlock70

20

O código que acabei usando. Eu acho que este é o melhor até agora:

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

5
não responde exatamente a pergunta.
Sheki

13

Solução simples e rápida com 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
Essa é provavelmente a resposta mais rápida quando a entrada pode ser grande (ou seria, se usasse o .rfindmétodo para procurar novas linhas para trás em vez de executar verificações de byte por vez no nível Python; no CPython, substituindo o código no nível Python por As chamadas internas C geralmente ganham muito). Para entradas menores, o dequewith a maxlené mais simples e provavelmente similarmente rápido.
ShadowRanger

4

Uma versão compatível com python3 ainda mais limpa que não insere, mas acrescenta e reverte:

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

use-o assim:

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

Não é tão ruim - mas em geral eu aconselho a não adicionar uma resposta a uma pergunta de 10 anos com muitas respostas. Mas me ajude: o que é específico do Python 3 no seu código?
precisa saber é o seguinte

As outras respostas não foram exatamente funcionando bem :-) PY3: ver stackoverflow.com/questions/136168/...
Hauke Rehfeld

3

Atualize a solução @papercrane para python3. Abra o arquivo com open(filename, 'rb')e:

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

Postando uma resposta a pedido dos comentaristas sobre a minha resposta a uma pergunta semelhante em que a mesma técnica foi usada para alterar a última linha de um arquivo, não apenas obtê-la.

Para um arquivo de tamanho significativo, mmapé a melhor maneira de fazer isso. Para melhorar a mmapresposta existente , esta versão é portátil entre Windows e Linux e deve ser executada mais rapidamente (embora não funcione sem algumas modificações no Python de 32 bits com arquivos na faixa de GB, consulte a outra resposta para obter dicas sobre como lidar com isso. e para modificar para trabalhar no 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)

Isso pressupõe que o número de linhas atadas seja pequeno o suficiente para que você possa lê-las na memória com segurança de uma só vez; você também pode fazer disso uma função de gerador e ler manualmente uma linha de cada vez, substituindo a linha final por:

        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 fim, isso é lido no modo binário (necessário usar mmap) para strfornecer linhas (Py2) e byteslinhas (Py3); se você quiser unicode(Py2) ou str(Py3), a abordagem iterativa pode ser aprimorada para decodificar e / ou corrigir novas linhas:

        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: digitei tudo isso em uma máquina na qual não tenho acesso ao Python para testar. Por favor, deixe-me saber se eu digitei alguma coisa; isso foi semelhante o suficiente à minha outra resposta e acho que deve funcionar, mas os ajustes (por exemplo, lidar com um offset) podem levar a erros sutis. Por favor, deixe-me saber nos comentários se houver algum erro.


3

Eu achei o Popen acima a melhor solução. É rápido e sujo e funciona Para o python 2.6 na máquina Unix, usei o seguinte

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

A saída de saída conterá as últimas n linhas do código. para iterar através da saída de linha por linha, faça:

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

2

com base na resposta mais votada de S.Lott (25 de setembro de 2008 às 21:43), mas corrigida para arquivos pequenos.

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 isso seja útil.


2

Existem algumas implementações existentes do tail on pypi que você pode instalar usando o pip:

  • mtFileUtil
  • multitail
  • log4tailer
  • ...

Dependendo da sua situação, pode haver vantagens em usar uma dessas ferramentas existentes.


Você conhece algum módulo que funcione no Windows? Eu tentei tailhead, tailermas eles não funcionaram. Também tentei mtFileUtil. Ele estava lançando um erro inicialmente porque as printinstruções não estavam entre parênteses (estou no Python 3.6). Eu os adicionei reverse.pye as mensagens de erro desapareceram, mas quando meu script chama o module ( mtFileUtil.tail(open(logfile_path), 5)), ele não imprime nada.
Technext 19/09/18

2

Simples:

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

Isso é totalmente ruim de implementação. Considere lidar com arquivos grandes, e onde n é também enorme, operação muito caro
Nivesh Krishna

1

Para obter eficiência com arquivos muito grandes (comum em situações de arquivo de log em que você pode usar o tail), geralmente você deseja evitar a leitura do arquivo inteiro (mesmo que faça isso sem ler o arquivo inteiro na memória de uma só vez). precisa de alguma forma calcular o deslocamento em linhas, em vez de caracteres. Uma possibilidade é ler de trás para frente com seek () char por char, mas isso é muito lento. Em vez disso, é melhor processar em blocos maiores.

Eu tenho uma função utilitária que escrevi há um tempo atrás para ler arquivos que podem ser usados ​​aqui atrás.

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] Adicionado versão mais específica (evita a necessidade de reverter duas vezes)


Um teste rápido mostra que isso tem um desempenho muito pior do que a minha versão acima. Provavelmente por causa do seu buffer.
Armin Ronacher

Eu suspeito que é porque estou fazendo várias buscas para trás, por isso não estou conseguindo usar o buffer readahead tão bem. No entanto, acho que pode ser melhor quando o seu palpite sobre o comprimento da linha não é preciso (por exemplo, linhas muito grandes), pois evita a necessidade de reler dados neste caso.
Brian Brian

1

você pode ir para o final do seu arquivo com f.seek (0, 2) e depois ler as linhas uma a uma com a seguinte substituição 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

Com base na resposta Eyecue (10 de junho de 2010 às 21:28): esta classe adiciona o método head () e tail () ao arquivo do 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

Várias dessas soluções têm problemas se o arquivo não terminar em \ n ou em garantir a leitura da primeira linha 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

Aqui está uma implementação bastante simples:

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()

Ótimo exemplo! Você poderia explicar o uso de try antes do f.seek? Por que não antes do with open? Além disso, por que exceptvocê faz um f.readlines()??

Honestamente, a tentativa provavelmente deve ocorrer primeiro. Não me lembro de ter um motivo para não pegar o open (), exceto em um sistema Linux padrão saudável, / etc / passwd sempre deve ser legível. tente, então com é a ordem mais comum.
GL2014

1

Há um módulo muito útil que pode fazer isso:

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

Outra solução

se o seu arquivo txt estiver assim: rato cobra gato lagarto lobo cachorro

você pode reverter esse arquivo simplesmente usando a indexação de array em 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: cachorro lagarto lobo gato


1

A maneira mais simples é usar deque:

from collections import deque

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

0

Eu tive que ler um valor específico da última linha de um arquivo e me deparei com esse tópico. Em vez de reinventar a roda no Python, acabei com um pequeno script de shell, salvo como / usr / local / bin / get_last_netp:

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

E no programa Python:

from subprocess import check_output

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

0

Não é o primeiro exemplo usando um deque, mas um exemplo mais simples. Este é geral: funciona em qualquer objeto iterável, não apenas em um arquivo.

#!/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

Atualização para resposta dada por A.Coady

Funciona com python 3 .

Isso usa a Pesquisa Exponencial e armazenará em buffer apenas as Nlinhas de trás e é muito 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

Pensando bem, isso provavelmente é tão rápido quanto qualquer coisa aqui.

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]

É muito mais simples. E parece rasgar a um bom ritmo.


Porque quase tudo aqui não funciona com arquivos de log com mais de 30 MB ou mais sem carregar a mesma quantidade de memória na RAM;) Sua primeira versão é muito melhor, mas para os arquivos de teste aqui, ela é um pouco pior que a minha e não funciona com diferentes caracteres de nova linha.
Armin Ronacher

3
Eu estava errado. A versão 1 levou 0,00248908996582 para 10 caudas no dicionário. A versão 2 levou 1.2963051796 para 10 caudas no dicionário. Eu quase me votaria para baixo.
S.Lott 25/09/08

"não funciona com diferentes caracteres de nova linha." Substitua datacount ('\ n') por len (data.splitlines ()), se for necessário.
S.Lott
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.