Récupère les n dernières lignes d'un fichier, similaire à tail

181

J'écris une visionneuse de fichiers journaux pour une application Web et pour cela, je veux paginer à travers les lignes du fichier journal. Les éléments du fichier sont basés sur la ligne avec l'élément le plus récent en bas.

J'ai donc besoin d'une tail()méthode capable de lire les nlignes par le bas et de prendre en charge un décalage. Ce que j'ai trouvé ressemble à ceci:

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

Est-ce une approche raisonnable? Quelle est la méthode recommandée pour terminer les fichiers journaux avec des décalages?

Armin Ronacher
la source
Sur mon système (linux SLES 10), la recherche relative à la fin lève une IOError "ne peut pas faire de recherches relatives à la fin non nulles". J'aime cette solution mais je l'ai modifiée pour obtenir la longueur du fichier ( seek(0,2)alors tell()), et j'utilise cette valeur pour rechercher par rapport au début.
Anne
2
Félicitations - cette question a été intégrée au code source de Kippo
Miles
Les paramètres de la opencommande utilisée pour générer l' fobjet fichier doivent être spécifiés, car selon si f=open(..., 'rb')ou f=open(..., 'rt')le fdoit être traité différemment
Igor Fobia

Réponses:

123

Cela peut être plus rapide que le vôtre. Ne fait aucune hypothèse sur la longueur de la ligne. Parcoure le fichier un bloc à la fois jusqu'à ce qu'il trouve le bon nombre de caractères «\ n».

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 quand - en pratique - on ne peut jamais savoir des choses comme ça.

Généralement, cela permettra de localiser les 20 dernières lignes sur le premier ou le deuxième passage dans la boucle. Si votre truc de 74 caractères est réellement précis, vous faites la taille de bloc de 2048 et vous suivrez 20 lignes presque immédiatement.

De plus, je ne brûle pas beaucoup de calories dans le cerveau en essayant d'affiner l'alignement avec les blocs OS physiques. En utilisant ces packages d'E / S de haut niveau, je doute que vous voyiez une conséquence sur les performances de la tentative d'alignement sur les limites des blocs du système d'exploitation. Si vous utilisez des E / S de niveau inférieur, vous verrez peut-être une accélération.


METTRE À JOUR

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

par exemple: 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:])
S.Lott
la source
13
Cela échoue sur les petits fichiers journaux - IOError: argument invalide - f.seek (block * 1024, 2)
ohnoes
1
Très belle approche en effet. J'ai utilisé une version légèrement modifiée du code ci-dessus et j'ai trouvé cette recette: code.activestate.com/recipes/577968-log-watcher-tail-f-log
Giampaolo Rodolà
6
Ne fonctionne plus en python 3.2. J'obtiens que io.UnsupportedOperation: can't do nonzero end-relative seeksje peux changer le décalage à 0, mais cela va à l'encontre de l'objectif de la fonction.
Logical Fallacy
4
@DavidEnglund Reason est ici . En bref: la recherche relative à la fin du fichier n'est pas autorisée en mode texte, probablement parce que le contenu du fichier doit être décodé, et, en général, la recherche d'une position arbitraire dans une séquence d'octets codés peut avoir des résultats indéfinis lorsque vous tenter de décoder en Unicode à partir de cette position. La suggestion proposée sur le lien est d'essayer d'ouvrir le fichier en mode binaire et de faire le décodage vous-même, en détectant les exceptions DecodeError.
max
6
N'UTILISEZ PAS CE CODE. Il corrompt les lignes dans certains cas de bordure en python 2.7. La réponse de @papercrane ci-dessous le corrige.
xApple
88

