web-dev-qa-db-fra.com

Le moyen le plus efficace de rechercher les x dernières lignes d'un fichier en python

J'ai un fichier et je ne sais pas quelle sera sa taille (il pourrait être assez volumineux, mais sa taille varie énormément). Je veux rechercher les 10 dernières lignes ou plus pour voir si l'une d'elles correspond à une chaîne. Je dois le faire aussi rapidement et efficacement que possible et je me demandais s'il y avait quelque chose de mieux que:

s = "foo"
last_bit = fileObj.readlines()[-10:]
for line in last_bit:
    if line == s:
        print "FOUND"
31
Harley Holcombe
# Tail
from __future__ import with_statement

find_str = "FIREFOX"                    # String to find
fname = "g:/autoIt/ActiveWin.log_2"     # File to check

with open(fname, "r") as f:
    f.seek (0, 2)           # Seek @ EOF
    fsize = f.tell()        # Get Size
    f.seek (max (fsize-1024, 0), 0) # Set pos @ last n chars
    lines = f.readlines()       # Read to end

lines = lines[-10:]    # Get last 10 lines

# This returns True if any line is exactly find_str + "\n"
print find_str + "\n" in lines

# If you're searching for a substring
for line in lines:
    if find_str in line:
        print True
        break
33
PabloG

Voici une réponse semblable à celle de MizardX, mais sans son problème apparent de prendre du temps quadratique dans le pire des cas, en analysant à nouveau la chaîne de travail à la recherche de nouvelles lignes à mesure que des morceaux sont ajoutés.

Comparé à la solution active (qui semble également être quadratique), cela n’explose pas si un fichier est vide, et on cherche une lecture par bloc lu au lieu de deux.

Comparé à la «queue» de frai, il est autonome. (Mais 'queue' est préférable si vous en avez.)

Par rapport à la saisie de quelques ko à la fin et en espérant que cela suffira, cela fonctionne pour toute longueur de ligne.

import os

def reversed_lines(file):
    "Generate the lines of file in reverse order."
    part = ''
    for block in reversed_blocks(file):
        for c in reversed(block):
            if c == '\n' and part:
                yield part[::-1]
                part = ''
            part += c
    if part: yield part[::-1]

def reversed_blocks(file, blocksize=4096):
    "Generate blocks of file's contents in reverse order."
    file.seek(0, os.SEEK_END)
    here = file.tell()
    while 0 < here:
        delta = min(blocksize, here)
        here -= delta
        file.seek(here, os.SEEK_SET)
        yield file.read(delta)

Pour l'utiliser comme demandé:

from itertools import islice

def check_last_10_lines(file, key):
    for line in islice(reversed_lines(file), 10):
        if line.rstrip('\n') == key:
            print 'FOUND'
            break

Éditer: modifié map () en itertools.imap () dans head (). Éditer 2: simplifié reverse_blocks (). Éditer 3: éviter de réanalyser la fin des nouvelles lignes. Edit 4: rewrote reverse_lines () car str.splitlines () ignore un '\ n' final, comme BrianB l’a remarqué (merci).

Notez que dans les très anciennes versions de Python, la concaténation de chaînes dans une boucle prend un temps quadratique. CPython des dernières années au moins évite automatiquement ce problème.

33
Darius Bacon

Si vous exécutez Python sur un système POSIX, vous pouvez utiliser 'tail -10' pour récupérer les dernières lignes. Cela peut être plus rapide que d’écrire votre propre code Python pour obtenir les 10 dernières lignes. Plutôt que d'ouvrir le fichier directement, ouvrez un tuyau à partir de la commande 'tail -10 nomfichier'. Si vous êtes cependant certain de la sortie du journal (par exemple, vous savez qu'il y a jamais de très longues lignes de plusieurs centaines ou milliers de caractères), utilisez ensuite l'une des approches 'lire les 2 derniers Ko' énumérés serait bien.

8
Myrddin Emrys

Je pense qu'en lisant les 2 derniers Ko environ du fichier, vous devriez vous assurer que vous avez 10 lignes et que cela ne devrait pas être un gros problème de ressources.

file_handle = open("somefile")
file_size = file_handle.tell()
file_handle.seek(max(file_size - 2*1024, 0))

# this will get rid of trailing newlines, unlike readlines()
last_10 = file_handle.read().splitlines()[-10:]

assert len(last_10) == 10, "Only read %d lines" % len(last_10)
7
Ryan Ginstrom

Voici une version utilisant mmap qui semble assez efficace. Le gros avantage est que mmap gérera automatiquement les exigences de pagination de fichier à mémoire pour vous.

import os
from mmap import mmap

