202 votes

Obtenir les n dernières lignes d'un fichier, similaire à tail.

Je suis en train d'écrire un visualisateur de fichier journal pour une application web et pour cela je veux paginer à travers les lignes du fichier journal. Les éléments dans le fichier sont basés sur les lignes avec l'élément le plus récent en bas.

J'ai donc besoin d'un tail() qui peut lire n lignes à partir du bas et soutenir un décalage. C'est le chapeau que j'ai trouvé :

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

S'agit-il d'une approche raisonnable ? Quelle est la méthode recommandée pour mettre en queue les fichiers journaux avec des décalages ?

0 votes

Sur mon système (linux SLES 10), la recherche relative à la fin soulève une IOError "can't do nonzero end-relative seeks". J'aime cette solution mais je l'ai modifiée pour obtenir la longueur du fichier ( seek(0,2) puis tell() ), et utiliser cette valeur pour chercher par rapport au début.

3 votes

Félicitations - cette question a été intégrée au code source de Kippo.

0 votes

Les paramètres de l open utilisée pour générer le f doit être spécifié, car selon que f=open(..., 'rb') o f=open(..., 'rt') el f doivent être traitées différemment

131voto

S.Lott Points 207588

Cela peut être plus rapide que le vôtre. Il ne fait aucune supposition sur la longueur des lignes. Il revient en arrière dans le fichier, un bloc à la fois, jusqu'à ce qu'il ait trouvé le bon nombre de ' \n des personnages.

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, alors que, dans la pratique, on ne peut jamais savoir ce genre de choses.

En général, cela permet de localiser les 20 dernières lignes lors du premier ou du deuxième passage dans la boucle. Si votre truc des 74 caractères est vraiment précis, vous faites la taille de bloc 2048 et vous aurez 20 lignes de queue presque immédiatement.

De plus, je ne brûle pas beaucoup de calories cérébrales en essayant d'affiner l'alignement avec des blocs OS physiques. En utilisant ces paquets d'E/S de haut niveau, je doute que vous puissiez constater une quelconque conséquence sur les performances en essayant de vous aligner sur les limites des blocs du système d'exploitation. Si vous utilisez des E/S de plus bas niveau, vous pourriez voir un gain de vitesse.


UPDATE

pour Python 3.2 et plus, suivez le processus sur les octets comme dans les fichiers texte In (ceux qui sont ouverts sans l'icône "b" dans la chaîne mode), seules les recherches relatives au début du fichier sont autorisées (l'exception étant la recherche jusqu'à la toute fin du fichier avec seek(0, 2)) :

eg : f = open('C:/.../../apache_logs.txt', 'rb')

 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 = []
    while lines_to_go > 0 and block_end_byte > 0:
        if (block_end_byte - BLOCK_SIZE > 0):
            f.seek(block_number*BLOCK_SIZE, 2)
            blocks.append(f.read(BLOCK_SIZE))
        else:
            f.seek(0,0)
            blocks.append(f.read(block_end_byte))
        lines_found = blocks[-1].count(b'\n')
        lines_to_go -= lines_found
        block_end_byte -= BLOCK_SIZE
        block_number -= 1
    all_read_text = b''.join(reversed(blocks))
    return b'\n'.join(all_read_text.splitlines()[-total_lines_wanted:])

0 votes

Joli. Au moins une personne qui a lu la question et le code qui s'y trouve :)

0 votes

Cela fonctionne très bien. Je viens de l'intégrer dans un script pour lire les 4000 dernières lignes d'un fichier journal avant de les analyser. Cela fonctionne rapidement et a du sens. Merci !

15 votes

Cela échoue sur les petits fichiers journaux -- IOError : invalid argument -- f.seek( block*1024, 2 )

99voto

Mark Points 33086

En supposant 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 le 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]

9 votes

Devrait être indépendant de la plateforme. De plus, si vous lisez la question, vous verrez que f est un objet de type fichier.

50 votes

La question ne dit pas que la dépendance à la plateforme est inacceptable. je ne vois pas pourquoi cela mérite deux downvotes alors que cela fournit un moyen très unixy (peut-être ce que vous recherchez... en tout cas pour moi) de faire exactement ce que la question demande.

3 votes

Merci, je pensais que je devais résoudre ce problème en Python pur, mais il n'y a aucune raison de ne pas utiliser les utilitaires UNIX quand ils sont à portée de main, alors j'ai choisi cette solution. Pour info, dans le Python moderne, subprocess.check_output est probablement préférable à os.popen2 ; il simplifie un peu les choses car il renvoie simplement la sortie sous forme de chaîne de caractères, et se lève sur un code de sortie non nul.

36voto

Coady Points 11374

Si la lecture de l'ensemble du fichier est acceptable, utilisez un deque.

from collections import deque
deque(f, maxlen=n)

Avant la 2.6, les deques n'avaient pas d'option maxlen, mais il est assez facile de l'implémenter.

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 depuis la fin, utilisez une recherche galopante (aussi appelée exponentielle).

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:]

0 votes

Pourquoi cette fonction de fond fonctionne-t-elle ? pos *= 2 semble complètement arbitraire. Quelle est sa signification ?

2 votes

@2mac Recherche exponentielle . Il lit la fin du fichier de manière itérative, en doublant la quantité lue à chaque fois, jusqu'à ce que suffisamment de lignes soient trouvées.

0 votes

Je pense que la solution consistant à lire à partir de la fin ne supportera pas les fichiers encodés en UTF-8, puisque la longueur des caractères est variable, et que vous pourriez (et vous le ferez probablement) arriver à un décalage bizarre qui ne pourrait pas être interprété correctement.

27voto

papercrane Points 156

La réponse de S.Lott ci-dessus fonctionne presque pour moi mais finit par me donner des lignes partielles. Il s'avère qu'elle corrompt les données aux limites des blocs parce que data contient les blocs lus dans un ordre inversé. Lorsque ''.join(data) est appelé, les blocs sont dans le mauvais ordre. Ceci corrige cela.

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:]

1 votes

Insérer au début de la liste est une mauvaise idée. Pourquoi ne pas utiliser la structure deque ?

1 votes

Malheureusement pas compatible avec Python 3... j'essaie de comprendre pourquoi.

24voto

Armin Ronacher Points 16894

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

7 votes

Ne répond pas exactement à la question.

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