Suppose un système de type Unix sur Python 2 que 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 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]
marque
la source
5
Doit être indépendant de la plateforme. De plus, si vous lisez la question, vous verrez que f est un fichier comme un objet.
Armin Ronacher
40
la question ne dit pas que la dépendance à la plate-forme est inacceptable. Je ne vois pas pourquoi cela mérite deux votes négatifs alors que cela fournit une manière très unixy (peut-être ce que vous cherchez ... c'était certainement pour moi) de faire exactement ce que la question demande.
Shabbyrobe
3
Merci, je pensais que je devais résoudre cela en pur Python, mais il n'y a aucune raison de ne pas utiliser les utilitaires UNIX lorsqu'ils sont à portée de main, alors j'ai choisi cela. FWIW en Python moderne, subprocess.check_output est probablement préférable à os.popen2; cela simplifie un peu les choses car il renvoie simplement la sortie sous forme de chaîne et déclenche un code de sortie non nul.
mrooney
3
Bien que cela dépende de la plate-forme, c'est un moyen très efficace de faire ce qui a été demandé, en plus d'être un moyen extrêmement rapide de le faire (vous n'avez pas à charger le fichier entier en mémoire). @Shabbyrobe
earthmeLon
6
Vous voudrez peut-être précalculer le décalage comme: offset_total = str(n+offset)et remplacer cette ligne stdin,stdout = os.popen2("tail -n "+offset_total+" "+f)pour éviterTypeErrors (cannot concatenate int+str)
AddingColor
32

Voici ma réponse. Python pur. En utilisant timeit, cela semble assez rapide. Tailing 100 lignes d'un fichier journal qui a 100 000 lignes:

>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=10)
0.0014600753784179688
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=100)
0.00899195671081543
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=1000)
0.05842900276184082
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=10000)
0.5394978523254395
>>> timeit.timeit('tail.tail(f, 100, 4098)', 'import tail; f = open("log.txt", "r");', number=100000)
5.377126932144165

Voici le code:

import os


def tail(f, lines=1, _buffer=4098):
    """Tail a file and get X lines from the end"""
    # place holder for the lines found
    lines_found = []

    # block counter will be multiplied by buffer
    # to get the block size from the end
    block_counter = -1

    # loop until we find X lines
    while len(lines_found) < lines:
        try:
            f.seek(block_counter * _buffer, os.SEEK_END)
        except IOError:  # either file is too small, or too many lines requested
            f.seek(0)
            lines_found = f.readlines()
            break

        lines_found = f.readlines()

        # we found enough lines, get out
        # Removed this line because it was redundant the while will catch
        # it, I left it for history
        # if len(lines_found) > lines:
        #    break

        # decrement the block counter to get the
        # next X bytes
        block_counter -= 1

    return lines_found[-lines:]
Glenbot
la source
3
Solution élégante! Est-ce if len(lines_found) > lines:vraiment nécessaire? La loopcondition ne l' attraperait-elle pas également?
Maximilian Peters
Une question pour ma compréhension: est os.SEEK_ENDutilisé simplement pour plus de clarté? Autant que j'ai trouvé, sa valeur est constante (= 2). Je me demandais si je ne voulais pas le laisser de côté pour pouvoir laisser de côté le import os. Merci pour la bonne solution!
n1k31t4
2
@MaximilianPeters oui. Ce n'est pas nécessaire. Je l'ai commenté.
glenbot
@DexterMorgan vous pouvez remplacer os.SEEK_ENDpar son équivalent entier. C'était principalement là pour la lisibilité.
glenbot
1
J'ai voté pour, mais j'ai une petite note. Après la recherche, la première ligne lue peut être incomplète, donc pour obtenir N _complete_lines, j'ai changé le while len(lines_found) < linesen while len(lines_found) <= linesdans ma copie. Merci!
Graham Klyne
30

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

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

Avant la version 2.6, deques n'avait pas d'option maxlen, mais c'est assez facile à 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 à partir de la fin, utilisez une recherche au galop (alias 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:]
A. Coady
la source
Pourquoi cette fonction de fond fonctionne-t-elle? pos *= 2semble complètement arbitraire. Quelle est sa signification?
2mac
1
@ Recherche exponentielle 2mac . Il lit de manière itérative à partir de la fin du fichier, doublant la quantité lue à chaque fois, jusqu'à ce que suffisamment de lignes soient trouvées.
A. Coady
Je pense que la solution pour lire à partir de la fin ne prendra pas en charge les fichiers encodés avec UTF-8, car la longueur des caractères est variable, et vous pourriez (probablement) atterrir à un décalage étrange qui ne peut pas être interprété correctement.
Mike
Malheureusement, votre solution de recherche galopante ne fonctionne pas pour python 3. Comme f.seek () ne prend pas de décalage négatif. J'ai mis à jour votre code pour le faire fonctionner pour le lien
itsjwala
25

La réponse de S.Lott ci-dessus fonctionne presque pour moi mais finit par me donner des lignes partielles. Il s'avère que cela corrompt les données sur les limites des blocs car les données contiennent les blocs lus dans l'ordre inverse. Lorsque '' .join (data) est appelé, les blocs sont dans le mauvais ordre. Cela 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:]
papercrane
la source
1
L'insertion au début de la liste est une mauvaise idée. Pourquoi ne pas utiliser deque structure?
Sergey11g
1
Malheureusement pas compatible Python 3 ... en essayant de comprendre pourquoi.
Sherlock70
20

