Holen Sie sich mit Python die letzten n Zeilen einer Datei, ähnlich wie bei tail


181

Ich schreibe einen Protokolldatei-Viewer für eine Webanwendung und möchte dafür durch die Zeilen der Protokolldatei paginieren. Die Elemente in der Datei basieren auf dem neuesten Element unten.

Ich brauche also eine tail()Methode, die nZeilen von unten lesen kann und einen Versatz unterstützt. Was ich mir ausgedacht habe, sieht so aus:

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

Ist das ein vernünftiger Ansatz? Was ist die empfohlene Methode, um Protokolldateien mit Offsets zu versehen?


Auf meinem System (Linux SLES 10) löst das Suchen relativ zum Ende einen IOError aus, der keine endbezogenen Suchvorgänge ungleich Null durchführen kann. Ich mag diese Lösung, habe sie aber geändert, um die Dateilänge ( seek(0,2)dann tell()) zu erhalten, und verwende diesen Wert, um relativ zum Anfang zu suchen.
Anne

2
Herzlichen Glückwunsch - diese Frage hat es in den Kippo-Quellcode geschafft
Miles

Die Parameter des openBefehls, der zum Generieren des ff=open(..., 'rb')f=open(..., 'rt')f
Dateiobjekts

Antworten:


123

Dies kann schneller sein als deins. Macht keine Annahmen über die Leitungslänge. Durchläuft die Datei blockweise, bis die richtige Anzahl von '\ n' Zeichen gefunden wurde.

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

Ich mag keine kniffligen Annahmen über die Zeilenlänge, wenn man so etwas praktisch nie wissen kann.

Im Allgemeinen werden dadurch die letzten 20 Zeilen beim ersten oder zweiten Durchgang durch die Schleife lokalisiert. Wenn Ihre Sache mit 74 Zeichen tatsächlich korrekt ist, stellen Sie die Blockgröße 2048 ein und Sie werden fast sofort 20 Zeilen abschließen.

Außerdem verbrenne ich nicht viele Gehirnkalorien, um die Ausrichtung auf physische Betriebssystemblöcke zu verfeinern. Ich bezweifle, dass Sie bei Verwendung dieser übergeordneten E / A-Pakete Leistungsfolgen sehen, wenn Sie versuchen, sich an den Blockgrenzen des Betriebssystems auszurichten. Wenn Sie E / A auf niedrigerer Ebene verwenden, wird möglicherweise eine Beschleunigung angezeigt.


AKTUALISIEREN

Befolgen Sie für Python 3.2 und höher den Vorgang für Bytes wie in Textdateien (diejenigen, die ohne ein "b" in der Moduszeichenfolge geöffnet wurden ), sind nur Suchvorgänge relativ zum Dateianfang zulässig (mit Ausnahme der Suche bis zum Ende der Datei) mit suchen (0, 2)).:

z.B: 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
Dies schlägt bei kleinen Protokolldateien fehl - IOError: ungültiges Argument - f.seek (Block * 1024, 2)
ohnoes

1
Sehr schöner Ansatz. Ich habe eine leicht modifizierte Version des obigen Codes verwendet und mir
Giampaolo Rodolà

6
Funktioniert nicht mehr in Python 3.2. Ich bekomme, io.UnsupportedOperation: can't do nonzero end-relative seeksich kann den Offset auf 0 ändern, aber das macht den Zweck der Funktion zunichte.
Logischer Irrtum

4
@ DavidEnglund Grund ist hier . Kurz gesagt: Das Suchen relativ zum Ende der Datei ist im Textmodus nicht zulässig, vermutlich weil der Dateiinhalt dekodiert werden muss, und im Allgemeinen kann das Suchen nach einer beliebigen Position innerhalb einer Folge von codierten Bytes undefinierte Ergebnisse haben, wenn Sie Versuchen Sie, ab dieser Position in Unicode zu dekodieren. Der unter dem Link angebotene Vorschlag besteht darin, die Datei im Binärmodus zu öffnen und die Dekodierung selbst durchzuführen, wobei die DecodeError-Ausnahmen abgefangen werden.
Max

6
VERWENDEN SIE DIESEN CODE NICHT. In einigen Grenzfällen in Python 2.7 werden Zeilen beschädigt. Die Antwort von @papercrane unten behebt das Problem.
xApple

88

Nimmt ein Unix-ähnliches System unter Python 2 an, das Sie ausführen können:

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]

Für Python 3 können Sie Folgendes tun:

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
Sollte plattformunabhängig sein. Wenn Sie die Frage lesen, werden Sie außerdem sehen, dass f ein dateiähnliches Objekt ist.
Armin Ronacher