def lastn(filename, n):
    # open the file and mmap it
    f = open(filename, 'r+')
    m = mmap(f.fileno(), os.path.getsize(f.name))

    nlcount = 0
    i = m.size() - 1 
    if m[i] == '\n': n += 1
    while nlcount < n and i > 0:
        if m[i] == '\n': nlcount += 1
        i -= 1
    if i > 0: i += 2

    return m[i:].splitlines()

target = "target string"
print [l for l in lastn('somefile', 10) if l == target]
5
mhawke

Je me suis heurté à ce problème, en analysant la dernière heure de gros fichiers syslog, et ai utilisé cette fonction à partir du site de recettes d’activestate ... ( http://code.activestate.com/recipes/439045/ )

!/usr/bin/env python
# -*-mode: python; coding: iso-8859-1 -*-
#
# Copyright (c) Peter Astrand <[email protected]>

import os
import string

class BackwardsReader:
    """Read a file line by line, backwards"""
    BLKSIZE = 4096

    def readline(self):
        while 1:
            newline_pos = string.rfind(self.buf, "\n")
            pos = self.file.tell()
            if newline_pos != -1:
                # Found a newline
                line = self.buf[newline_pos+1:]
                self.buf = self.buf[:newline_pos]
                if pos != 0 or newline_pos != 0 or self.trailing_newline:
                    line += "\n"
                return line
            else:
                if pos == 0:
                    # Start-of-file
                    return ""
                else:
                    # Need to fill buffer
                    toread = min(self.BLKSIZE, pos)
                    self.file.seek(-toread, 1)
                    self.buf = self.file.read(toread) + self.buf
                    self.file.seek(-toread, 1)
                    if pos - toread == 0:
                        self.buf = "\n" + self.buf

    def __init__(self, file):
        self.file = file
        self.buf = ""
        self.file.seek(-1, 2)
        self.trailing_newline = 0
        lastchar = self.file.read(1)
        if lastchar == "\n":
            self.trailing_newline = 1
            self.file.seek(-1, 2)

# Example usage
br = BackwardsReader(open('bar'))

while 1:
    line = br.readline()
    if not line:
        break
    print repr(line)

Cela fonctionne vraiment bien et est beaucoup plus efficace que tout ce qui ressemble à fileObj.readlines () [- 10:], ce qui permet à python de lire le fichier entier en mémoire, puis d’en couper les dix dernières lignes.

2
user32716

Si vous êtes sur une boîte unix, os.popen("tail -10 " + filepath).readlines() sera probablement le moyen le plus rapide. Sinon, cela dépend de la robustesse que vous souhaitez. Les méthodes proposées jusqu'ici vont toutes tomber, d'une manière ou d'une autre. Pour des raisons de robustesse et de rapidité, dans le cas le plus courant, vous souhaiterez probablement une recherche logarithmique: utilisez file.seek pour aller à la fin du fichier moins 1000 caractères, lisez-le, vérifiez le nombre de lignes qu’il contient, puis appuyez sur EOF moins 3000 caractères, lu en 2000 caractères, compte les lignes, puis EOF moins 7000, lu en 4000 caractères, compte les lignes, etc. jusqu'à obtenir autant de lignes que nécessaire. Mais si vous savez avec certitude qu'il sera toujours exécuté sur des fichiers avec des longueurs de ligne raisonnables, vous n'en aurez peut-être pas besoin.

Vous pouvez également trouver une source d’inspiration dans code source pour la commande unix tail.

2
Alex Coventry

Je pense me souvenir d’avoir adapté le code de ce billet de blog de Manu Garg lorsque je devais faire quelque chose de similaire.

2
Daryl Spitzer

J'ai suivi la suggestion de mhawke d'utiliser mmap et ai écrit une version qui utilise rfind:

from mmap import mmap
import sys

def reverse_file(f):
    mm = mmap(f.fileno(), 0)
    nl = mm.size() - 1
    prev_nl = mm.size()
    while nl > -1:
        nl = mm.rfind('\n', 0, nl)
        yield mm[nl + 1:prev_nl]
        prev_nl = nl + 1

def main():
    # Example usage
    with open('test.txt', 'r+') as infile:
        for line in reverse_file(infile):
            sys.stdout.write(line)
1
Edd

Vous pouvez également compter les lignes au fur et à mesure que vous parcourez le fichier, au lieu de deviner un décalage d'octet. 

lines = 0
chunk_size = 1024

f = file('filename')
f.seek(0, 2)
f.seek(f.tell() - chunk_size)

while True:
    s = f.read(chunk_size)
    lines += s.count('\n')
    if lines > NUM_OF_LINES:
        break
    f.seek(f.tell() - chunk_size*2)

Le fichier est maintenant dans une bonne position pour exécuter readlines(). Vous pouvez également mettre en cache les chaînes que vous avez lues pour la première fois, afin d'éviter de lire deux fois la même partie du fichier.

1
JimB

Vous pouvez lire des fragments d'environ 1 000 octets à partir de la fin du fichier dans un tampon jusqu'à 10 lignes.

1
Robert Gamble

Tout d'abord, une fonction qui retourne une liste:

def lastNLines(file, N=10, chunksize=1024):
    lines = None
    file.seek(0,2) # go to eof
    size = file.tell()
    for pos in xrange(chunksize,size-1,chunksize):
        # read a chunk
        file.seek(pos,2)
        chunk = file.read(chunksize)
        if lines is None:
            # first time
            lines = chunk.splitlines()
        else:
            # other times, update the 'first' line with
            # the new data, and re-split
            lines[0:1] = (chunk + lines[0]).splitlines()
        if len(lines) > N:
            return lines[-N:]
    file.seek(0)
    chunk = file.read(size-pos)
    lines[0:1] = (chunk + lines[0]).splitlines()
    return lines[-N:]

Deuxièmement, une fonction qui parcourt les lignes dans l’ordre inverse:

def iter_lines_reversed(file, chunksize=1024):
    file.seek(0,2)
    size = file.tell()
    last_line = ""
    for pos in xrange(chunksize,size-1,chunksize):
        # read a chunk
        file.seek(pos,2)
        chunk = file.read(chunksize) + last_line
        # split into lines
        lines = chunk.splitlines()
        last_line = lines[0]
        # iterate in reverse order
        for index,line in enumerate(reversed(lines)):
            if index > 0:
                yield line
    # handle the remaining data at the beginning of the file
    file.seek(0)
    chunk = file.read(size-pos) + last_line
    lines = chunk.splitlines()
    for line in reversed(lines):
        yield line

Pour votre exemple:

s = "foo"
for index, line in enumerate(iter_lines_reversed(fileObj)):
    if line == s:
        print "FOUND"
        break
    Elif index+1 >= 10:
        break

Edit: Maintenant récupère automatiquement la taille du fichier
Edit2: Maintenant, itère seulement pour 10 lignes.

0
Markus Jarderot

Cette solution ne lit le fichier qu'une seule fois, mais en utilisant 2 pointeurs d'objet fichier pour pouvoir obtenir les N dernières lignes du fichier sans le relire:

def getLastLines (path, n):
    # return the las N lines from the file indicated in path

    fp = open(path)
    for i in range(n):
        line = fp.readline()
        if line == '':
            return []

    back = open(path)
    for each in fp:
        back.readline()

    result = []
    for line in back:
        result.append(line[:-1])

    return result




s = "foo"
last_bit = getLastLines(r'C:\Documents and Settings\ricardo.m.reyes\My Documents\desarrollo\tail.py', 10)
for line in last_bit:
    if line == s:
        print "FOUND"
0
Ricardo Reyes

lit les derniers K du fichier et scinde cela en lignes pour ne renvoyer que les 10 derniers.

il est très peu probable que le début de cette partie tombe sur une limite de ligne, mais vous échapperez quand même les premières lignes.

0
Javier

Merci à la solution de 18 Darius Bacon, mais avec une implémentation et une intégration 30% plus rapides dans la classe io.BaseIO.

class ReverseFile(io.IOBase):
    def __init__ (self, filename, headers=1):
        self.fp = open(filename)
        self.headers = headers
        self.reverse = self.reversed_lines()
        self.end_position = -1
        self.current_position = -1

    def readline(self, size=-1):
        if self.headers > 0:
            self.headers -= 1
            raw = self.fp.readline(size)
            self.end_position = self.fp.tell()
            return raw

        raw = next(self.reverse)
        if self.current_position > self.end_position:
            return raw

        raise StopIteration

    def reversed_lines(self):
        """Generate the lines of file in reverse order.
        """
        part = ''
        for block in self.reversed_blocks():
            block = block + part
            block = block.split('\n')
            block.reverse()
            part = block.pop()
            if block[0] == '':
                block.pop(0)

            for line in block:
                yield line + '\n'

        if part:
            yield part

    def reversed_blocks(self, blocksize=0xFFFF):
        "Generate blocks of file's contents in reverse order."
        file = self.fp
        file.seek(0, os.SEEK_END)
        here = file.tell()
        while 0 < here:
            delta = min(blocksize, here)
            here -= delta
            file.seek(here, os.SEEK_SET)
            self.current_position = file.tell()
            yield file.read(delta)

Un exemple

rev = ReverseFile(filename)
for i, line in enumerate(rev):
        print("{0}: {1}".format(i, line.strip()))
0
asterio gonzalez

Peut-être que cela pourrait être utile:

import os.path

path = 'path_to_file'
os.system('tail -n1 ' + path)
0
AM01

Personnellement, je serais tenté de sortir du shell et d'appeler tail -n10 pour charger le fichier. Mais alors je ne suis pas vraiment un programmeur Python;)

0
Gareth