Le code que j'ai fini par utiliser. Je pense que c'est le meilleur à ce jour:

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
Armin Ronacher
la source
5
ne répond pas exactement à la question.
sheki
13

Solution simple et rapide avec mmap:

import mmap
import os

def tail(filename, n):
    """Returns last n lines from the filename. No exception handling"""
    size = os.path.getsize(filename)
    with open(filename, "rb") as f:
        # for Windows the mmap parameters are different
        fm = mmap.mmap(f.fileno(), 0, mmap.MAP_SHARED, mmap.PROT_READ)
        try:
            for i in xrange(size - 1, -1, -1):
                if fm[i] == '\n':
                    n -= 1
                    if n == -1:
                        break
            return fm[i + 1 if i else 0:].splitlines()
        finally:
            fm.close()
Dimitri
la source
1
C'est probablement la réponse la plus rapide lorsque l'entrée pourrait être énorme (ou ce serait le cas, si elle utilisait la .rfindméthode pour rechercher les nouvelles lignes en arrière, plutôt que d'effectuer des vérifications octet à la fois au niveau Python; en CPython, en remplaçant le code de niveau Python par Les appels intégrés en C gagnent généralement beaucoup). Pour les entrées plus petites, le dequeavec a maxlenest plus simple et probablement aussi rapide.
ShadowRanger
4

Une version compatible python3 encore plus propre qui n'insère pas mais ajoute et inverse:

def tail(f, window=1):
    """
    Returns the last `window` lines of file `f` as a list of bytes.
    """
    if window == 0:
        return b''
    BUFSIZE = 1024
    f.seek(0, 2)
    end = f.tell()
    nlines = window + 1
    data = []
    while nlines > 0 and end > 0:
        i = max(0, end - BUFSIZE)
        nread = min(end, BUFSIZE)

        f.seek(i)
        chunk = f.read(nread)
        data.append(chunk)
        nlines -= chunk.count(b'\n')
        end -= nread
    return b'\n'.join(b''.join(reversed(data)).splitlines()[-window:])

utilisez-le comme ceci:

with open(path, 'rb') as f:
    last_lines = tail(f, 3).decode('utf-8')
Hauke ​​Rehfeld
la source
Pas trop minable - mais je conseillerais en général de ne pas ajouter de réponse à une question de 10 ans avec beaucoup de réponses. Mais aidez-moi: qu'est-ce qui est spécifique à Python 3 dans votre code?
usr2564301
Les autres réponses ne fonctionnaient pas vraiment bien :-) py3: voir stackoverflow.com/questions/136168/...
Hauke ​​Rehfeld
3

Mettez à jour la solution @papercrane vers python3. Ouvrez le fichier avec open(filename, 'rb')et:

def tail(f, window=20):
    """Returns the last `window` lines of file `f` as a list.
    """
    if window == 0:
        return []

    BUFSIZ = 1024
    f.seek(0, 2)
    remaining_bytes = f.tell()
    size = window + 1
    block = -1
    data = []

    while size > 0 and remaining_bytes > 0:
        if remaining_bytes - BUFSIZ > 0:
            # Seek back one whole BUFSIZ
            f.seek(block * BUFSIZ, 2)
            # read BUFFER
            bunch = f.read(BUFSIZ)
        else:
            # file too small, start from beginning
            f.seek(0, 0)
            # only read what was not read
            bunch = f.read(remaining_bytes)

        bunch = bunch.decode('utf-8')
        data.insert(0, bunch)
        size -= bunch.count('\n')
        remaining_bytes -= BUFSIZ
        block -= 1

    return ''.join(data).splitlines()[-window:]
Emilio
la source
3

Publier une réponse à la demande des commentateurs sur ma réponse à une question similaire où la même technique a été utilisée pour muter la dernière ligne d'un fichier, pas seulement pour l'obtenir.

Pour un fichier de taille importante, mmapc'est la meilleure façon de le faire. Pour améliorer la mmapréponse existante , cette version est portable entre Windows et Linux, et devrait fonctionner plus rapidement (même si elle ne fonctionnera pas sans quelques modifications sur Python 32 bits avec des fichiers de la plage Go, voir l' autre réponse pour des conseils sur la gestion de cela , et pour modifier pour travailler sur Python 2 ).

