47 votes

Trouver efficacement la dernière ligne d'un fichier texte

J'ai besoin d'extraire la dernière ligne d'un certain nombre de fichiers texte très volumineux (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 d'y parvenir.

Quelle est la meilleure façon de récupérer la dernière ligne d'un fichier texte en utilisant Python ?

5voto

Bryan Oakley Points 63365

Rechercher la fin du fichier moins 100 octets environ. Effectuez une lecture et recherchez une nouvelle ligne. S'il n'y a pas de nouvelle ligne, recherchez une centaine d'octets supplémentaires. Répéter, rincer, répéter. Vous finirez par trouver une nouvelle ligne. La dernière ligne commence immédiatement après cette nouvelle ligne.

Dans le meilleur des cas, vous n'effectuez qu'une lecture de 100 octets.

2voto

Zack Bloom Points 4515

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 seek peut être utilisée pour passer à n'importe quel décalage d'octet dans le fichier. Vous pouvez donc commencer très près de la fin du fichier et prendre des morceaux de plus en plus grands jusqu'à ce que la dernière fin de ligne soit trouvée :

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

1voto

Voici une solution légèrement différente. Au lieu d'utiliser plusieurs lignes, je me suis concentré sur la dernière ligne, et au lieu d'avoir une taille de bloc constante, j'ai une taille de bloc dynamique (qui double). 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 intégrer cette fonction dans la classe LastTextFileLine et suivre une moyenne mobile de la longueur des lignes. Cela vous donnerait une bonne idée de len_guess.

0voto

ChrisC Points 1026

Pourriez-vous charger le fichier dans un mmap puis utiliser mmap.rfind(string[, start[, end]]) pour trouver l'avant-dernier caractère EOL du fichier ? Une recherche à ce point du fichier devrait vous amener à la dernière ligne, je pense.

-2voto

Jon Martin Points 1192
lines = file.readlines()
fileHandle.close()
last_line = lines[-1]

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X