J'ai un fichier et je ne sais pas quelle sera sa taille (il pourrait être assez volumineux, mais sa taille varie énormément). Je veux rechercher les 10 dernières lignes ou plus pour voir si l'une d'elles correspond à une chaîne. Je dois le faire aussi rapidement et efficacement que possible et je me demandais s'il y avait quelque chose de mieux que:
s = "foo"
last_bit = fileObj.readlines()[-10:]
for line in last_bit:
if line == s:
print "FOUND"
# Tail
from __future__ import with_statement
find_str = "FIREFOX" # String to find
fname = "g:/autoIt/ActiveWin.log_2" # File to check
with open(fname, "r") as f:
f.seek (0, 2) # Seek @ EOF
fsize = f.tell() # Get Size
f.seek (max (fsize-1024, 0), 0) # Set pos @ last n chars
lines = f.readlines() # Read to end
lines = lines[-10:] # Get last 10 lines
# This returns True if any line is exactly find_str + "\n"
print find_str + "\n" in lines
# If you're searching for a substring
for line in lines:
if find_str in line:
print True
break
Voici une réponse semblable à celle de MizardX, mais sans son problème apparent de prendre du temps quadratique dans le pire des cas, en analysant à nouveau la chaîne de travail à la recherche de nouvelles lignes à mesure que des morceaux sont ajoutés.
Comparé à la solution active (qui semble également être quadratique), cela n’explose pas si un fichier est vide, et on cherche une lecture par bloc lu au lieu de deux.
Comparé à la «queue» de frai, il est autonome. (Mais 'queue' est préférable si vous en avez.)
Par rapport à la saisie de quelques ko à la fin et en espérant que cela suffira, cela fonctionne pour toute longueur de ligne.
import os
def reversed_lines(file):
"Generate the lines of file in reverse order."
part = ''
for block in reversed_blocks(file):
for c in reversed(block):
if c == '\n' and part:
yield part[::-1]
part = ''
part += c
if part: yield part[::-1]
def reversed_blocks(file, blocksize=4096):
"Generate blocks of file's contents in reverse order."
file.seek(0, os.SEEK_END)
here = file.tell()
while 0 < here:
delta = min(blocksize, here)
here -= delta
file.seek(here, os.SEEK_SET)
yield file.read(delta)
Pour l'utiliser comme demandé:
from itertools import islice
def check_last_10_lines(file, key):
for line in islice(reversed_lines(file), 10):
if line.rstrip('\n') == key:
print 'FOUND'
break
Éditer: modifié map () en itertools.imap () dans head (). Éditer 2: simplifié reverse_blocks (). Éditer 3: éviter de réanalyser la fin des nouvelles lignes. Edit 4: rewrote reverse_lines () car str.splitlines () ignore un '\ n' final, comme BrianB l’a remarqué (merci).
Notez que dans les très anciennes versions de Python, la concaténation de chaînes dans une boucle prend un temps quadratique. CPython des dernières années au moins évite automatiquement ce problème.
Si vous exécutez Python sur un système POSIX, vous pouvez utiliser 'tail -10' pour récupérer les dernières lignes. Cela peut être plus rapide que d’écrire votre propre code Python pour obtenir les 10 dernières lignes. Plutôt que d'ouvrir le fichier directement, ouvrez un tuyau à partir de la commande 'tail -10 nomfichier'. Si vous êtes cependant certain de la sortie du journal (par exemple, vous savez qu'il y a jamais de très longues lignes de plusieurs centaines ou milliers de caractères), utilisez ensuite l'une des approches 'lire les 2 derniers Ko' énumérés serait bien.
Je pense qu'en lisant les 2 derniers Ko environ du fichier, vous devriez vous assurer que vous avez 10 lignes et que cela ne devrait pas être un gros problème de ressources.
file_handle = open("somefile")
file_size = file_handle.tell()
file_handle.seek(max(file_size - 2*1024, 0))
# this will get rid of trailing newlines, unlike readlines()
last_10 = file_handle.read().splitlines()[-10:]
assert len(last_10) == 10, "Only read %d lines" % len(last_10)
Voici une version utilisant mmap
qui semble assez efficace. Le gros avantage est que mmap
gérera automatiquement les exigences de pagination de fichier à mémoire pour vous.
import os
from mmap import mmap
def lastn(filename, n):
# open the file and mmap it
f = open(filename, 'r+')
m = mmap(f.fileno(), os.path.getsize(f.name))
nlcount = 0
i = m.size() - 1
if m[i] == '\n': n += 1
while nlcount < n and i > 0:
if m[i] == '\n': nlcount += 1
i -= 1
if i > 0: i += 2
return m[i:].splitlines()
target = "target string"
print [l for l in lastn('somefile', 10) if l == target]
Je me suis heurté à ce problème, en analysant la dernière heure de gros fichiers syslog, et ai utilisé cette fonction à partir du site de recettes d’activestate ... ( http://code.activestate.com/recipes/439045/ )
!/usr/bin/env python
# -*-mode: python; coding: iso-8859-1 -*-
#
# Copyright (c) Peter Astrand <[email protected]>
import os
import string
class BackwardsReader:
"""Read a file line by line, backwards"""
BLKSIZE = 4096
def readline(self):
while 1:
newline_pos = string.rfind(self.buf, "\n")
pos = self.file.tell()
if newline_pos != -1:
# Found a newline
line = self.buf[newline_pos+1:]
self.buf = self.buf[:newline_pos]
if pos != 0 or newline_pos != 0 or self.trailing_newline:
line += "\n"
return line
else:
if pos == 0:
# Start-of-file
return ""
else:
# Need to fill buffer
toread = min(self.BLKSIZE, pos)
self.file.seek(-toread, 1)
self.buf = self.file.read(toread) + self.buf
self.file.seek(-toread, 1)
if pos - toread == 0:
self.buf = "\n" + self.buf
def __init__(self, file):
self.file = file
self.buf = ""
self.file.seek(-1, 2)
self.trailing_newline = 0
lastchar = self.file.read(1)
if lastchar == "\n":
self.trailing_newline = 1
self.file.seek(-1, 2)
# Example usage
br = BackwardsReader(open('bar'))
while 1:
line = br.readline()
if not line:
break
print repr(line)
Cela fonctionne vraiment bien et est beaucoup plus efficace que tout ce qui ressemble à fileObj.readlines () [- 10:], ce qui permet à python de lire le fichier entier en mémoire, puis d’en couper les dix dernières lignes.
Si vous êtes sur une boîte unix, os.popen("tail -10 " + filepath).readlines()
sera probablement le moyen le plus rapide. Sinon, cela dépend de la robustesse que vous souhaitez. Les méthodes proposées jusqu'ici vont toutes tomber, d'une manière ou d'une autre. Pour des raisons de robustesse et de rapidité, dans le cas le plus courant, vous souhaiterez probablement une recherche logarithmique: utilisez file.seek pour aller à la fin du fichier moins 1000 caractères, lisez-le, vérifiez le nombre de lignes qu’il contient, puis appuyez sur EOF moins 3000 caractères, lu en 2000 caractères, compte les lignes, puis EOF moins 7000, lu en 4000 caractères, compte les lignes, etc. jusqu'à obtenir autant de lignes que nécessaire. Mais si vous savez avec certitude qu'il sera toujours exécuté sur des fichiers avec des longueurs de ligne raisonnables, vous n'en aurez peut-être pas besoin.
Vous pouvez également trouver une source d’inspiration dans code source pour la commande unix tail
.
Je pense me souvenir d’avoir adapté le code de ce billet de blog de Manu Garg lorsque je devais faire quelque chose de similaire.
J'ai suivi la suggestion de mhawke d'utiliser mmap
et ai écrit une version qui utilise rfind
:
from mmap import mmap
import sys
def reverse_file(f):
mm = mmap(f.fileno(), 0)
nl = mm.size() - 1
prev_nl = mm.size()
while nl > -1:
nl = mm.rfind('\n', 0, nl)
yield mm[nl + 1:prev_nl]
prev_nl = nl + 1
def main():
# Example usage
with open('test.txt', 'r+') as infile:
for line in reverse_file(infile):
sys.stdout.write(line)
Vous pouvez également compter les lignes au fur et à mesure que vous parcourez le fichier, au lieu de deviner un décalage d'octet.
lines = 0
chunk_size = 1024
f = file('filename')
f.seek(0, 2)
f.seek(f.tell() - chunk_size)
while True:
s = f.read(chunk_size)
lines += s.count('\n')
if lines > NUM_OF_LINES:
break
f.seek(f.tell() - chunk_size*2)
Le fichier est maintenant dans une bonne position pour exécuter readlines()
. Vous pouvez également mettre en cache les chaînes que vous avez lues pour la première fois, afin d'éviter de lire deux fois la même partie du fichier.
Vous pouvez lire des fragments d'environ 1 000 octets à partir de la fin du fichier dans un tampon jusqu'à 10 lignes.
Tout d'abord, une fonction qui retourne une liste:
def lastNLines(file, N=10, chunksize=1024):
lines = None
file.seek(0,2) # go to eof
size = file.tell()
for pos in xrange(chunksize,size-1,chunksize):
# read a chunk
file.seek(pos,2)
chunk = file.read(chunksize)
if lines is None:
# first time
lines = chunk.splitlines()
else:
# other times, update the 'first' line with
# the new data, and re-split
lines[0:1] = (chunk + lines[0]).splitlines()
if len(lines) > N:
return lines[-N:]
file.seek(0)
chunk = file.read(size-pos)
lines[0:1] = (chunk + lines[0]).splitlines()
return lines[-N:]
Deuxièmement, une fonction qui parcourt les lignes dans l’ordre inverse:
def iter_lines_reversed(file, chunksize=1024):
file.seek(0,2)
size = file.tell()
last_line = ""
for pos in xrange(chunksize,size-1,chunksize):
# read a chunk
file.seek(pos,2)
chunk = file.read(chunksize) + last_line
# split into lines
lines = chunk.splitlines()
last_line = lines[0]
# iterate in reverse order
for index,line in enumerate(reversed(lines)):
if index > 0:
yield line
# handle the remaining data at the beginning of the file
file.seek(0)
chunk = file.read(size-pos) + last_line
lines = chunk.splitlines()
for line in reversed(lines):
yield line
Pour votre exemple:
s = "foo"
for index, line in enumerate(iter_lines_reversed(fileObj)):
if line == s:
print "FOUND"
break
Elif index+1 >= 10:
break
Edit: Maintenant récupère automatiquement la taille du fichier
Edit2: Maintenant, itère seulement pour 10 lignes.
Cette solution ne lit le fichier qu'une seule fois, mais en utilisant 2 pointeurs d'objet fichier pour pouvoir obtenir les N dernières lignes du fichier sans le relire:
def getLastLines (path, n):
# return the las N lines from the file indicated in path
fp = open(path)
for i in range(n):
line = fp.readline()
if line == '':
return []
back = open(path)
for each in fp:
back.readline()
result = []
for line in back:
result.append(line[:-1])
return result
s = "foo"
last_bit = getLastLines(r'C:\Documents and Settings\ricardo.m.reyes\My Documents\desarrollo\tail.py', 10)
for line in last_bit:
if line == s:
print "FOUND"
lit les derniers K du fichier et scinde cela en lignes pour ne renvoyer que les 10 derniers.
il est très peu probable que le début de cette partie tombe sur une limite de ligne, mais vous échapperez quand même les premières lignes.
Merci à la solution de 18 Darius Bacon, mais avec une implémentation et une intégration 30% plus rapides dans la classe io.BaseIO.
class ReverseFile(io.IOBase):
def __init__ (self, filename, headers=1):
self.fp = open(filename)
self.headers = headers
self.reverse = self.reversed_lines()
self.end_position = -1
self.current_position = -1
def readline(self, size=-1):
if self.headers > 0:
self.headers -= 1
raw = self.fp.readline(size)
self.end_position = self.fp.tell()
return raw
raw = next(self.reverse)
if self.current_position > self.end_position:
return raw
raise StopIteration
def reversed_lines(self):
"""Generate the lines of file in reverse order.
"""
part = ''
for block in self.reversed_blocks():
block = block + part
block = block.split('\n')
block.reverse()
part = block.pop()
if block[0] == '':
block.pop(0)
for line in block:
yield line + '\n'
if part:
yield part
def reversed_blocks(self, blocksize=0xFFFF):
"Generate blocks of file's contents in reverse order."
file = self.fp
file.seek(0, os.SEEK_END)
here = file.tell()
while 0 < here:
delta = min(blocksize, here)
here -= delta
file.seek(here, os.SEEK_SET)
self.current_position = file.tell()
yield file.read(delta)
Un exemple
rev = ReverseFile(filename)
for i, line in enumerate(rev):
print("{0}: {1}".format(i, line.strip()))
Peut-être que cela pourrait être utile:
import os.path
path = 'path_to_file'
os.system('tail -n1 ' + path)
Personnellement, je serais tenté de sortir du shell et d'appeler tail -n10 pour charger le fichier. Mais alors je ne suis pas vraiment un programmeur Python;)