import io  # Gets consistent version of open for both Py2.7 and Py3.x
import itertools
import mmap

def skip_back_lines(mm, numlines, startidx):
    '''Factored out to simplify handling of n and offset'''
    for _ in itertools.repeat(None, numlines):
        startidx = mm.rfind(b'\n', 0, startidx)
        if startidx < 0:
            break
    return startidx

def tail(f, n, offset=0):
    # Reopen file in binary mode
    with io.open(f.name, 'rb') as binf, mmap.mmap(binf.fileno(), 0, access=mmap.ACCESS_READ) as mm:
        # len(mm) - 1 handles files ending w/newline by getting the prior line
        startofline = skip_back_lines(mm, offset, len(mm) - 1)
        if startofline < 0:
            return []  # Offset lines consumed whole file, nothing to return
            # If using a generator function (yield-ing, see below),
            # this should be a plain return, no empty list

        endoflines = startofline + 1  # Slice end to omit offset lines

        # Find start of lines to capture (add 1 to move from newline to beginning of following line)
        startofline = skip_back_lines(mm, n, startofline) + 1

        # Passing True to splitlines makes it return the list of lines without
        # removing the trailing newline (if any), so list mimics f.readlines()
        return mm[startofline:endoflines].splitlines(True)
        # If Windows style \r\n newlines need to be normalized to \n, and input
        # is ASCII compatible, can normalize newlines with:
        # return mm[startofline:endoflines].replace(os.linesep.encode('ascii'), b'\n').splitlines(True)

Cela suppose que le nombre de lignes suivies est suffisamment petit pour pouvoir les lire toutes en mémoire en toute sécurité; vous pouvez également en faire une fonction de générateur et lire manuellement une ligne à la fois en remplaçant la dernière ligne par:

        mm.seek(startofline)
        # Call mm.readline n times, or until EOF, whichever comes first
        # Python 3.2 and earlier:
        for line in itertools.islice(iter(mm.readline, b''), n):
            yield line

        # 3.3+:
        yield from itertools.islice(iter(mm.readline, b''), n)

Enfin, cette lecture en mode binaire (nécessaire à utiliser mmap) donc elle donne des strlignes (Py2) et des byteslignes (Py3); si vous voulez unicode(Py2) ou str(Py3), l'approche itérative pourrait être modifiée pour décoder pour vous et / ou corriger les nouvelles lignes:

        lines = itertools.islice(iter(mm.readline, b''), n)
        if f.encoding:  # Decode if the passed file was opened with a specific encoding
            lines = (line.decode(f.encoding) for line in lines)
        if 'b' not in f.mode:  # Fix line breaks if passed file opened in text mode
            lines = (line.replace(os.linesep, '\n') for line in lines)
        # Python 3.2 and earlier:
        for line in lines:
            yield line
        # 3.3+:
        yield from lines

Remarque: j'ai tapé tout cela sur une machine sur laquelle je n'ai pas accès à Python pour tester. S'il vous plaît laissez-moi savoir si j'ai fait une faute de frappe; c'était assez similaire à mon autre réponse que je pense que cela devrait fonctionner, mais les ajustements (par exemple la gestion d'un offset) pourraient conduire à des erreurs subtiles. S'il vous plaît laissez-moi savoir dans les commentaires s'il y a des erreurs.

ShadowRanger
la source
3

J'ai trouvé que le Popen ci-dessus était la meilleure solution. C'est rapide et sale et cela fonctionne Pour python 2.6 sur une machine Unix, j'ai utilisé ce qui suit

def GetLastNLines(self, n, fileName):
    """
    Name:           Get LastNLines
    Description:        Gets last n lines using Unix tail
    Output:         returns last n lines of a file
    Keyword argument:
    n -- number of last lines to return
    filename -- Name of the file you need to tail into
    """
    p = subprocess.Popen(['tail','-n',str(n),self.__fileName], stdout=subprocess.PIPE)
    soutput, sinput = p.communicate()
    return soutput

soutput contiendra les n dernières lignes du code. pour parcourir le soutput ligne par ligne, faites:

for line in GetLastNLines(50,'myfile.log').split('\n'):
    print line
Marko
la source
2

basé sur la réponse la plus votée de S.Lott (25 septembre 2008 à 21:43), mais corrigé pour les petits fichiers.

