Получить последние n строк файла, похожих на tail

181

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

Поэтому мне нужен 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

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

Армин Ронахер
источник
В моей системе (linux SLES 10) поиск относительно конца вызывает IOError «не может делать ненулевые запросы относительно конца». Мне нравится это решение, но я изменил его, чтобы получить длину файла ( seek(0,2)затем tell()), и использую это значение для поиска относительно начала.
Энн
2
Поздравляю - этот вопрос вошел в исходный код Kippo
Miles
Параметры openкоманды , используемой для создания fобъекта файла должен быть указан, так как в зависимости от того f=open(..., 'rb')или должны быть обработаны по- другомуf=open(..., 'rt')f
Игорь Fobia

Ответы:

123

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

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


ОБНОВИТЬ

для Python 3.2 и выше, следуйте процессу на байтах, как в текстовых файлах (те, которые открыты без «b» в строке режима), разрешен только поиск относительно начала файла (исключение - поиск до самого конца файла с поиском (0, 2)) .:

например: 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
Это терпит неудачу на маленьких файлах журнала - IOError: неверный аргумент - f.seek (блок * 1024, 2)
ohnoes
1
Очень хороший подход на самом деле. Я использовал слегка измененную версию кода выше и придумал этот рецепт: code.activestate.com/recipes/577968-log-watcher-tail-f-log
Джампаоло Родола,
6
Больше не работает в Python 3.2. Я получаю, io.UnsupportedOperation: can't do nonzero end-relative seeksя могу изменить смещение на 0, но это противоречит цели функции.
Логическая ошибка
4
@DavidEnglund Причина здесь . Вкратце: поиск относительно конца файла не разрешен в текстовом режиме, предположительно потому, что содержимое файла должно быть декодировано, и, как правило, поиск произвольной позиции в последовательности закодированных байтов может иметь неопределенные результаты, когда вы попытаться декодировать в Unicode, начиная с этой позиции. Предложение, предлагаемое по ссылке, состоит в том, чтобы попытаться открыть файл в двоичном режиме и выполнить декодирование самостоятельно, перехватывая исключения DecodeError.
максимум
6
НЕ ИСПОЛЬЗУЙТЕ ЭТОТ КОД. Это портит строки в некоторых случаях в Python 2.7. Ответ от @papercrane ниже исправляет это.
xApple
88

Предполагается 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]
отметка
источник
5
Должен быть независимым от платформы. Кроме того, если вы прочитаете вопрос, вы увидите, что f - это файл-подобный объект.
Армин Ронахер
40
вопрос не говорит, что зависимость от платформы недопустима. Я не понимаю, почему это заслуживает двух отрицательных ответов, когда это обеспечивает очень необычный (может быть, то, что вы ищете ... конечно, для меня) способ сделать именно то, что задает вопрос.
Шаббироб
3
Спасибо, я думал, что должен был решить эту проблему на чистом Python, но нет причин не использовать утилиты UNIX, когда они под рукой, поэтому я пошел с этим. FWIW в современном Python, subprocess.check_output, вероятно, предпочтительнее, чем os.popen2; это немного упрощает вещи, так как он просто возвращает вывод в виде строки и вызывает ненулевой код выхода.
mrooney
3
Хотя это зависит от платформы, это очень эффективный способ выполнения заданного, а также чрезвычайно быстрый способ выполнения этого (вам не нужно загружать весь файл в память). @Shabbyrobe
earthmeLon
6
Возможно, вы захотите предварительно рассчитать смещение, например: offset_total = str(n+offset)и заменить эту строку, stdin,stdout = os.popen2("tail -n "+offset_total+" "+f)чтобы избежатьTypeErrors (cannot concatenate int+str)
AddingColor
32

Вот мой ответ. Чистый питон. Использование 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:]
glenbot
источник
3
Элегантное решение! Это if len(lines_found) > lines:действительно необходимо? Разве loopусловие не поймает это также?
Максимилиан Питерс
Вопрос для моего понимания: os.SEEK_ENDиспользуется просто для ясности? Насколько я нашел, его значение является постоянным (= 2). Я задавался вопросом о том, чтобы оставить это, чтобы иметь возможность оставить вне import os. Спасибо за отличное решение!
n1k31t4
2
@MaximilianPeters да. Это необязательно. Я закомментировал это.
Гленбот
@DexterMorgan вы можете заменить os.SEEK_ENDего целочисленным эквивалентом. Это было главным образом для удобочитаемости.
Гленбот
1
Я проголосовал, но есть небольшая гнида. После того , как искать, первая линия считывания может быть неполным, поэтому , чтобы получить N _complete_lines я изменил , while len(lines_found) < linesчтобы while len(lines_found) <= linesв моей копии. Спасибо!
Грэм Клин
30

