J'ai besoin d'extraire la dernière ligne d'un certain nombre de très gros fichiers texte (plusieurs centaines de mégaoctets) pour obtenir certaines données. Actuellement, j'utilise python pour parcourir toutes les lignes jusqu'à ce que le fichier soit vide, puis je traite la dernière ligne renvoyée, mais je suis certain qu'il existe un moyen plus efficace de le faire.
Quelle est la meilleure façon de récupérer uniquement la dernière ligne d'un fichier texte à l'aide de python?
Pas de manière simple, mais probablement beaucoup plus rapide qu'une simple implémentation Python:
line = subprocess.check_output(['tail', '-1', filename])
with open('output.txt', 'r') as f:
lines = f.read().splitlines()
last_line = lines[-1]
print last_line
Utilisez la méthode seek
du fichier avec un décalage négatif et whence=os.SEEK_END
pour lire un bloc à la fin du fichier. Cherchez dans ce bloc le ou les derniers caractères de fin de ligne et récupérez tous les caractères après. S'il n'y a pas de fin de ligne, sauvegardez plus loin et répétez le processus.
def last_line(in_file, block_size=1024, ignore_ending_newline=False):
suffix = ""
in_file.seek(0, os.SEEK_END)
in_file_length = in_file.tell()
seek_offset = 0
while(-seek_offset < in_file_length):
# Read from end.
seek_offset -= block_size
if -seek_offset > in_file_length:
# Limit if we ran out of file (can't seek backward from start).
block_size -= -seek_offset - in_file_length
if block_size == 0:
break
seek_offset = -in_file_length
in_file.seek(seek_offset, os.SEEK_END)
buf = in_file.read(block_size)
# Search for line end.
if ignore_ending_newline and seek_offset == -block_size and buf[-1] == '\n':
buf = buf[:-1]
pos = buf.rfind('\n')
if pos != -1:
# Found line end.
return buf[pos+1:] + suffix
suffix = buf + suffix
# One-line file.
return suffix
Notez que cela ne fonctionnera pas sur les choses qui ne prennent pas en charge seek
, comme stdin ou sockets. Dans ces cas, vous êtes bloqué en lisant le tout (comme le fait la commande tail
).
Si vous connaissez la longueur maximale d'une ligne, vous pouvez le faire
def getLastLine(fname, maxLineLength=80):
fp=file(fname, "rb")
fp.seek(-maxLineLength-1, 2) # 2 means "from the end of the file"
return fp.readlines()[-1]
Cela fonctionne sur ma machine Windows. Mais je ne sais pas ce qui se passe sur d'autres plateformes si vous ouvrez un fichier texte en mode binaire. Le mode binaire est nécessaire si vous souhaitez utiliser seek ().
Cherchez à la fin du fichier moins 100 octets environ. Faites une lecture et recherchez une nouvelle ligne. S'il n'y a pas de nouvelle ligne, recherchez à nouveau environ 100 octets. Faire mousser, rincer, répéter. Finalement, vous trouverez une nouvelle ligne. La dernière ligne commence immédiatement après cette nouvelle ligne.
Dans le meilleur des cas, vous ne faites qu'une seule lecture de 100 octets.
Si vous pouvez choisir une longueur de ligne maximale raisonnable, vous pouvez rechercher jusqu'à la fin du fichier avant de commencer la lecture.
myfile.seek(-max_line_length, os.SEEK_END)
line = myfile.readlines()[-1]
L'inefficacité ici n'est pas vraiment due à Python, mais à la nature de la lecture des fichiers. La seule façon de trouver la dernière ligne est de lire le fichier et de trouver les fins de ligne. Cependant, l'opération de recherche peut être utilisée pour ignorer n'importe quel décalage d'octet dans le fichier. Vous pouvez donc commencer très près de la fin du fichier, et récupérer des morceaux de plus en plus gros selon les besoins jusqu'à ce que la dernière ligne se termine:
from os import SEEK_END
def get_last_line(file):
CHUNK_SIZE = 1024 # Would be good to make this the chunk size of the filesystem
last_line = ""
while True:
# We grab chunks from the end of the file towards the beginning until we
# get a new line
file.seek(-len(last_line) - CHUNK_SIZE, SEEK_END)
chunk = file.read(CHUNK_SIZE)
if not chunk:
# The whole file is one big line
return last_line
if not last_line and chunk.endswith('\n'):
# Ignore the trailing newline at the end of the file (but include it
# in the output).
last_line = '\n'
chunk = chunk[:-1]
nl_pos = chunk.rfind('\n')
# What's being searched for will have to be modified if you are searching
# files with non-unix line endings.
last_line = chunk[nl_pos + 1:] + last_line
if nl_pos == -1:
# The whole chunk is part of the last line.
continue
return last_line
Voici une solution légèrement différente. Au lieu de plusieurs lignes, je me suis concentré uniquement sur la dernière ligne et au lieu d'une taille de bloc constante, j'ai une taille de bloc dynamique (doublée). Voir les commentaires pour plus d'informations.
# Get last line of a text file using seek method. Works with non-constant block size.
# IDK if that speed things up, but it's good enough for us,
# especially with constant line lengths in the file (provided by len_guess),
# in which case the block size doubling is not performed much if at all. Currently,
# we're using this on a textfile format with constant line lengths.
# Requires that the file is opened up in binary mode. No nonzero end-rel seeks in text mode.
REL_FILE_END = 2
def lastTextFileLine(file, len_guess=1):
file.seek(-1, REL_FILE_END) # 1 => go back to position 0; -1 => 1 char back from end of file
text = file.read(1)
tot_sz = 1 # store total size so we know where to seek to next rel file end
if text != b'\n': # if newline is the last character, we want the text right before it
file.seek(0, REL_FILE_END) # else, consider the text all the way at the end (after last newline)
tot_sz = 0
blocks = [] # For storing succesive search blocks, so that we don't end up searching in the already searched
j = file.tell() # j = end pos
not_done = True
block_sz = len_guess
while not_done:
if j < block_sz: # in case our block doubling takes us past the start of the file (here j also = length of file remainder)
block_sz = j
not_done = False
tot_sz += block_sz
file.seek(-tot_sz, REL_FILE_END) # Yes, seek() works with negative numbers for seeking backward from file end
text = file.read(block_sz)
i = text.rfind(b'\n')
if i != -1:
text = text[i+1:].join(reversed(blocks))
return str(text)
else:
blocks.append(text)
block_sz <<= 1 # double block size (converge with open ended binary search-like strategy)
j = j - block_sz # if this doesn't work, try using tmp j1 = file.tell() above
return str(b''.join(reversed(blocks))) # if newline was never found, return everything read
Idéalement, vous devriez envelopper cela dans une classe LastTextFileLine et garder une trace d'une moyenne mobile des longueurs de ligne. Cela vous donnerait peut-être une bonne len_guess.
lines = file.readlines()
fileHandle.close()
last_line = lines[-1]
Pourriez-vous charger le fichier dans un mmap , puis utiliser mmap.rfind (chaîne [ début [ fin]]) pour trouver l'avant-dernier caractère EOL du fichier? Une recherche à ce point dans le fichier devrait vous diriger vers la dernière ligne, je pense.