def tail(the_file, lines_2find=20):  
    the_file.seek(0, 2)                         #go to end of file
    bytes_in_file = the_file.tell()             
    lines_found, total_bytes_scanned = 0, 0
    while lines_2find+1 > lines_found and bytes_in_file > total_bytes_scanned: 
        byte_block = min(1024, bytes_in_file-total_bytes_scanned)
        the_file.seek(-(byte_block+total_bytes_scanned), 2)
        total_bytes_scanned += byte_block
        lines_found += the_file.read(1024).count('\n')
    the_file.seek(-total_bytes_scanned, 2)
    line_list = list(the_file.readlines())
    return line_list[-lines_2find:]

    #we read at least 21 line breaks from the bottom, block by block for speed
    #21 to ensure we don't get a half line

J'espère que c'est utile.

Eyecue
la source
2

Il existe des implémentations existantes de tail sur pypi que vous pouvez installer à l'aide de pip:

  • mtFileUtil
  • multitail
  • log4tailer
  • ...

Selon votre situation, il peut y avoir des avantages à utiliser l'un de ces outils existants.

Travis Bear
la source
Connaissez-vous un module qui fonctionne sous Windows? J'ai essayé tailhead, tailermais cela n'a pas fonctionné. Également essayé mtFileUtil. C'était initialement une erreur parce que les printdéclarations n'avaient pas de parenthèses (je suis sur Python 3.6). Je les ai ajoutés reverse.pyet les messages d'erreur ont disparu, mais lorsque mon script appelle le module ( mtFileUtil.tail(open(logfile_path), 5)), il n'imprime rien.
Technext
2

Facile :

with open("test.txt") as f:
data = f.readlines()
tail = data[-2:]
print(''.join(tail)
Samba Siva Reddy
la source
C'est une mise en œuvre totalement mauvaise. Envisagez de gérer d'énormes fichiers, et où n est également énorme, opération trop coûteuse
Nivesh Krishna
1

Pour plus d'efficacité avec des fichiers très volumineux (courant dans les situations de fichier journal où vous voudrez peut-être utiliser tail), vous voulez généralement éviter de lire le fichier entier (même si vous le faites sans lire le fichier entier en mémoire à la fois). besoin de travailler en quelque sorte sur le décalage en lignes plutôt qu'en caractères. Une possibilité est de lire à l'envers avec seek () char par char, mais c'est très lent. Au lieu de cela, il est préférable de traiter des blocs plus grands.

J'ai une fonction utilitaire que j'ai écrite il y a quelque temps pour lire les fichiers à l'envers qui peuvent être utilisés ici.

import os, itertools

def rblocks(f, blocksize=4096):
    """Read file as series of blocks from end of file to start.

    The data itself is in normal order, only the order of the blocks is reversed.
    ie. "hello world" -> ["ld","wor", "lo ", "hel"]
    Note that the file must be opened in binary mode.
    """
    if 'b' not in f.mode.lower():
        raise Exception("File must be opened using binary mode.")
    size = os.stat(f.name).st_size
    fullblocks, lastblock = divmod(size, blocksize)

    # The first(end of file) block will be short, since this leaves 
    # the rest aligned on a blocksize boundary.  This may be more 
    # efficient than having the last (first in file) block be short
    f.seek(-lastblock,2)
    yield f.read(lastblock)

    for i in range(fullblocks-1,-1, -1):
        f.seek(i * blocksize)
        yield f.read(blocksize)

def tail(f, nlines):
    buf = ''
    result = []
    for block in rblocks(f):
        buf = block + buf
        lines = buf.splitlines()

        # Return all lines except the first (since may be partial)
        if lines:
            result.extend(lines[1:]) # First line may not be complete
            if(len(result) >= nlines):
                return result[-nlines:]

            buf = lines[0]

    return ([buf]+result)[-nlines:]


f=open('file_to_tail.txt','rb')
for line in tail(f, 20):
    print line

[Modifier] Ajout d'une version plus spécifique (évite d'avoir à inverser deux fois)

Brian
la source
Un test rapide montre que cela fonctionne bien pire que ma version ci-dessus. Probablement à cause de votre mise en mémoire tampon.
Armin Ronacher le
Je soupçonne que c'est parce que je fais plusieurs recherches en arrière, donc je n'utilise pas aussi bien le tampon de lecture anticipée. Cependant, je pense que cela peut faire mieux lorsque votre estimation de la longueur de la ligne n'est pas précise (par exemple, de très grandes lignes), car cela évite d'avoir à relire les données dans ce cas.
Brian
1

