Ottieni le ultime n righe di un file, simile a tail


181

Sto scrivendo un visualizzatore di file di registro per un'applicazione Web e per questo voglio impaginare le righe del file di registro. Gli elementi nel file sono basati in linea con l'elemento più recente in fondo.

Quindi ho bisogno di un tail()metodo in grado di leggere le nrighe dal basso e supporta un offset. Quello che mi è venuto in mente è simile al seguente:

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

È un approccio ragionevole? Qual è il modo consigliato per personalizzare i file di registro con gli offset?


Sul mio sistema (Linux SLES 10), la ricerca relativa alla fine genera un IOError "impossibile eseguire ricerche relative alla fine diversa da zero". Mi piace questa soluzione, ma l'ho modificata per ottenere la lunghezza del file ( seek(0,2)quindi tell()) e utilizzare quel valore per cercare rispetto all'inizio.
Anne

2
Complimenti - questa domanda è diventata il codice sorgente di Kippo
Miglia,

I parametri del opencomando utilizzato per generare il foccorre precisare oggetto file, perché a seconda se f=open(..., 'rb')o f=open(..., 'rt')il fdevono essere trattati differentemente
Igor Fobia

Risposte:


123

Questo potrebbe essere più veloce del tuo. Non fa ipotesi sulla lunghezza della linea. Esegue il backup del file di un blocco alla volta fino a quando non trova il numero corretto di caratteri "\ 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:])

Non mi piacciono le ipotesi complicate sulla lunghezza della linea quando - in pratica - non puoi mai sapere cose del genere.

Generalmente, questo localizzerà le ultime 20 righe sul primo o sul secondo passaggio attraverso il loop. Se la tua cosa di 74 caratteri è effettivamente accurata, fai la dimensione del blocco 2048 e traccerai quasi immediatamente 20 linee.

Inoltre, non brucio molte calorie del cervello cercando di perfezionare l'allineamento con i blocchi fisici del sistema operativo. Usando questi pacchetti I / O di alto livello, dubito che vedrai qualsiasi conseguenza delle prestazioni del tentativo di allinearlo sui limiti del blocco del sistema operativo. Se si utilizza l'I / O di livello inferiore, è possibile che venga visualizzato un aumento di velocità.


AGGIORNARE

per Python 3.2 e versioni successive, seguire il processo sui byte come Nei file di testo (quelli aperti senza una "b" nella stringa della modalità), sono consentite solo ricerche relative all'inizio del file (l'eccezione è alla fine del file stesso con seek (0, 2)) .:

per esempio: 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
Questo non riesce su piccoli file di registro - IOError: argomento non valido - f.seek (blocco * 1024, 2)
ohnoes

1
Approccio davvero piacevole. Ho usato una versione leggermente modificata del codice sopra e ho trovato questa ricetta: code.activestate.com/recipes/577968-log-watcher-tail-f-log
Giampaolo Rodolà

6
Non funziona più in Python 3.2. Sto ottenendo io.UnsupportedOperation: can't do nonzero end-relative seeksche posso cambiare l'offset a 0, ma ciò vanifica lo scopo della funzione.
Fallimento logico

4
@DavidEnglund Reason è qui . In breve: la ricerca relativa alla fine del file non è consentita in modalità testo, presumibilmente perché il contenuto del file deve essere decodificato e, in generale, la ricerca di una posizione arbitraria all'interno di una sequenza di byte codificati può avere risultati indefiniti quando si tenta di decodificare in Unicode partendo da quella posizione. Il suggerimento offerto sul link è provare ad aprire il file in modalità binaria ed eseguire la decodifica da soli, rilevando le eccezioni DecodeError.
max

6
NON USARE QUESTO CODICE. Corrompe le linee in alcuni casi limite in Python 2.7. La risposta di @papercrane di seguito la risolve.
xApple

88

Presuppone un sistema simile a unix su Python 2 che puoi fare:

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]

Per python 3 puoi fare:

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
Dovrebbe essere indipendente dalla piattaforma. Inoltre, se leggi la domanda vedrai che f è un file come oggetto.
Armin Ronacher,

