Получить последние n строк файла с Python, похожий на хвост
Я пишу просмотрщик файлов журнала для веб-приложения, и для этого я хочу разбить на страницы по строкам файла журнала. Элементы в файле основаны на строке с самым новым элементом внизу.
Так что мне нужен 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
это разумный подход? Каков рекомендуемый способ хвостовых файлов журнала со смещениями?
28 ответов:
Это может быть быстрее, чем ваши. Не делает никаких предположений о длине линии. Резервное копирование через файл по одному блоку за раз, пока не будет найдено нужное количество символов '\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 линий почти сразу.
кроме того, я не сжигаю много мозговых калорий, пытаясь утончить выравнивание с физическими блоками ОС. Используя эти высокоуровневые пакеты ввода-вывода, я сомневаюсь, что вы увидите какие-либо последствия для производительности при попытке выровнять границы блока ОС. Если вы используете низкоуровневый ввод-вывод, то вы можете увидеть ускорение.
предполагает 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]
Если чтение всего файла приемлемо, то используйте deque.
from collections import deque deque(f, maxlen=n)
до 2.6, двухсторонних очередей не было, аналогичная опция, но это достаточно легко реализовать.
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:]
ответ С. Лотта выше почти работает для меня, но в конечном итоге дает мне частичные строки. Оказывается, что он повреждает данные на границах блоков, потому что данные содержат считанные блоки в обратном порядке. Когда.'' соединение (данные) называется, блоки находятся в неправильном порядке. Это исправляет это.
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) 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:]
вот мой ответ. Чистый питон. С помощью 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:]
код, который я в конечном итоге использовал. Я думаю, что это лучший до сих пор:
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
простое и быстрое решение с помощью 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()
Я нашел Попен выше, чтобы быть лучшим решением. Это быстро и грязно, и это работает Для 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
еще более чистая 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')
размещение ответа по просьбе комментаторов на мой ответ на подобный вопрос где тот же метод был использован для мутации последней строки файла, а не просто получить его.
для файла значительного размера,
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
) может привести к тонким ошибкам. Пожалуйста, дайте мне знать в комментариях, если есть какие-то ошибки.
на основе ответа S. Lott's top voted (Sep 25 ' 08 at 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
надеюсь, что это полезно.
есть некоторые существующие реализации хвоста на pypi, которые вы можете установить с помощью pip:
- mtFileUtil
- multitail
- log4tailer
- ...
в зависимости от вашей ситуации, могут быть преимущества использования одного из этих существующих инструментов.
вот довольно простая реализация:
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()
для эффективности работы с очень большими файлами (распространенными в ситуациях с лог-файлами, когда вы можете использовать tail), вы обычно хотите избежать чтения всего файла (даже если вы делаете это без чтения всего файла в память сразу), однако вам нужно как-то выработать смещение в строках, а не в символах. Одна из возможностей-чтение назад с помощью seek () char by 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
[Edit] добавлена более конкретная версия (избегает необходимости обратного дважды)
вы можете перейти к концу файла с помощью 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
на основе ответа Eyecue (Jun 10 '10 at 21:28): этот класс добавляет метод head() и tail() в объект file.
class File(file): def head(self, lines_2find=1): self.seek(0) #Rewind file return [self.next() for x in xrange(lines_2find)] def tail(self, lines_2find=1): self.seek(0, 2) #go to end of file bytes_in_file = self.tell() lines_found, total_bytes_scanned = 0, 0 while (lines_2find+1 > lines_found and bytes_in_file > total_bytes_scanned): byte_block = min(1024, bytes_in_file-total_bytes_scanned) self.seek(-(byte_block+total_bytes_scanned), 2) total_bytes_scanned += byte_block lines_found += self.read(1024).count('\n') self.seek(-total_bytes_scanned, 2) line_list = list(self.readlines()) return line_list[-lines_2find:]
использование:
f = File('path/to/file', 'r') f.head(3) f.tail(3)
некоторые из этих решений имеют проблемы, если файл не заканчивается в \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
обновить решение @ 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:]
Я должен был прочитать конкретное значение из последней строки файла, и наткнулся на эту ветку. Вместо того, чтобы изобретать колесо в Python, я получил крошечный скрипт оболочки, сохраненный как /usr / local/bin / get_last_netp:
#! /bin/bash tail -n1 /home/leif/projects/transfer/export.log | awk {'print '}
и в программе Python:
from subprocess import check_output last_netp = int(check_output("/usr/local/bin/get_last_netp"))
не первый пример использования 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)
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
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
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]))
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)
Я нашел наверное самый простой способ найти первые или последние n строк из файла
последние N строк файла (например:N=10)
file=open("xyz.txt",'r") liner=file.readlines() for ran in range((len(liner)-N),len(liner)): print liner[ran]
первые N строк файла (например:N=10)
file=open("xyz.txt",'r") liner=file.readlines() for ran in range(0,N+1): print liner[ran]
С другой стороны, это, вероятно, так же быстро, как и все здесь.
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]
Это намного проще. И он, кажется, рвется вперед в хорошем темпе.
это так просто:
def tail(fname,nl): with open(fname) as f: data=f.readlines() #readlines return a list print(''.join(data[-nl:]))
хотя это на самом деле не на эффективной стороне с большими файлами, этот код довольно прямолинейный:
- он читает объект file,
f
.- он разбивает строку, возвращенную с помощью новых строк,
\n
.он получает массив списков последних индексов, используя отрицательный знак для обозначения последних индексов, и
:
чтобы получить подмассив.def tail(f,n): return "\n".join(f.read().split("\n")[-n:])