Uzyskaj ostatnie n wierszy pliku, podobnie jak tail


181

Piszę przeglądarkę plików dziennika dla aplikacji sieci web i do tego chcę paginować według wierszy pliku dziennika. Elementy w pliku są oparte na linii z najnowszym elementem na dole.

Potrzebuję więc tail()metody, która może odczytywać nlinie od dołu i obsługuje przesunięcie. To, co wymyśliłem, wygląda następująco:

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

Czy to rozsądne podejście? Jaki jest zalecany sposób łączenia plików dziennika z przesunięciami?


W moim systemie (linux SLES 10) szukanie względem końca powoduje błąd IOError „nie można wykonać niezerowych poszukiwań względem końca”. Podoba mi się to rozwiązanie, ale zmodyfikowałem je, aby uzyskać długość pliku ( seek(0,2)wtedy tell()), i użyj tej wartości, aby wyszukać względem początku.
Anne

2
Gratulacje - to pytanie trafiło do kodu źródłowego Kippo
Miles

Parametry openpolecenia wykorzystywanego do generowania fobiektu plik powinien być określony, ponieważ zależnie od tego czy f=open(..., 'rb')lub musi być przetwarzany różnief=open(..., 'rt')f
Igor fobia

Odpowiedzi:


123

To może być szybsze niż twoje. Nie przyjmuje żadnych założeń dotyczących długości linii. Cofa plik po bloku, aż znajdzie odpowiednią liczbę znaków „\ 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:])

Nie lubię podchwytliwych założeń dotyczących długości linii, gdy - ze względów praktycznych - nigdy nie możesz wiedzieć takich rzeczy.

Zasadniczo spowoduje to zlokalizowanie ostatnich 20 linii przy pierwszym lub drugim przejściu przez pętlę. Jeśli twoja 74 postać jest właściwie dokładna, ustawiasz rozmiar bloku 2048 i prawie natychmiast odsuwasz 20 linii.

Ponadto nie spalam zbyt wiele kalorii w mózgu, próbując precyzyjnie dopasować się do fizycznych bloków systemu operacyjnego. Korzystając z tych wysokopoziomowych pakietów we / wy, wątpię, abyś zobaczył jakąkolwiek konsekwencję wydajnościową próbowania wyrównania na granicach bloków systemu operacyjnego. Jeśli używasz we / wy niższego poziomu, możesz zauważyć przyspieszenie.


AKTUALIZACJA

w Pythonie 3.2 i nowszych postępuj zgodnie z procesem bajtów, ponieważ w plikach tekstowych (otwartych bez „b” w ciągu trybu) dozwolone są tylko wyszukiwania względem początku pliku (wyjątek dotyczy samego końca pliku z funkcją seek (0, 2)) .:

na przykład: 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
Nie udaje się to w przypadku małych plików dziennika - IOError: nieprawidłowy argument - f.seek (blok * 1024, 2)
ohnoes

1
Rzeczywiście bardzo miłe podejście. Użyłem nieco zmodyfikowanej wersji powyższego kodu i wymyśliłem ten przepis: code.activestate.com/recipes/577968-log-watcher-tail-f-log
Giampaolo Rodolà

6
Nie działa już w Pythonie 3.2. Dostaję, io.UnsupportedOperation: can't do nonzero end-relative seeksże mogę zmienić offset na 0, ale to przeczy celowi funkcji.
Logiczny błąd

4
@DavidEnglund Powód jest tutaj . W skrócie: wyszukiwanie w stosunku do końca pliku jest niedozwolone w trybie tekstowym, prawdopodobnie dlatego, że zawartość pliku musi zostać zdekodowana, i, ogólnie rzecz biorąc, wyszukiwanie do dowolnej pozycji w sekwencji zakodowanych bajtów może mieć niezdefiniowane wyniki, gdy próba dekodowania do Unicode, zaczynając od tej pozycji. Sugerowana w linku sugestia polega na próbie otwarcia pliku w trybie binarnym i samodzielnym dekodowaniu, przechwytując wyjątki DecodeError.
maks.

6
NIE UŻYWAJ TEGO KODU. W niektórych przypadkach granicznych psuje linie w Pythonie 2.7. Odpowiedź z @ papercrane poniżej to naprawia.
xApple

88

Zakłada system uniksopodobny w Pythonie 2, który możesz wykonać:

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]

W przypadku Pythona 3 możesz wykonać:

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
Powinien być niezależny od platformy. Poza tym, jeśli przeczytasz pytanie, zobaczysz, że f jest obiektem podobnym do pliku.
Armin Ronacher