40
Die Frage besagt nicht, dass die Abhängigkeit von der Plattform inakzeptabel ist. Ich verstehe nicht, warum dies zwei Abstimmungen verdient, wenn es eine sehr unixy (vielleicht das, wonach Sie suchen ... sicherlich war es für mich) Möglichkeit bietet, genau das zu tun, was die Frage stellt.
Shabbyrobe

3
Vielen Dank, ich dachte, ich müsste dies in reinem Python lösen, aber es gibt keinen Grund, UNIX-Dienstprogramme nicht zu verwenden, wenn sie zur Hand sind, also habe ich mich dafür entschieden. FWIW in modernem Python ist subprocess.check_output wahrscheinlich os.popen2 vorzuziehen; Es vereinfacht die Dinge ein wenig, da es nur die Ausgabe als Zeichenfolge zurückgibt und einen Exit-Code ungleich Null auslöst.
mrooney

3
Obwohl dies plattformabhängig ist, ist es eine sehr effiziente Methode, um das zu tun, was gefragt wurde, und eine extrem schnelle Methode, dies zu tun (Sie müssen nicht die gesamte Datei in den Speicher laden). @ Shabbyrobe
earthmeLon

6
Möglicherweise möchten Sie den Versatz wie offset_total = str(n+offset)folgt vorberechnen und diese Zeile ersetzen stdin,stdout = os.popen2("tail -n "+offset_total+" "+f), um zu vermeidenTypeErrors (cannot concatenate int+str)
AddingColor

32

Hier ist meine Antwort. Reine Python. Mit timeit scheint es ziemlich schnell zu sein. Tailing 100 Zeilen einer Protokolldatei mit 100.000 Zeilen:

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

Hier ist der Code:

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
Elegante Lösung! Ist das if len(lines_found) > lines:wirklich notwendig? Würde der loopZustand es nicht auch fangen?
Maximilian Peters

Eine Frage für mein Verständnis: os.SEEK_ENDWird nur zur Klarheit verwendet? Soweit ich festgestellt habe, ist sein Wert konstant (= 2). Ich habe mich gefragt, ob ich es weglassen soll, um das weglassen zu können import os. Danke für die tolle Lösung!
n1k31t4

2
@ MaximilianPeters ja. Es ist nicht nötig. Ich habe es auskommentiert.
Glenbot

@DexterMorgan können Sie durch os.SEEK_ENDdas Integer-Äquivalent ersetzen . Es war hauptsächlich für die Lesbarkeit da.
Glenbot

1
Ich habe gestimmt, habe aber eine kleine Schwäche. Nach dem sucht, kann die erste Zeile Lese unvollständig sein, so zu N _complete_lines bekommt ich das geändert , while len(lines_found) < linesum while len(lines_found) <= linesin meinem Exemplar. Vielen Dank!
Graham Klyne

30

Wenn das Lesen der gesamten Datei akzeptabel ist, verwenden Sie eine Deque.

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

Vor 2.6 hatten Deques keine Maxlen-Option, aber die Implementierung ist einfach genug.

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

Wenn es erforderlich ist, die Datei vom Ende an zu lesen, verwenden Sie eine Galoppsuche (auch Exponentialsuche genannt).

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

Warum funktioniert diese untere Funktion? pos *= 2scheint völlig willkürlich. Welche Bedeutung hat es?
2mac

1
@ 2mac Exponential Search . Es liest iterativ vom Ende der Datei und verdoppelt jedes Mal die gelesene Menge, bis genügend Zeilen gefunden wurden.
A. Coady

Ich denke, dass die vom Ende zu lesende Lösung keine mit UTF-8 codierten Dateien unterstützt, da die Zeichenlänge variabel ist und Sie (wahrscheinlich) mit einem ungeraden Versatz landen könnten, der nicht richtig interpretiert werden kann.
Mike

Leider funktioniert Ihre galoppierende Suchlösung für Python 3 nicht. Da f.seek () keinen negativen Offset nimmt. Ich habe Ihren Code macht es für Python 3 arbeitet aktualisiert Link
itsjwala

25

Die Antwort von S.Lott oben funktioniert fast für mich, gibt mir aber am Ende Teilzeilen. Es stellt sich heraus, dass Daten an Blockgrenzen beschädigt werden, da die Daten die Leseblöcke in umgekehrter Reihenfolge enthalten. Wenn '' .join (Daten) aufgerufen wird, sind die Blöcke in der falschen Reihenfolge. Dies behebt das.

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
Das Einfügen am Anfang der Liste ist eine schlechte Idee. Warum nicht Deque-Struktur verwenden?
Sergey11g

1
Leider nicht Python 3-kompatibel ... versucht herauszufinden, warum.
Sherlock70

20

Der Code, den ich letztendlich verwendet habe. Ich denke, das ist das bisher beste:

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
beantwortet die Frage nicht genau.
Sheki

13

