Question

J'écris un visualiseur 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 alignés, 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. Voici ce que j'ai créé:

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?

Était-ce utile?

La solution 6

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

Autres conseils

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 trouve le nombre correct 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.

En général, cela localisera les 20 dernières lignes du premier ou du deuxième passage dans la boucle. Si votre texte de 74 caractères est précis, vous définissez une taille de bloc de 2048 et vous couperez presque 20 lignes immédiatement.

De plus, je ne brûle pas beaucoup de calories dans le cerveau en essayant d’aligner avec finesse 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 en termes de performances si vous essayez d'aligner les limites de blocs du système d'exploitation. Si vous utilisez des E / S de niveau inférieur, vous constaterez peut-être une accélération.

Supposons qu'un système de type Unix sous Python 2 puisse être utilisé:

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]

Si la lecture de l'intégralité du 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 à partir de la fin, utilisez une recherche au galop (a.k.a 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:]

Voici ma réponse. Pur python. En utilisant timeit, cela semble assez rapide. Triage de 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:]
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 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:]

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

Une version compatible python3 encore plus propre qui n’insère pas mais ajoute & amp; renverse:

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

J'ai trouvé le Popen ci-dessus comme étant la meilleure solution. C'est rapide et sale et ça marche Pour Python 2.6 sur machine 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 n dernières lignes du code. pour parcourir ligne par ligne soutput faire:

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

Mettez à jour la solution @papercrane vers python3. Ouvrez le fichier avec open (nom du fichier, '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:]

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

Pour un fichier de taille importante, mmap a> est le meilleur moyen de le faire. Pour améliorer la réponse mmap existante, cette version est portable entre Windows et Linux et devrait fonctionner plus vite (bien qu'elle ne puisse fonctionner sans certaines modifications sur Python 32 bits avec des fichiers de l'ordre de Go, voir La autre réponse contient des astuces permettant de gérer ce problème et de le modifier pour qu'il fonctionne sous 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 que vous puissiez toutes les lire en toute sécurité en une seule 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, cela se lit en mode binaire (il est nécessaire d’utiliser mmap ) pour donner des lignes str (Py2) et des lignes octets (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 pour effectuer un test. S'il vous plaît laissez-moi savoir si j'ai tapé quelque chose; c’était assez similaire pour mon autre réponse que je pensais que cela devrait fonctionner, mais les ajustements nécessaires (par exemple, la gestion d’un décalage ) pourrait conduire à des erreurs subtiles. Faites-le moi savoir dans les commentaires s'il y a des erreurs.

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 cela vous sera utile.

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

  • mtFileUtil
  • multitail
  • log4tailer
  • ...

L'utilisation de l'un de ces outils existants peut présenter des avantages.

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

Simple:

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

Pour être efficace avec des fichiers très volumineux (ce qui est courant dans les situations où vous souhaitez utiliser un fichier journal), évitez généralement de lire tout le fichier (même si vous le faites sans lire tout le fichier en mémoire en même temps). , vous devez en quelque sorte calculer 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 une fonction utilitaire que j'ai écrite il y a quelque temps pour lire les fichiers en arrière 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 de devoir inverser deux fois)

vous pouvez aller à la fin de votre fichier avec f.seek (0, 2), puis lire les lignes une à 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

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

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

Utilisation:

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

Plusieurs de ces solutions ont des problèmes si le fichier ne se termine pas \ 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

J'ai dû lire une valeur spécifique à partir 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 / 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"))

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

Un module très utile peut le faire:

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)
À la réflexion, ceci 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 bien se dérouler à un bon rythme.

J'ai trouvé le moyen le plus facile de trouver la première ou la dernière ligne d'un fichier

Les N dernières lignes d'un fichier (par exemple: N = 10)

file=open("xyz.txt",'r")
liner=file.readlines()
for ran in range((len(liner)-N),len(liner)):
    print liner[ran]

Premières N lignes d'un fichier (par exemple: N = 10)

file=open("xyz.txt",'r")
liner=file.readlines()
for ran in range(0,N+1):
    print liner[ran]

c'est si simple:

def tail(fname,nl):
with open(fname) as f:
    data=f.readlines() #readlines return a list
    print(''.join(data[-nl:]))

Bien que cela ne soit pas vraiment efficace avec les gros fichiers, ce code est assez simple:

  1. Il lit l'objet fichier, f .
  2. Il divise la chaîne renvoyée à l'aide des nouvelles lignes, \ n .
  3. Il obtient le tableau qui répertorie les derniers index en utilisant le signe négatif pour indiquer les derniers index et le : pour obtenir un sous-tableau.

    def tail(f,n):
        return "\n".join(f.read().split("\n")[-n:])
    
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top