40
la domanda non dice che la dipendenza dalla piattaforma è inaccettabile. non riesco a capire perché questo meriti due voti negativi quando fornisce un modo molto univoco (può essere quello che stai cercando ... sicuramente è stato per me) di fare esattamente ciò che la domanda pone.
Shabbyrobe,

3
Grazie, stavo pensando di doverlo risolvere in Python puro, ma non c'è motivo di non usare le utility UNIX quando sono a portata di mano, quindi sono andato con questo. FWIW nel moderno Python, subprocess.check_output è probabilmente preferibile a os.popen2; semplifica un po 'le cose in quanto restituisce semplicemente l'output come stringa e genera un codice di uscita diverso da zero.
mrooney,

3
Sebbene questo dipenda dalla piattaforma, è un modo molto efficiente di fare ciò che è stato chiesto, oltre ad essere un modo estremamente veloce di farlo (non è necessario caricare l'intero file in memoria). @Shabbyrobe
earthmeLon

6
Potresti voler ricalcolare l'offset come: offset_total = str(n+offset)e sostituire questa linea stdin,stdout = os.popen2("tail -n "+offset_total+" "+f)per evitareTypeErrors (cannot concatenate int+str)
Aggiungendo Color

32

Ecco la mia risposta Pitone puro. Usando timeit sembra abbastanza veloce. Coda di 100 righe di un file di registro con 100.000 righe:

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

Ecco il codice:

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
Soluzione elegante! È if len(lines_found) > lines:davvero necessario? Anche la loopcondizione non la catturerebbe?
Maximilian Peters,

Una domanda per la mia comprensione: viene os.SEEK_ENDutilizzata semplicemente per chiarezza? Per quanto ho trovato, il suo valore è costante (= 2). Mi chiedevo di lasciarlo fuori per poter tralasciare il import os. Grazie per l'ottima soluzione!
n1k31t4,

2
@MaximilianPeters sì. Non è necessario. L'ho commentato.
Glenbot,

@DexterMorgan è possibile sostituire os.SEEK_ENDcon il suo equivalente intero. Era principalmente lì per la leggibilità.
Glenbot,

1
Ho votato a favore, ma ho un piccolo valore. Al termine della ricerca, la prima linea di lettura può essere incompleta, in modo da ottenere N _complete_lines ho cambiato la while len(lines_found) < linesa while len(lines_found) <= linesnella mia copia. Grazie!
Graham Klyne,

30

Se la lettura dell'intero file è accettabile, utilizzare un deque.

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

Prima della 2.6, i deques non avevano un'opzione maxlen, ma è abbastanza facile da implementare.

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 è necessario leggere il file alla fine, utilizzare una ricerca al galoppo (alias esponenziale).

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

Perché quella funzione inferiore funziona? pos *= 2sembra completamente arbitrario. Qual è il suo significato?
2

1
@ 2mac Ricerca esponenziale . Legge dalla fine del file in modo iterativo, raddoppiando la quantità letta ogni volta, fino a quando non vengono trovate abbastanza righe.
A. Coady,

Penso che la soluzione da leggere alla fine non supporterà i file codificati con UTF-8, poiché la lunghezza del carattere è variabile e potresti (probabilmente lo farai) atterrare con qualche offset dispari che non può essere interpretato correttamente.
Mike,

sfortunatamente la tua soluzione di ricerca al galoppo non funziona per python 3. Dato che f.seek () non ha un offset negativo. Ho aggiornato il tuo codice per farlo funzionare per il collegamento
itsjwala,

25

La risposta di S.Lott sopra funziona quasi per me, ma finisce per darmi linee parziali. Si scopre che corrompe i dati sui limiti dei blocchi perché i dati mantengono i blocchi letti in ordine inverso. Quando viene chiamato '' .join (data), i blocchi sono nell'ordine sbagliato. Questo risolve questo.

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
L'inserimento all'inizio dell'elenco è una cattiva idea. Perché non usare la struttura del deque?
Sergey11g

1
Purtroppo non compatibile con Python 3 ... cercando di capire perché.
Sherlock70

20

Il codice che ho finito per usare. Penso che questo sia il migliore finora:

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
non risponde esattamente alla domanda.
Sheki,

13

Soluzione semplice e veloce 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
Questa è probabilmente la risposta più rapida quando l'input potrebbe essere enorme (o lo sarebbe, se usasse il .rfindmetodo per scansionare all'indietro alla ricerca di nuove righe, piuttosto che eseguire controlli byte alla volta a livello Python; in CPython, sostituendo il codice livello Python con Le chiamate interne C di solito vincono di molto). Per input più piccoli, dequecon a maxlenè più semplice e probabilmente altrettanto veloce.
ShadowRanger,