Если чтение всего файла приемлемо, тогда используйте 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:]
А. Коади
источник
Почему эта нижняя функция работает? pos *= 2кажется совершенно произвольным. Каково его значение?
марта
1
@ 2mac Экспоненциальный поиск . Он читает с конца файла итеративно, удваивая количество чтения каждый раз, пока не будет найдено достаточно строк.
A. Coady
Я думаю, что решение для чтения с конца не будет поддерживать файлы, закодированные с помощью UTF-8, так как длина символа является переменной, и вы можете (вероятно, попадете) с некоторым странным смещением, которое не может быть правильно интерпретировано.
Майк
к сожалению , Ваш скачет поисковое решение не работает на Python 3. Как f.seek () не принимает отрицательное смещение. Я обновил ваш код, чтобы он работал для ссылки
itsjwala
25

Ответ С.Лотта выше почти работает для меня, но в итоге дает мне частичные строки. Оказывается, это повреждает данные на границах блоков, потому что данные хранят прочитанные блоки в обратном порядке. Когда вызывается '' .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:]
papercrane
источник
1
Вставка в начале списка - плохая идея. Почему бы не использовать структуру deque?
Sergey11g
1
К сожалению, не совместим с Python 3 ... пытаясь понять, почему.
Sherlock70
20

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

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
точно не отвечает на вопрос.
Шеки
13

Простое и быстрое решение с помощью 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()
Dimitri
источник
1
Это, вероятно, самый быстрый ответ, когда ввод может быть огромным (или это было бы, если бы он использовал .rfindметод для сканирования в обратном порядке на наличие новых строк, вместо того, чтобы выполнять проверку байтов за раз на уровне Python; в CPython, заменяя код уровня Python на C встроенными звонками обычно много выигрывает). Для небольших входов, dequeс maxlenпроще и, вероятно, так же быстро.
ShadowRanger
4

Еще более чистая версия, совместимая с 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')
Хауке Реффельд
источник
Не слишком потертый - но я бы вообще посоветовал не добавлять ответ на 10-летний вопрос с большим количеством ответов. Но помогите мне: что специфично для Python 3 в вашем коде?
usr2564301
Другие ответы не были точно работает хорошо :-) PY3: см stackoverflow.com/questions/136168/...
Hauke Rehfeld
3

Обновите решение @papercrane до python3. Откройте файл с помощью open(filename, 'rb')и:

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

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

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

ShadowRanger
источник
3

Я нашел Popen выше, чтобы быть лучшим решением. Это быстро и грязно, и это работает Для 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
Marko
источник
2

основано на топовом ответе С. Лотта (25 сентября 2008 г. в 21:43), но исправлено для небольших файлов.

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

Надеюсь, это полезно.

Eyecue
источник
2

Существует несколько существующих реализаций tail на pypi, которые вы можете установить с помощью pip:

  • mtFileUtil
  • multitail
  • log4tailer
  • ...

В зависимости от вашей ситуации могут быть преимущества использования одного из этих существующих инструментов.

Трэвис Медведь
источник
Вам известен какой-нибудь модуль, работающий в Windows? Я пытался tailhead, tailerно они не работали. Также попробовал mtFileUtil. Первоначально он выдавал ошибку, потому что printоператоры не имели круглых скобок (я на Python 3.6). Я добавил их, reverse.pyи сообщения об ошибках исчезли, но когда мой скрипт вызывает модуль ( mtFileUtil.tail(open(logfile_path), 5)), он ничего не печатает.
Technext
2

Просто :