Einfache und schnelle Lösung mit 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
Dies ist wahrscheinlich die schnellste Antwort, wenn die Eingabe sehr groß sein könnte (oder wenn die .rfindMethode verwendet würde, um rückwärts nach Zeilenumbrüchen zu suchen, anstatt Byte-zu-Zeit-Überprüfungen auf Python-Ebene durchzuführen; in CPython wird Python-Code durch Code ersetzt C eingebaute Anrufe gewinnen normalerweise um ein Vielfaches). Für kleinere Eingänge ist das dequemit a maxleneinfacher und wahrscheinlich ähnlich schnell.
ShadowRanger

4

Eine noch sauberere Python3-kompatible Version, die nicht einfügt, sondern anfügt und umkehrt:

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

benutze es so:

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

Nicht allzu schäbig - aber ich würde generell raten, eine Antwort auf eine 10 Jahre alte Frage mit vielen Antworten nicht hinzuzufügen. Aber helfen Sie mir: Was ist spezifisch für Python 3 in Ihrem Code?
usr2564301

Die anderen Antworten funktionierten nicht genau :-) py3: siehe stackoverflow.com/questions/136168/…
Hauke ​​Rehfeld

3

Aktualisieren Sie die @ papercrane-Lösung auf python3. Öffnen Sie die Datei mit open(filename, 'rb')und:

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

Eine Antwort auf Geheiß von Kommentatoren zu meiner Antwort auf eine ähnliche Frage posten bei der dieselbe Technik verwendet wurde, um die letzte Zeile einer Datei zu mutieren, nicht nur um sie zu erhalten.

mmapDies ist für eine Datei von erheblicher Größe der beste Weg, dies zu tun. Um die vorhandene mmapAntwort zu verbessern , ist diese Version zwischen Windows und Linux portierbar und sollte schneller ausgeführt werden (obwohl sie ohne einige Änderungen an 32-Bit-Python mit Dateien im GB-Bereich nicht funktioniert. Hinweise zur Handhabung finden Sie in der anderen Antwort und zum Ändern für 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)

Dies setzt voraus, dass die Anzahl der getailten Zeilen klein genug ist, damit Sie sie alle sicher gleichzeitig in den Speicher einlesen können. Sie können dies auch zu einer Generatorfunktion machen und jeweils eine Zeile manuell lesen, indem Sie die letzte Zeile durch Folgendes ersetzen:

        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)

Zuletzt wird dies im Binärmodus gelesen (zur Verwendung erforderlich mmap), sodass strZeilen (Py2) und bytesZeilen (Py3) erhalten werden. Wenn Sie unicode(Py2) oder str(Py3) möchten, kann der iterative Ansatz optimiert werden, um für Sie zu dekodieren und / oder Zeilenumbrüche zu korrigieren:

        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

Hinweis: Ich habe dies alles auf einem Computer eingegeben, auf dem ich keinen Zugriff auf Python zum Testen habe. Bitte lassen Sie mich wissen, wenn ich etwas getippt habe. Dies war meiner anderen Antwort so ähnlich, dass ich denke, es sollte funktionieren, aber die Optimierungen (z. B. Handhabung eines offset) könnten zu subtilen Fehlern führen. Bitte lassen Sie mich in den Kommentaren wissen, wenn es Fehler gibt.


3

Ich fand den Popen oben die beste Lösung. Es ist schnell und schmutzig und es funktioniert Für Python 2.6 auf Unix-Computer habe ich Folgendes verwendet

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 enthält die letzten n Zeilen des Codes. Zeile für Zeile durch Soutput iterieren:

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

2

basierend auf S.Lotts bester Antwort (25. September 2008 um 21:43 Uhr), aber behoben für kleine Dateien.

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

Hoffe das ist nützlich.


2

Es gibt einige vorhandene Implementierungen von tail on pypi, die Sie mit pip installieren können:

  • mtFileUtil
  • Multitail
  • log4tailer
  • ...

Abhängig von Ihrer Situation kann die Verwendung eines dieser vorhandenen Tools Vorteile haben.


Kennen Sie ein Modul, das unter Windows funktioniert? Ich habe es versucht tailhead, taileraber sie haben nicht funktioniert. Auch versucht mtFileUtil. Es war anfangs ein Fehler, weil printAnweisungen keine Klammern hatten (ich bin auf Python 3.6). Ich habe diese hinzugefügt reverse.pyund die Fehlermeldungen waren verschwunden, aber wenn mein Skript das Modul ( mtFileUtil.tail(open(logfile_path), 5)) aufruft , wird nichts gedruckt.
Technext

2

Einfach:

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

Dies ist eine total schlechte Implementierung. Betrachten Sie den Umgang mit großen Dateien, und wo n auch eine riesige, zu teure Operation ist
Nivesh Krishna

1