4

Una versione compatibile ancora più pulita di python3 che non si inserisce ma aggiunge e inverte:

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

usalo così:

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

Non troppo malandato - ma in generale consiglierei di non aggiungere una risposta a una domanda di 10 anni con molte risposte. Ma aiutatemi: che cosa è specifico di Python 3 nel vostro codice?
usr2564301

Le altre risposte non erano esattamente funzionando bene :-) PY3: vedi stackoverflow.com/questions/136168/...
Hauke Rehfeld

3

Aggiorna la soluzione @papercrane a python3. Apri il file con 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

Pubblicare una risposta per volere di commentatori sulla mia risposta a una domanda simile in cui la stessa tecnica è stata utilizzata per mutare l'ultima riga di un file, non solo per ottenerlo.

Per un file di dimensioni significative, mmapè il modo migliore per farlo. Per migliorare la mmaprisposta esistente , questa versione è portatile tra Windows e Linux e dovrebbe funzionare più velocemente (anche se non funzionerà senza alcune modifiche su Python a 32 bit con file nell'intervallo GB, vedere l' altra risposta per suggerimenti sulla gestione di questo e per modificare il funzionamento su 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)

Ciò presuppone che il numero di righe codificate sia abbastanza piccolo da poterle leggere in sicurezza tutte in una volta; potresti anche rendere questa una funzione generatore e leggere manualmente una riga alla volta sostituendo la riga finale 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)

Infine, questo letto in modalità binaria (necessario da usare mmap) in modo da dare strlinee (Py2) e byteslinee (Py3); se si desidera unicode(Py2) o str(Py3), è possibile modificare l'approccio iterativo per decodificare e / o correggere le nuove righe:

        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: ho scritto tutto su una macchina in cui non ho accesso a Python per testare. Per favore fatemi sapere se ho digitato qualcosa; questo era abbastanza simile alla mia altra risposta che penso che dovrebbe funzionare, ma le modifiche (ad esempio la gestione di un offset) potrebbero portare a sottili errori. Per favore fatemi sapere nei commenti se ci sono errori.


3

Ho trovato il Popen sopra per essere la migliore soluzione. È veloce e sporco e funziona per Python 2.6 su macchine Unix ho usato quanto segue

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 avrà conterrà le ultime n righe del codice. per iterare attraverso soutput riga per riga fare:

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

2

basato sulla risposta più votata di S.Lott (25 settembre 2008 alle 21:43), ma risolto per file di piccole dimensioni.

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

Spero sia utile.


2

Esistono alcune implementazioni esistenti di tail su pypi che puoi installare usando pip:

  • mtFileUtil
  • multtail
  • log4tailer
  • ...

A seconda della situazione, potrebbero esserci dei vantaggi nell'utilizzare uno di questi strumenti esistenti.


Sei a conoscenza di qualche modulo che funziona su Windows? Ci ho provato tailhead, tailerma non hanno funzionato. Anche provato mtFileUtil. Inizialmente stava generando un errore perché le printdichiarazioni non avevano parentesi (I am on Python 3.6). Ho aggiunto quelli in reverse.pye i messaggi di errore erano spariti ma quando il mio script chiama il modulo ( mtFileUtil.tail(open(logfile_path), 5)), non stampa nulla.
Technext,

2

Semplice:

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

Questa è un'implementazione totalmente negativa. Prendi in considerazione la gestione di file di grandi dimensioni e dove n è anche un'operazione enorme e troppo costosa
Nivesh Krishna,

