web-dev-qa-db-fra.com

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

J'écris une visionneuse de fichier journal pour une application Web et je souhaite pour cela paginer entre les lignes du fichier journal. Les éléments du fichier sont basés sur les lignes, l’article le plus récent se trouvant en bas.

Il me faut donc une méthode tail() capable de lire les lignes n à partir du bas et prenant en charge un décalage. Ce que je suis venu avec 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 réduire les fichiers journaux avec des décalages?

162
Armin Ronacher

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
19
Armin Ronacher

Cela peut être plus rapide que le vôtre. Ne fait aucune hypothèse sur la longueur de la ligne. Parcourt le fichier bloc par bloc jusqu'à ce qu'il ait trouvé 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 lorsque, pour des raisons pratiques, vous ne pouvez jamais savoir de telles choses.

Généralement, cela localisera les 20 dernières lignes du premier ou du deuxième passage dans la boucle. Si votre position de 74 caractères est réellement exacte, vous créez une taille de bloc de 2048 et vous réduisez presque immédiatement 20 lignes.

De plus, je ne brûle pas beaucoup de calories dans le cerveau en essayant de mettre fin à l'alignement avec les blocs physiques du système d'exploitation. Avec ces packages d'E/S de haut niveau, je doute que vous constatiez une conséquence sur les performances si vous essayez d'aligner les limites des blocs du système d'exploitation. Si vous utilisez des E/S de niveau inférieur, vous constaterez peut-être une accélération.

115
S.Lott

Suppose 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 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]
73
Mark

Si la lecture de tout le fichier est acceptable, utilisez un deque.

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

Avant la version 2.6, deques n’avait pas d’option maxlen, mais elle est assez facile à mettre en œuvre.

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 à la fin, utilisez une recherche au galop (exponentielle a.k.a).

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:]
29
A. Coady

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 sont stockées dans l'ordre inverse Lorsque ".join (data) est appelé, les blocs sont dans le mauvais ordre. Cela corrige ça.

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

Voici ma réponse. Pur python. En utilisant timeit, cela semble assez rapide. Tail 100 lignes d'un fichier journal de 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:]
24
glenbot

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()
13
dimitri

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')
4
Hauke Rehfeld

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, mmap est le meilleur moyen de procéder. Pour améliorer la réponse mmap existante, cette version est portable entre Windows et Linux et devrait fonctionner plus rapidement (bien qu'elle ne fonctionne pas sans quelques modifications sur Python 32 bits avec des fichiers de la plage de Go, voir la réponse autre pour obtenir des conseils sur le traitement 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 en queue est suffisamment petit pour pouvoir les lire toutes en mémoire en une fois; vous pouvez également en faire une fonction génératrice 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 se fait en mode binaire (il est nécessaire d’utiliser mmap) pour donner les lignes str (Py2) et les lignes bytes (Py3); si vous voulez unicode (Py2) ou str (Py3), l'approche itérative peut être modifiée pour la décodage et/ou la correction des 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 tout saisi sur une machine sur laquelle je n'ai pas accès à Python à tester. S'il vous plaît laissez-moi savoir si j'ai tapé quelque chose; cela était assez similaire à mon autre réponse que je pense cela devrait fonctionner, mais les ajustements (par exemple, manipuler une offset) pourraient conduire à des erreurs subtiles. S'il vous plaît laissez-moi savoir dans les commentaires s'il y a des erreurs.

3
ShadowRanger

J'ai trouvé le Popen ci-dessus pour être la meilleure solution. C’est rapide, sale et ça marche Pour Python 2.6 sur Unix, j’utilisais 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 aura contiendra les n dernières lignes du code. pour parcourir ligne par ligne soutput faire:

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

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

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()
2
GL2014

d'après 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.

2
Eyecue

Il existe certaines implémentations de tail on pypi que vous pouvez installer avec pip: 

  • mtFileUtil
  • multitail
  • log4tailer
  • ...

Selon votre situation, l’utilisation de l’un de ces outils existants peut présenter des avantages.

2
Travis Bear

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
1
rabbit

Pour être efficace avec des fichiers très volumineux (ce qui est courant dans les situations de fichier journal où vous voudrez peut-être utiliser tail), vous voudrez généralement éviter de lire le fichier entier (même si vous le faites sans lire le fichier entier en mémoire en une fois). besoin de trouver en quelque sorte 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 dans des blocs plus grands.

J'ai écrit il y a quelque temps une fonction utilitaire pour lire les fichiers à l'envers que vous pouvez utiliser 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 de devoir inverser deux fois)

1
Brian

Simple:

with open("test.txt") as f:
data = f.readlines()
tail = data[-2:]
print(''.join(tail)
1
Samba Siva Reddy

Basé sur la réponse de Eyecue (10 juin 10 à 21:28): cette classe ajoute les méthodes head () et tail () pour archiver un objet.

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)
1
fdb

Plusieurs de ces solutions ont 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
1
David Rogers

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

Fonctionne avec python 3 .

Ceci utilise recherche exponentielle et ne met en mémoire tampon que N lignes à partir du dos 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))

0
Jigar Wala

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 __== '__main__':
    for line in tail(sys.stdin,10):
        sys.stdout.write(line)
0
Hal Canary
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
0
Raj

Il est très utile module qui peut faire ceci:

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)
0
Quinten Cabo

J'ai dû lire une valeur spécifique de la dernière ligne d'un fichier et suis tombé sur ce fil. Plutôt que de réinventer la roue en Python, je me suis retrouvé avec un petit script Shell, enregistré sous le nom de.

#! /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"))
0
Leifbk
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]))
0
Y Kal
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
0
moylop260
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) 
0
Kant Manapure