vous pouvez aller à la fin de votre fichier avec f.seek (0, 2) puis lire les lignes une par une avec le remplacement suivant pour readline ():

def readline_backwards(self, f):
    backline = ''
    last = ''
    while not last == '\n':
        backline = last + backline
        if f.tell() <= 0:
            return backline
        f.seek(-1, 1)
        last = f.read(1)
        f.seek(-1, 1)
    backline = last
    last = ''
    while not last == '\n':
        backline = last + backline
        if f.tell() <= 0:
            return backline
        f.seek(-1, 1)
        last = f.read(1)
        f.seek(-1, 1)
    f.seek(1, 1)
    return backline
lapin
la source
1

Basé sur la réponse d'Eyecue (10 juin 10 à 21:28): cette classe ajoute la méthode head () et tail () à l'objet de fichier.

class File(file):
    def head(self, lines_2find=1):
        self.seek(0)                            #Rewind file
        return [self.next() for x in xrange(lines_2find)]

    def tail(self, lines_2find=1):  
        self.seek(0, 2)                         #go to end of file
        bytes_in_file = self.tell()             
        lines_found, total_bytes_scanned = 0, 0
        while (lines_2find+1 > lines_found and
               bytes_in_file > total_bytes_scanned): 
            byte_block = min(1024, bytes_in_file-total_bytes_scanned)
            self.seek(-(byte_block+total_bytes_scanned), 2)
            total_bytes_scanned += byte_block
            lines_found += self.read(1024).count('\n')
        self.seek(-total_bytes_scanned, 2)
        line_list = list(self.readlines())
        return line_list[-lines_2find:]

Usage:

f = File('path/to/file', 'r')
f.head(3)
f.tail(3)
fdb
la source
1

Plusieurs de ces solutions posent des problèmes si le fichier ne se termine pas par \ n ou si la première ligne complète est lue.

def tail(file, n=1, bs=1024):
    f = open(file)
    f.seek(-1,2)
    l = 1-f.read(1).count('\n') # If file doesn't end in \n, count it anyway.
    B = f.tell()
    while n >= l and B > 0:
            block = min(bs, B)
            B -= block
            f.seek(B, 0)
            l += f.read(block).count('\n')
    f.seek(B, 0)
    l = min(l,n) # discard first (incomplete) line if l > n
    lines = f.readlines()[-l:]
    f.close()
    return lines
David Rogers
la source
1

Voici une implémentation assez simple:

with open('/etc/passwd', 'r') as f:
  try:
    f.seek(0,2)
    s = ''
    while s.count('\n') < 11:
      cur = f.tell()
      f.seek((cur - 10))
      s = f.read(10) + s
      f.seek((cur - 10))
    print s
  except Exception as e:
    f.readlines()
GL2014
la source
Excellent exemple! Pourriez-vous s'il vous plaît expliquer l'utilisation d'essayer avant le f.seek? Pourquoi pas avant le with open? Aussi, pourquoi dans le exceptvous faites un f.readlines()??
Honnêtement, l'essai devrait probablement commencer. Je ne me souviens pas d'avoir une raison pour ne pas attraper l'open () autrement que sur un système Linux standard sain, / etc / passwd devrait toujours être lisible. essayez, alors avec est l'ordre le plus courant.
GL2014
1

Il existe un module très utile qui peut faire cela:

from file_read_backwards import FileReadBackwards

with FileReadBackwards("/tmp/file", encoding="utf-8") as frb:

# getting lines by lines starting from the last line up
for l in frb:
    print(l)
Quinten Cabo
la source
1

Une autre solution

si votre fichier txt ressemble à ceci: souris serpent chat lézard chien loup

vous pouvez inverser ce fichier en utilisant simplement l'indexation de tableau en python '' '

contents=[]
def tail(contents,n):
    with open('file.txt') as file:
        for i in file.readlines():
            contents.append(i)

    for i in contents[:n:-1]:
        print(i)

tail(contents,-5)

résultat: chien loup lézard chat

Blaine McMahon
la source
1

Le moyen le plus simple est d'utiliser deque:

from collections import deque

def tail(filename, n=10):
    with open(filename) as f:
        return deque(f, n)
Zhen Wang
la source
0