1

Per efficienza con file di dimensioni molto grandi (comuni nelle situazioni di file di registro in cui potresti voler usare tail), in genere vuoi evitare di leggere l'intero file (anche se lo fai senza leggere l'intero file in memoria contemporaneamente) è necessario in qualche modo elaborare l'offset in linee anziché in caratteri. Una possibilità è leggere all'indietro con seek () char da char, ma questo è molto lento. Invece, è meglio elaborare in blocchi più grandi.

Ho una funzione di utilità che ho scritto qualche tempo fa per leggere i file all'indietro che possono essere utilizzati qui.

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

[Modifica] Aggiunta versione più specifica (evita la necessità di invertire due volte)


Un rapido test mostra che questo funziona molto peggio della mia versione dall'alto. Probabilmente a causa del tuo buffering.
Armin Ronacher,

Ho il sospetto che sia perché sto facendo più ricerche all'indietro, quindi non sto usando altrettanto bene il buffer readahead. Tuttavia, penso che potrebbe fare di meglio quando la tua ipotesi sulla lunghezza della linea non è accurata (es. Linee molto grandi), in quanto evita di dover rileggere i dati in questo caso.
Brian,

1

puoi andare alla fine del tuo file con f.seek (0, 2) e poi leggere le righe una ad una con la seguente sostituzione per 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

Basato sulla risposta di Eyecue (10 giugno 10 alle 21:28): questa classe aggiunge il metodo head () e tail () all'oggetto file.

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

Molte di queste soluzioni hanno problemi se il file non termina in \ n o nel garantire la lettura della prima riga 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

Ecco un'implementazione piuttosto semplice:

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

Ottimo esempio! Potresti spiegare l'uso di try prima del f.seek? Perché non prima del with open? Inoltre, perché nel exceptfai un f.readlines()??

Onestamente, il tentativo dovrebbe probabilmente iniziare per primo .. Non ricordo di avere una ragione per non catturare open () se non su un sistema Linux standard sano, / etc / passwd dovrebbe essere sempre leggibile. prova, quindi con è l'ordine più comune.
GL2014,

1

C'è un modulo molto utile che può fare questo:

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

Un'altra soluzione

se il tuo file txt è simile al seguente: mouse snake cat lizard wolf dog

potresti invertire questo file semplicemente usando l'indicizzazione dell'array in 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)

risultato: cane lupo lucertola gatto


1

Il modo più semplice è usare deque:

from collections import deque

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

0

Ho dovuto leggere un valore specifico dall'ultima riga di un file e sono incappato in questo thread. Invece di reinventare la ruota in Python, ho finito con un piccolo script shell, salvato come / usr / local / bin / get_last_netp:

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

E nel programma Python:

from subprocess import check_output

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

0

Non il primo esempio usando un deque, ma uno più semplice. Questo è generale: funziona su qualsiasi oggetto iterabile, non solo su un file.

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

Aggiornamento per risposta data da A.Coady

Funziona con Python 3 .

Questo utilizza la ricerca esponenziale e bufferizza solo le Nrighe dal retro ed è molto efficiente.

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

A pensarci bene, questo è probabilmente veloce come qualsiasi altra cosa qui.

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]

È molto più semplice. E sembra strappare ad un buon ritmo.


Perché quasi tutto qui non funziona con file di registro con più di 30 MB o giù di lì senza caricare la stessa quantità di memoria nella RAM;) La tua prima versione è molto migliore, ma per i file di test qui ha prestazioni leggermente peggiori della mia e non funziona con diversi personaggi newline.
Armin Ronacher,

3
Mi sbagliavo. La versione 1 ha richiesto 0,00248908996582 per 10 code attraverso il dizionario. La versione 2 ha impiegato 1.2963051796 per 10 code attraverso il dizionario. Mi sarei quasi votato verso il basso.
S.Lott

"non funziona con diversi personaggi newline." Sostituisci datacount ('\ n') con len (data.splitlines ()) se è importante.
S.Lott
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.