40
pytanie nie mówi, że zależność od platformy jest niedopuszczalna. nie rozumiem, dlaczego zasługuje to na dwa negatywne opinie, gdy zapewnia bardzo unikalny (być może to, czego szukasz ... z pewnością był dla mnie) sposób robienia dokładnie tego, o co pyta pytanie.
Shabbyrobe

3
Dzięki, myślałem, że muszę to rozwiązać w czystym Pythonie, ale nie ma powodu, aby nie używać narzędzi UNIX, gdy są one pod ręką, więc poszedłem z tym. FWIW we współczesnym języku Python subprocess.check_output jest prawdopodobnie lepszy niż os.popen2; upraszcza to nieco, ponieważ zwraca wynik jako ciąg znaków i podnosi niezerowy kod wyjścia.
mrooney

3
Mimo że jest to zależne od platformy, jest to bardzo skuteczny sposób wykonywania tego, o co proszono, a także niezwykle szybki sposób (nie musisz ładować całego pliku do pamięci). @Shabbyrobe
earthmeLon

6
Być może zechcesz wstępnie obliczyć przesunięcie, takie jak: offset_total = str(n+offset)i zastąp tę linię, stdin,stdout = os.popen2("tail -n "+offset_total+" "+f)aby uniknąćTypeErrors (cannot concatenate int+str)
AddingColor

32

Oto moja odpowiedź. Czysty python. Korzystanie z czasu wydaje się dość szybkie. Dostosowywanie 100 linii pliku dziennika zawierającego 100 000 linii:

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

Oto kod:

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
Eleganckie rozwiązanie! Czy to if len(lines_found) > lines:naprawdę konieczne? Czy loopwarunek też tego nie złapie?
Maximilian Peters

Pytanie dla mojego zrozumienia: jest os.SEEK_ENDużywane po prostu dla jasności? O ile się przekonałem, jego wartość jest stała (= 2). Zastanawiałem się, czy pominąć to, aby móc pominąć import os. Dzięki za świetne rozwiązanie!
n1k31t4

2
@MaximilianPeters tak. To nie jest konieczne. Skomentowałem to.
glenbot

@DexterMorgan możesz zastąpić os.SEEK_ENDliczbą całkowitą. Było tam głównie dla czytelności.
glenbot

1
Głosowałem za głosem, ale mam małą nitkę. Po poszukiwania, pierwsza linia odczytu mogą być niekompletne, tak aby uzyskać N _complete_lines Zmieniłem while len(lines_found) < linessię while len(lines_found) <= linesw moim egzemplarzu. Dzięki!
Graham Klyne,

30

Jeśli odczytanie całego pliku jest dopuszczalne, użyj deque.

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

Przed wersją 2.6 deques nie miały opcji maxlen, ale można ją łatwo wdrożyć.

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

Jeśli wymagane jest odczytanie pliku od końca, użyj wyszukiwania galopowego (aka wykładniczego).

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

Dlaczego ta funkcja dolna działa? pos *= 2wydaje się całkowicie arbitralny. Jakie jest jego znaczenie?
2mac

1
@ 2 wykładnicze wyszukiwanie wykładnicze . Odczytuje iteracyjnie od końca pliku, podwajając ilość odczytów za każdym razem, aż do znalezienia wystarczającej liczby wierszy.
A. Coady

Myślę, że rozwiązanie do odczytu od końca nie obsługuje plików zakodowanych za pomocą UTF-8, ponieważ długość znaków jest zmienna, a ty możesz (prawdopodobnie będzie) wylądować z pewnym dziwnym przesunięciem, którego nie można poprawnie zinterpretować.
Mike

niestety twoje galopujące rozwiązanie wyszukiwania nie działa dla Pythona 3. Ponieważ f.seek () nie przyjmuje ujemnego przesunięcia. Zaktualizowałem twój kod, aby działał dla linku
itsjwala

25

Powyższa odpowiedź S.Lott prawie dla mnie działa, ale ostatecznie daje mi częściowe zdanie. Okazuje się, że powoduje uszkodzenie danych na granicach bloków, ponieważ dane blokują odczytane bloki w odwrotnej kolejności. Po wywołaniu „.join (dane) bloki są w niewłaściwej kolejności. To naprawia to.

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
Wstawianie na początku listy to zły pomysł. Dlaczego nie użyć struktury deque?
Sergey11g

1
Niestety nie jest kompatybilny z Python 3 ... próbuje dowiedzieć się, dlaczego.
Sherlock70

20