J'ai dû lire une valeur spécifique de la dernière ligne d'un fichier et je suis tombé sur ce fil. Plutôt que de réinventer la roue en Python, je me suis retrouvé avec un minuscule script shell, enregistré sous / usr / local / bin / get_last_netp:

#! /bin/bash
tail -n1 /home/leif/projects/transfer/export.log | awk {'print $14'}

Et dans le programme Python:

from subprocess import check_output

last_netp = int(check_output("/usr/local/bin/get_last_netp"))
Leifbk
la source
0

Pas le premier exemple utilisant un deque, mais un plus simple. Celui-ci est général: il fonctionne sur n'importe quel objet itérable, pas seulement un fichier.

#!/usr/bin/env python
import sys
import collections
def tail(iterable, N):
    deq = collections.deque()
    for thing in iterable:
        if len(deq) >= N:
            deq.popleft()
        deq.append(thing)
    for thing in deq:
        yield thing
if __name__ == '__main__':
    for line in tail(sys.stdin,10):
        sys.stdout.write(line)
Hal Canary
la source
0
This is my version of tailf

import sys, time, os

filename = 'path to file'

try:
    with open(filename) as f:
        size = os.path.getsize(filename)
        if size < 1024:
            s = size
        else:
            s = 999
        f.seek(-s, 2)
        l = f.read()
        print l
        while True:
            line = f.readline()
            if not line:
                time.sleep(1)
                continue
            print line
except IOError:
    pass
Raj
la source
0
import time

attemps = 600
wait_sec = 5
fname = "YOUR_PATH"

with open(fname, "r") as f:
    where = f.tell()
    for i in range(attemps):
        line = f.readline()
        if not line:
            time.sleep(wait_sec)
            f.seek(where)
        else:
            print line, # already has newline
moylop260
la source
0
import itertools
fname = 'log.txt'
offset = 5
n = 10
with open(fname) as f:
    n_last_lines = list(reversed([x for x in itertools.islice(f, None)][-(offset+1):-(offset+n+1):-1]))
Y Kal
la source
0
abc = "2018-06-16 04:45:18.68"
filename = "abc.txt"
with open(filename) as myFile:
    for num, line in enumerate(myFile, 1):
        if abc in line:
            lastline = num
print "last occurance of work at file is in "+str(lastline) 
Manapure de Kant
la source
0

Mise à jour pour réponse donnée par A.Coady

Fonctionne avec python 3 .

Cela utilise la recherche exponentielle et ne tamponnera que les Nlignes de l'arrière et est très efficace.

import time
import os
import sys

def tail(f, n):
    assert n >= 0
    pos, lines = n+1, []

    # set file pointer to end

    f.seek(0, os.SEEK_END)

    isFileSmall = False

    while len(lines) <= n:
        try:
            f.seek(f.tell() - pos, os.SEEK_SET)
        except ValueError as e:
            # lines greater than file seeking size
            # seek to start
            f.seek(0,os.SEEK_SET)
            isFileSmall = True
        except IOError:
            print("Some problem reading/seeking the file")
            sys.exit(-1)
        finally:
            lines = f.readlines()
            if isFileSmall:
                break

        pos *= 2

    print(lines)

    return lines[-n:]




with open("stream_logs.txt") as f:
    while(True):
        time.sleep(0.5)
        print(tail(f,2))
itsjwala
la source
-1

Après réflexion, c'est probablement aussi rapide que n'importe quoi ici.

def tail( f, window=20 ):
    lines= ['']*window
    count= 0
    for l in f:
        lines[count%window]= l
        count += 1
    print lines[count%window:], lines[:count%window]

C'est beaucoup plus simple. Et cela semble se dérouler à un bon rythme.

S.Lott
la source
Parce que presque tout ici ne fonctionne pas avec des fichiers journaux de plus de 30 Mo environ sans charger la même quantité de mémoire dans la RAM;) Votre première version est bien meilleure, mais pour les fichiers de test ici, elle fonctionne légèrement moins bien que la mienne et cela ne fonctionne pas avec différents caractères de nouvelle ligne.
Armin Ronacher
3
J'avais tort. La version 1 a pris 0,00248908996582 pour 10 queues dans le dictionnaire. La version 2 a pris 1.2963051796 pour 10 queues dans le dictionnaire. Je voterais presque moi-même contre.
S.Lott
"ne fonctionne pas avec différents caractères de nouvelle ligne." Remplacez datacount ('\ n') par len (data.splitlines ()) si cela est important.
S.Lott