Aus Gründen der Effizienz bei sehr großen Dateien (häufig in Protokolldateisituationen, in denen Sie möglicherweise tail verwenden möchten) möchten Sie im Allgemeinen vermeiden, die gesamte Datei zu lesen (auch wenn Sie dies tun, ohne die gesamte Datei auf einmal in den Speicher einzulesen) müssen irgendwie den Versatz in Zeilen anstatt in Zeichen berechnen. Eine Möglichkeit besteht darin, mit seek () char by char rückwärts zu lesen, dies ist jedoch sehr langsam. Stattdessen ist es besser, in größeren Blöcken zu verarbeiten.

Ich habe eine Dienstprogrammfunktion, die ich vor einiger Zeit geschrieben habe, um Dateien rückwärts zu lesen, die hier verwendet werden können.

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

[Bearbeiten] Spezifischere Version hinzugefügt (vermeidet die Notwendigkeit, zweimal umzukehren)


Ein schneller Test zeigt, dass dies viel schlechter abschneidet als meine Version von oben. Wahrscheinlich wegen Ihrer Pufferung.
Armin Ronacher

Ich vermute, das liegt daran, dass ich mehrere Suchvorgänge rückwärts durchführe, sodass der Readahead-Puffer nicht so gut genutzt wird. Ich denke jedoch, dass es möglicherweise besser ist, wenn Ihre Schätzung der Zeilenlänge nicht korrekt ist (z. B. sehr große Zeilen), da in diesem Fall keine Daten erneut gelesen werden müssen.
Brian

1

Sie können mit f.seek (0, 2) zum Ende Ihrer Datei gehen und dann die Zeilen einzeln mit dem folgenden Ersatz für readline () ablesen:

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

Basierend auf der Eyecue-Antwort (10. Juni 10 um 21:28 Uhr): Diese Klasse fügt dem Dateiobjekt die Methoden head () und tail () hinzu.

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

Verwendung:

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

1

Einige dieser Lösungen haben Probleme, wenn die Datei nicht mit \ n endet oder die vollständige erste Zeile gelesen wird.

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

Hier ist eine ziemlich einfache Implementierung:

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

Tolles Beispiel! Könnten Sie bitte die Verwendung von try vor dem erklären f.seek? Warum nicht vor dem with open? Auch warum in der exceptdu ein f.readlines()??

Ehrlich gesagt sollte der Versuch wahrscheinlich an erster Stelle stehen. Ich erinnere mich nicht, dass ich einen Grund hatte, open () nicht zu fangen, außer auf einem gesunden Standard-Linux-System. / Etc / passwd sollte immer lesbar sein. versuchen, dann mit ist die üblichere Reihenfolge.
GL2014

1

Es gibt ein sehr nützliches Modul , das dies tun kann:

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

Eine andere Lösung

Wenn Ihre TXT-Datei so aussieht: Maus Schlange Katze Eidechse Wolf Hund

Sie können diese Datei umkehren, indem Sie einfach die Array-Indizierung in Python '' 'verwenden.

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)

Ergebnis: Hund Wolf Eidechse Katze


1

Der einfachste Weg ist zu verwenden deque:

from collections import deque

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

0

Ich musste einen bestimmten Wert aus der letzten Zeile einer Datei lesen und bin auf diesen Thread gestoßen. Anstatt das Rad in Python neu zu erfinden, erhielt ich ein winziges Shell-Skript, das unter / usr / local / bin / get_last_netp gespeichert wurde:

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

Und im Python-Programm:

from subprocess import check_output

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

0

Nicht das erste Beispiel mit einer Deque, sondern ein einfacheres. Dieser ist allgemein: Er funktioniert mit jedem iterierbaren Objekt, nicht nur mit einer Datei.

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

Update für die Antwort von A.Coady

Funktioniert mit Python 3 .

Dies verwendet die exponentielle Suche und puffert nur NZeilen von hinten und ist sehr effizient.

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

Auf den zweiten Blick ist dies wahrscheinlich genauso schnell wie alles hier.

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 ist viel einfacher. Und es scheint in einem guten Tempo voranzukommen.


Weil fast alles hier nicht mit Protokolldateien mit mehr als 30 MB funktioniert, ohne die gleiche Speichermenge in den RAM zu laden;) Ihre erste Version ist viel besser, aber für die Testdateien hier ist sie etwas schlechter als meine und es funktioniert nicht mit verschiedenen Zeilenumbrüchen.
Armin Ronacher

3
Ich lag falsch. Version 1 hat 0,00248908996582 für 10 Schwänze durch das Wörterbuch geführt. Version 2 hat 1.2963051796 für 10 Schwänze durch das Wörterbuch geführt. Ich würde mich fast abwählen.
S.Lott

"funktioniert nicht mit verschiedenen Zeilenumbrüchen." Ersetzen Sie die Datenanzahl ('\ n') durch len (data.splitlines ()), wenn es darauf ankommt.
S.Lott
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.