J'écris une visionneuse de fichier journal pour une application Web et je souhaite pour cela paginer entre les lignes du fichier journal. Les éléments du fichier sont basés sur les lignes, l’article le plus récent se trouvant en bas.
Il me faut donc une méthode tail()
capable de lire les lignes n
à partir du bas et prenant en charge un décalage. Ce que je suis venu avec ressemble à ceci:
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
Est-ce une approche raisonnable? Quelle est la méthode recommandée pour réduire les fichiers journaux avec des décalages?
Le code que j'ai fini par utiliser. Je pense que c'est le meilleur jusqu'à présent:
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
Cela peut être plus rapide que le vôtre. Ne fait aucune hypothèse sur la longueur de la ligne. Parcourt le fichier bloc par bloc jusqu'à ce qu'il ait trouvé le bon nombre de caractères '\ 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:])
Je n'aime pas les hypothèses délicates sur la longueur des lignes lorsque, pour des raisons pratiques, vous ne pouvez jamais savoir de telles choses.
Généralement, cela localisera les 20 dernières lignes du premier ou du deuxième passage dans la boucle. Si votre position de 74 caractères est réellement exacte, vous créez une taille de bloc de 2048 et vous réduisez presque immédiatement 20 lignes.
De plus, je ne brûle pas beaucoup de calories dans le cerveau en essayant de mettre fin à l'alignement avec les blocs physiques du système d'exploitation. Avec ces packages d'E/S de haut niveau, je doute que vous constatiez une conséquence sur les performances si vous essayez d'aligner les limites des blocs du système d'exploitation. Si vous utilisez des E/S de niveau inférieur, vous constaterez peut-être une accélération.
Suppose un système de type unix sur Python 2, vous pouvez faire:
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]
Pour Python 3, vous pouvez faire:
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]
Si la lecture de tout le fichier est acceptable, utilisez un deque.
from collections import deque
deque(f, maxlen=n)
Avant la version 2.6, deques n’avait pas d’option maxlen, mais elle est assez facile à mettre en œuvre.
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
S'il est nécessaire de lire le fichier à la fin, utilisez une recherche au galop (exponentielle a.k.a).
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:]
La réponse de S.Lott ci-dessus fonctionne presque pour moi mais finit par me donner des lignes partielles. Il s'avère que cela corrompt les données sur les limites des blocs, car les données sont stockées dans l'ordre inverse Lorsque ".join (data) est appelé, les blocs sont dans le mauvais ordre. Cela corrige ça.
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:]
Voici ma réponse. Pur python. En utilisant timeit, cela semble assez rapide. Tail 100 lignes d'un fichier journal de 100 000 lignes:
>>> 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
Voici le code:
import os
def tail(f, lines=1, _buffer=4098):
"""Tail a file and get X lines from the end"""
# place holder for the lines found
lines_found = []
# block counter will be multiplied by buffer
# to get the block size from the end
block_counter = -1
# loop until we find X lines
while len(lines_found) < lines:
try:
f.seek(block_counter * _buffer, os.SEEK_END)
except IOError: # either file is too small, or too many lines requested
f.seek(0)
lines_found = f.readlines()
break
lines_found = f.readlines()
# we found enough lines, get out
# Removed this line because it was redundant the while will catch
# it, I left it for history
# if len(lines_found) > lines:
# break
# decrement the block counter to get the
# next X bytes
block_counter -= 1
return lines_found[-lines:]
Solution simple et rapide avec 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()
Une version compatible python3 encore plus propre qui n'insère pas mais ajoute et inverse:
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:])
utilisez-le comme ceci:
with open(path, 'rb') as f:
last_lines = tail(f, 3).decode('utf-8')
Publier une réponse à la demande des commentateurs sur ma réponse à une question similaire où la même technique a été utilisée pour muter la dernière ligne d'un fichier, pas seulement pour l'obtenir.
Pour un fichier de taille importante, mmap
est le meilleur moyen de procéder. Pour améliorer la réponse mmap
existante, cette version est portable entre Windows et Linux et devrait fonctionner plus rapidement (bien qu'elle ne fonctionne pas sans quelques modifications sur Python 32 bits avec des fichiers de la plage de Go, voir la réponse autre pour obtenir des conseils sur le traitement de cela, et pour modifier pour travailler sur 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)
Cela suppose que le nombre de lignes en queue est suffisamment petit pour pouvoir les lire toutes en mémoire en une fois; vous pouvez également en faire une fonction génératrice et lire manuellement une ligne à la fois en remplaçant la dernière ligne par:
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)
Enfin, cette lecture se fait en mode binaire (il est nécessaire d’utiliser mmap
) pour donner les lignes str
(Py2) et les lignes bytes
(Py3); si vous voulez unicode
(Py2) ou str
(Py3), l'approche itérative peut être modifiée pour la décodage et/ou la correction des nouvelles lignes:
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
Remarque: j'ai tout saisi sur une machine sur laquelle je n'ai pas accès à Python à tester. S'il vous plaît laissez-moi savoir si j'ai tapé quelque chose; cela était assez similaire à mon autre réponse que je pense cela devrait fonctionner, mais les ajustements (par exemple, manipuler une offset
) pourraient conduire à des erreurs subtiles. S'il vous plaît laissez-moi savoir dans les commentaires s'il y a des erreurs.
J'ai trouvé le Popen ci-dessus pour être la meilleure solution. C’est rapide, sale et ça marche Pour Python 2.6 sur Unix, j’utilisais ce qui suit:
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 aura contiendra les n dernières lignes du code. pour parcourir ligne par ligne soutput faire:
for line in GetLastNLines(50,'myfile.log').split('\n'):
print line
Mettez à jour la solution @papercrane vers python3 . Ouvrez le fichier avec open(filename, 'rb')
et:
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:]
Voici une implémentation assez simple:
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()
d'après la réponse la plus votée de S.Lott (25 septembre 2008 à 21:43), mais corrigé pour les petits fichiers.
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
J'espère que c'est utile.
Il existe certaines implémentations de tail on pypi que vous pouvez installer avec pip:
Selon votre situation, l’utilisation de l’un de ces outils existants peut présenter des avantages.
vous pouvez aller à la fin de votre fichier avec f.seek (0, 2), puis lire les lignes une par une avec le remplacement suivant pour 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
Pour être efficace avec des fichiers très volumineux (ce qui est courant dans les situations de fichier journal où vous voudrez peut-être utiliser tail), vous voudrez généralement éviter de lire le fichier entier (même si vous le faites sans lire le fichier entier en mémoire en une fois). besoin de trouver en quelque sorte le décalage en lignes plutôt qu'en caractères. Une possibilité est de lire à l'envers avec seek () char par char, mais c'est très lent. Au lieu de cela, il est préférable de traiter dans des blocs plus grands.
J'ai écrit il y a quelque temps une fonction utilitaire pour lire les fichiers à l'envers que vous pouvez utiliser ici.
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
[Modifier] Ajout d'une version plus spécifique (évite de devoir inverser deux fois)
Simple:
with open("test.txt") as f:
data = f.readlines()
tail = data[-2:]
print(''.join(tail)
Basé sur la réponse de Eyecue (10 juin 10 à 21:28): cette classe ajoute les méthodes head () et tail () pour archiver un objet.
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:]
Usage:
f = File('path/to/file', 'r')
f.head(3)
f.tail(3)
Plusieurs de ces solutions ont des problèmes si le fichier ne se termine pas par\n ou si la première ligne complète est lue.
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
Mise à jour pour la réponse donnée par A.Coady
Fonctionne avec python 3 .
Ceci utilise recherche exponentielle et ne met en mémoire tampon que N
lignes à partir du dos et est très efficace.
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))
Pas le premier exemple utilisant un deque, mais un plus simple. Celui-ci est général: il fonctionne sur n'importe quel objet itérable, pas seulement un fichier.
#!/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 __== '__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
Il est très utile module qui peut faire ceci:
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)
J'ai dû lire une valeur spécifique de la dernière ligne d'un fichier et suis tombé sur ce fil. Plutôt que de réinventer la roue en Python, je me suis retrouvé avec un petit script Shell, enregistré sous le nom de.
#! /bin/bash
tail -n1 /home/leif/projects/transfer/export.log | awk {'print $14'}
Et dans le programme Python:
from subprocess import check_output
last_netp = int(check_output("/usr/local/bin/get_last_netp"))
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]))
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
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)