with open("test.txt") as f:
data = f.readlines()
tail = data[-2:]
print(''.join(tail)
Самба Шива Редди
источник
Это абсолютно плохая реализация. Подумайте об обработке огромных файлов, а там, где n также огромная, слишком дорогая операция
Нивеш Кришна,
1

Для повышения эффективности работы с очень большими файлами (что часто встречается в файлах журналов, где вы можете использовать tail), вы, как правило, хотите избегать чтения всего файла (даже если вы делаете это, не считывая весь файл сразу в память). Однако вы делаете нужно как-то отработать смещение в строках, а не в символах. Одной из возможностей является чтение в обратном порядке с помощью функции seek () char за char, но это очень медленно. Вместо этого лучше обрабатывать большими блоками.

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

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

[Редактировать] Добавлена ​​более конкретная версия (избегать необходимости реверса дважды)

Брайан
источник
Быстрые тесты показывают, что это работает намного хуже, чем моя версия сверху. Возможно из-за вашей буферизации.
Армин Ронахер
Я подозреваю, что это потому, что я делаю несколько поисков в обратном направлении, поэтому не так хорошо используется буфер readahead. Тем не менее, я думаю, что это может быть лучше, когда ваши предположения о длине строки не точны (например, очень большие строки), так как в этом случае вам не нужно перечитывать данные.
Брайан
1

вы можете перейти к концу вашего файла с помощью f.seek (0, 2), а затем прочитать строки одну за другой со следующей заменой 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

Основано на ответе Eyecue (10 июня 2010 в 21:28): этот класс добавляет метод head () и tail () к объекту файла.

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

Использование:

f = File('path/to/file', 'r')
f.head(3)
f.tail(3)
FDB
источник
1

Некоторые из этих решений имеют проблемы, если файл не заканчивается на \ n или гарантирует, что вся первая строка будет прочитана.

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

Вот довольно простая реализация:

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()
GL2014
источник
Отличный пример! Не могли бы вы объяснить использование попробовать до f.seek? Почему не до того with open? Кроме того, почему exceptвы делаете f.readlines()?
Честно говоря, попытка, вероятно, должна идти в первую очередь ... Я не помню, чтобы у меня была причина не перехватывать open (), кроме как в здоровой стандартной системе Linux, / etc / passwd всегда должен быть читабельным. попробуй, тогда с это более общий порядок.
GL2014
1

Есть очень полезный модуль, который может сделать это:

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)
Quinten Cabo
источник
1

Другое решение

если ваш текстовый файл выглядит так: мышь змея кошка ящерица волк собака

вы можете перевернуть этот файл, просто используя индексирование массива в 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)

результат: собака волк ящерица кот

Блейн МакМахон
источник
1

Самый простой способ - использовать deque:

from collections import deque

def tail(filename, n=10):
    with open(filename) as f:
        return deque(f, n)
Чжэнь ван
источник
0

Мне пришлось прочитать определенное значение из последней строки файла, и наткнулся на этот поток. Вместо того, чтобы заново изобретать колесо в Python, я закончил крошечным сценарием оболочки, сохраненным как / usr / local / bin / get_last_netp:

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

И в программе Python:

from subprocess import check_output

last_netp = int(check_output("/usr/local/bin/get_last_netp"))
Leifbk
источник
0

Не первый пример использования deque, но более простой. Это общее правило: оно работает с любым повторяемым объектом, а не только с файлом.

#!/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
moylop260
источник
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]))
Y Kal
источник
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

Обновление для ответа, данного A.Coady

Работает с питоном 3 .

Это использует Экспоненциальный поиск и будет буферизовать только Nстроки сзади и очень эффективно.

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))
itsjwala
источник
-1

Если подумать, это, вероятно, так же быстро, как и все здесь.

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]

Это намного проще. И это, кажется, рвется в хорошем темпе.

С. Лотт
источник
Потому что почти все здесь не работает с файлами журналов, размер которых превышает 30 МБ, без загрузки такого же объема памяти в ОЗУ;) Ваша первая версия намного лучше, но для тестовых файлов здесь она работает немного хуже, чем у меня и это не работает с разными символами новой строки.
Армин Ронахер
3
Я ошибался. Версия 1 взяла 0,00248908996582 за 10 хвостов через словарь. Версия 2 взяла 1.2963051796 за 10 хвостов через словарь. Я бы почти проголосовал за себя.
С.Лотт
"не работает с разными символами новой строки." Замените datacount ('\ n') на len (data.splitlines ()), если это важно.
С.Лотт