web-dev-qa-db-fra.com

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

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?

28
TimothyAWiseman

Pas de manière simple, mais probablement beaucoup plus rapide qu'une simple implémentation Python:

line = subprocess.check_output(['tail', '-1', filename])
32
sth
with open('output.txt', 'r') as f:
    lines = f.read().splitlines()
    last_line = lines[-1]
    print last_line
24
mick barry

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).

7
Mike DeSimone

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 ().

5
rocksportrocker

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.

5
Bryan Oakley

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]
3
Mark Ransom

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
1
Zack Bloom

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.

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

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.

0
ChrisC