Kod, którego używałem. Myślę, że jak dotąd jest to najlepsze:

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
nie odpowiada dokładnie na pytanie.
sheki

13

Proste i szybkie rozwiązanie z 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
Jest to prawdopodobnie najszybsza odpowiedź, gdy dane wejściowe mogą być ogromne (lub byłoby, gdyby użył .rfindmetody skanowania wstecz w poszukiwaniu nowych linii, zamiast sprawdzania bajtów naraz na poziomie Pythona; w CPython zastępowanie kodu poziomu Python Wbudowane połączenia C zwykle dużo wygrywają). W przypadku mniejszych wejść, dequez maxlenjest prostsze i prawdopodobnie podobnie szybkie.
ShadowRanger

4

Jeszcze czystsza wersja zgodna z Python3, która nie wstawia, ale dołącza i odwraca:

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

użyj tego w ten sposób:

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

Niezbyt obskurny - ale ogólnie radziłbym nie dodawać odpowiedzi na 10-letnie pytanie z dużą ilością odpowiedzi. Ale pomóż mi: co jest specyficzne dla Python 3 w twoim kodzie?
usr2564301

Inne odpowiedzi nie działały dobrze :-) py3: patrz stackoverflow.com/questions/136168/…
Hauke ​​Rehfeld

3

Zaktualizuj rozwiązanie @papercrane do python3. Otwórz plik za pomocą open(filename, 'rb')i:

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

Publikując odpowiedź na żądanie komentujących moją odpowiedź na podobne pytanie, w którym zastosowano tę samą technikę do mutacji ostatniego wiersza pliku, a nie tylko go otrzymuję.

W przypadku pliku o znacznych rozmiarach mmapjest to najlepszy sposób na zrobienie tego. Aby poprawić istniejącą mmapodpowiedź, ta wersja jest przenośna między systemami Windows i Linux i powinna działać szybciej (chociaż nie będzie działać bez pewnych modyfikacji 32-bitowego Pythona z plikami w zakresie GB, zobacz inną odpowiedź na wskazówki dotyczące obsługi tego oraz do modyfikacji w celu pracy z 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)

Zakłada się, że liczba linii jest na tyle mała, że ​​można bezpiecznie odczytać je wszystkie do pamięci jednocześnie; możesz także ustawić tę funkcję jako generator i ręcznie odczytywać wiersz, zastępując ostatni wiersz:

        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)

Na koniec odczytany w trybie binarnym (konieczny do użycia mmap), więc podaje strlinie (Py2) i byteslinie (Py3); jeśli chcesz unicode(Py2) lub str(Py3), iteracyjne podejście można zmodyfikować w celu odkodowania dla Ciebie i / lub naprawienia nowych linii:

        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

Uwaga: wpisałem to wszystko na maszynie, na której nie mam dostępu do Pythona do przetestowania. Daj mi znać, jeśli coś napisałem na maszynie; było to na tyle podobne do mojej innej odpowiedzi , że myślę, że powinno działać, ale poprawki (np. obsługa an offset) mogą prowadzić do subtelnych błędów. Daj mi znać w komentarzach, jeśli są jakieś błędy.


3

Uważam, że Popen powyżej jest najlepszym rozwiązaniem. Jest szybki i brudny i działa W przypadku Pythona 2.6 na maszynie Unix zastosowałem następujące

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 będzie zawierał ostatnie n wierszy kodu. aby wykonać iterację przez Soutput linia po linii:

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

2

na podstawie najczęściej głosowanej odpowiedzi S.Lott (25 września 08 21:43), ale naprawiono dla małych plików.

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

Mam nadzieję, że to się przyda.


2

Istnieje kilka istniejących implementacji taila na pypi, które można zainstalować za pomocą pip:

  • mtFileUtil
  • multitail
  • log4tailer
  • ...

W zależności od sytuacji korzystanie z jednego z tych istniejących narzędzi może być korzystne.


Czy znasz moduł działający w systemie Windows? Próbowałem tailhead, tailerale nie działały. Próbowałem też mtFileUtil. Początkowo powodował błąd, ponieważ printinstrukcje nie miały nawiasów (korzystam z Pythona 3.6). Dodałem je reverse.pyi komunikaty o błędach zniknęły, ale kiedy mój skrypt wywołuje moduł ( mtFileUtil.tail(open(logfile_path), 5)), nic nie drukuje.
Technext,

2

Prosty :

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

To jest całkowicie złe wdrożenie. Zastanów się nad obsługą dużych plików, a gdzie n jest również ogromną, zbyt kosztowną operacją
Nivesh Krishna

1

