Получить последние n строк файла с Python, похожего на tail - PullRequest
167 голосов
/ 26 сентября 2008

Я пишу средство просмотра файла журнала для веб-приложения, и для этого я хочу разбить на страницы строки файла журнала. Элементы в файле являются строками, основанными на самом новом элементе внизу.

Так что мне нужен метод tail(), который может читать n строки снизу и поддерживает смещение. То, что я придумал, выглядит так:

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

Это разумный подход? Каков рекомендуемый способ подгонки файлов журнала со смещением?

Ответы [ 30 ]

117 голосов
/ 26 сентября 2008

Это может быть быстрее, чем у вас. Не делает никаких предположений о длине линии. Выполняется обратный просмотр файла по одному блоку за раз, пока не будет найдено правильное количество символов \ 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:])

Мне не нравятся хитрые предположения о длине линии, когда - с практической точки зрения - вы никогда не узнаете такие вещи.

Как правило, это будет определять последние 20 строк на первом или втором проходе цикла. Если ваша 74-символьная вещь на самом деле точна, вы делаете размер блока 2048, и вы почти сразу же наберете 20 строк.

Кроме того, я не сжигаю много мозговых калорий, пытаясь выровнять соответствие с физическими блоками ОС. Используя эти высокоуровневые пакеты ввода / вывода, я сомневаюсь, что вы увидите какие-либо последствия для производительности при попытке выравнивания по границам блоков ОС. Если вы используете низкоуровневый ввод-вывод, вы можете увидеть ускорение.

76 голосов
/ 26 сентября 2008

Предполагается, что Unix-подобная система на Python 2 вы можете сделать:

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]

Для Python 3 вы можете сделать:

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]
29 голосов
/ 11 ноября 2008

Если чтение всего файла приемлемо, используйте deque.

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

До версии 2.6 у deques не было опции maxlen, но ее достаточно легко реализовать.

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

Если требуется прочитать файл с конца, используйте поиск по галопу (экспоненциально).

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:]
26 голосов
/ 13 августа 2011

Ответ С. Лотта выше почти работает для меня, но в итоге дает мне частичные строки. Оказывается, это повреждает данные на границах блоков, потому что данные хранят прочитанные блоки в обратном порядке. Когда вызывается '.join (данные), блоки расположены в неправильном порядке. Это исправляет это.

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:]
25 голосов
/ 09 декабря 2012

Вот мой ответ. Чистый питон. Использование timeit кажется довольно быстрым. Хвост 100 строк файла журнала, который имеет 100 000 строк:

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

Вот код:

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:]
19 голосов
/ 28 марта 2009

Код, который я использовал в итоге. Я думаю, что это пока лучшее:

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
13 голосов
/ 25 июля 2011

Простое и быстрое решение с помощью 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()
4 голосов
/ 04 января 2018

Еще более чистая версия, совместимая с python3, которая не вставляет, а добавляет и переворачивает:

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

используйте это так:

with open(path, 'rb') as f:
    last_lines = tail(f, 3).decode('utf-8')
3 голосов
/ 16 апреля 2012

Я нашел попен выше, чтобы быть лучшим решением. Это быстро и грязно, и это работает Для python 2.6 на Unix-машине я использовал следующее

    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 будет содержать последние n строк кода. Чтобы перебрать soutput строка за строкой, сделайте:

for line in GetLastNLines(50,'myfile.log').split('\n'):
    print line
3 голосов
/ 01 декабря 2015

Публикация ответа по указанию комментаторов на мой ответ на аналогичный вопрос , где тот же метод использовался для изменения последней строки файла, а не только для его получения.

Для файла значительного размера, mmap - лучший способ сделать это. Чтобы улучшить существующий ответ mmap, эта версия переносима между Windows и Linux и должна работать быстрее (хотя она не будет работать без некоторых модификаций 32-битного Python с файлами в диапазоне ГБ, см. other Ответьте за подсказки по обработке этого, и за изменение для работы на 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)

Предполагается, что количество выровненных строк достаточно мало, и вы можете безопасно прочитать их все сразу в память; Вы также можете сделать это функцией генератора и вручную прочитать строку за раз, заменив последнюю строку на:

        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)

Наконец, это чтение в двоичном режиме (необходимо использовать mmap), поэтому оно дает str строк (Py2) и bytes строк (Py3); если вы хотите unicode (Py2) или str (Py3), итеративный подход может быть изменен для декодирования для вас и / или исправления новых строк:

        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

Примечание: я напечатал все это на машине, где у меня нет доступа к Python для тестирования. Пожалуйста, дайте мне знать, если я что-то опечатал; это было достаточно похоже на мой другой ответ , что я думаю это должно сработать, но изменения (например, обработка offset) могут привести к незначительным ошибкам. Пожалуйста, дайте мне знать в комментариях, если есть какие-либо ошибки.

...