Aby uzyskać wydajność w przypadku bardzo dużych plików (często w sytuacjach, gdy konieczne jest użycie pliku dziennika), zwykle należy unikać czytania całego pliku (nawet jeśli robisz to bez wczytywania całego pliku do pamięci). trzeba jakoś wypracować przesunięcie w wierszach, a nie w znakach. Jedną z możliwości jest czytanie wstecz przy pomocy seek () char po char, ale jest to bardzo wolne. Zamiast tego lepiej jest przetwarzać w większych blokach.

Mam funkcję narzędzia, którą napisałem jakiś czas temu, aby odczytać pliki do tyłu, których można tu użyć.

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

[Edytuj] Dodano bardziej szczegółową wersję (pozwala uniknąć dwukrotnego cofnięcia)


Szybkie testy pokazują, że działa to znacznie gorzej niż moja wersja z góry. Prawdopodobnie z powodu buforowania.
Armin Ronacher

Podejrzewam, że dzieje się tak, ponieważ wykonuję wiele operacji wyszukiwania wstecz, więc nie używam tak dobrze bufora readahead. Myślę jednak, że może być lepiej, gdy twoje przypuszczenia dotyczące długości linii nie są dokładne (np. Bardzo duże linie), ponieważ pozwala to uniknąć konieczności ponownego odczytu danych w tym przypadku.
Brian

1

możesz przejść do końca pliku za pomocą f.seek (0, 2), a następnie odczytywać wiersze jeden po drugim z następującym zamiennikiem 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

Na podstawie odpowiedzi Eyecue (10 czerwca 10 o 21:28): ta klasa dodaje metodę head () i tail () do obiektu pliku.

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

Stosowanie:

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

1

Kilka z tych rozwiązań ma problemy, jeśli plik nie kończy się na \ n lub zapewnia, że ​​zostanie przeczytany cały pierwszy wiersz.

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

Oto całkiem prosta implementacja:

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

Świetny przykład! Czy mógłbyś wyjaśnić użycie try przed f.seek? Dlaczego nie przed with open? Ponadto, dlaczego exceptrobisz f.readlines()??

Szczerze mówiąc, próba powinna prawdopodobnie rozpocząć się najpierw. Nie pamiętam powodu, dla którego nie złapałem open () inaczej niż na zdrowym standardowym systemie Linux, / etc / passwd zawsze powinno być czytelne. spróbuj, a następnie z jest bardziej powszechną kolejnością.
GL2014

1

Istnieje bardzo przydatny moduł, który może to zrobić:

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

Inne rozwiązanie

jeśli twój plik txt wygląda tak: mysz wąż kot jaszczurka wilk pies

możesz odwrócić ten plik, po prostu używając indeksowania tablic w pythonie ''

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)

wynik: pies wilk jaszczurka kot


1

Najprostszym sposobem jest użycie deque:

from collections import deque

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

0

Musiałem odczytać określoną wartość z ostatniego wiersza pliku i natknąłem się na ten wątek. Zamiast na nowo wymyślić koło w Pythonie, skończyłem z małym skryptem powłoki, zapisanym jako / usr / local / bin / get_last_netp:

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

A w programie Python:

from subprocess import check_output

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

0

Nie pierwszy przykład z użyciem deque, ale prostszy. Ten jest ogólny: działa na każdym iterowalnym obiekcie, nie tylko na pliku.

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

Aktualizacja dla odpowiedzi udzielonej przez A.Coady

Działa z python 3 .

Korzysta z wyszukiwania wykładniczego i buforuje tylko Nwiersze z tyłu i jest bardzo wydajny.

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

Po namyśle jest to prawdopodobnie tak szybkie, jak cokolwiek tutaj.

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]

To o wiele prostsze. I wydaje się, że rozpruwa się w dobrym tempie.


Ponieważ prawie wszystko tutaj nie działa z plikami dziennika o rozmiarze większym niż 30 MB, bez ładowania tej samej ilości pamięci do pamięci RAM;) Twoja pierwsza wersja jest znacznie lepsza, ale w przypadku plików testowych działa nieco gorzej niż moja i nie działa z różnymi znakami nowej linii.
Armin Ronacher

3
Myliłem się. Wersja 1 zajęła 0,00248908996582 przez 10 ogonów w słowniku. Wersja 2 zajęła 1.2963051796 na 10 ogonów w słowniku. Prawie zagłosowałbym w dół.
S.Lott,

„nie działa z różnymi znakami nowej linii”. W razie potrzeby zamień konto danych ('\ n') na len (data.splitlines ()).
S.Lott,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.