32 votes

Quelle est la méthode la plus efficace pour rechercher les X dernières lignes d'un fichier ?

J'ai un dossier et je ne sais pas quelle sera sa taille (il pourrait être très grand, mais la taille sera très variable). Je veux rechercher les 10 dernières lignes environ pour voir si l'une d'entre elles correspond à une chaîne de caractères. Je dois faire cela 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"

2 votes

37voto

PabloG Points 9308
# 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

1 votes

Le "if len(l) < 10" est redondant. "print l[:-10]" gère ce cas.

0 votes

@Darius : Je voulais dire si len(l) > 10, corrigé

2 votes

Lignes[:-10] affiche les 10 dernières lignes. Ce que vous voulez, c'est lignes[-10 :].

35voto

Darius Bacon Points 9741

Voici une réponse semblable à celle de MizardX, mais sans le problème apparent de prendre un temps quadratique dans le pire des cas à cause de la recherche répétée de nouvelles lignes dans la chaîne de travail au fur et à mesure que des morceaux sont ajoutés.

Par rapport à la solution Active State (qui semble également quadratique), cette solution n'explose pas dans le cas d'un fichier vide et effectue une recherche par bloc lu au lieu de deux.

Par rapport à la "queue" de frai, il s'agit d'un phénomène autonome. (Mais "tail" est préférable si vous l'avez déjà).

Par rapport à la prise de quelques kB à la fin et à l'espoir que cela suffise, cette méthode fonctionne pour n'importe quelle 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)

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

Edita: remplacement de map() par itertools.imap() dans head(). Editer 2 : simplifié reversed_blocks(). Editer 3 : éviter d'analyser à nouveau la queue pour les nouvelles lignes. Editer 4 : a réécrit reversed_lines() parce que str.splitlines() ignore un ' final. \n comme l'a remarqué BrianB (merci).

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

0 votes

Très bien - j'ai lu la liste des réponses jusqu'à ce que j'arrive ici, sachant que la meilleure serait celle qui serait assez intelligente pour utiliser l'application yield directive

0 votes

Correction d'un cas particulier : parfois un bloc se termine par une nouvelle ligne, donc la queue est sa propre entrée.

0 votes

@BrianB, merci - pouvez-vous donner un cas de test où mon code est cassé ? J'ai inversé votre changement parce qu'il a échoué sur la première chose que j'ai essayée, ' \nhello\n\nworld\n (avec une taille de bloc fixée à 2). (Mes remerciements ne sont pas ironiques car je m'attends à ce que vous ayez remarqué un cas réel où mon code a échoué).

8voto

Myrddin Emrys Points 7261

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

0 votes

Je serais prudent, car les appels au shell sont beaucoup plus coûteux qu'un accès direct.

1 votes

Cette question est assez ancienne, mais je ne préconisais pas un appel à l'obus. Je recommandais d'appeler le script avec la sortie pipée de tail, plutôt que d'appeler le script pour lire tout le fichier lui-même.

8voto

Ryan Ginstrom Points 8354

Je pense que la lecture des 2 derniers Ko du fichier devrait permettre d'obtenir 10 lignes et ne devrait pas être trop gourmande en 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)

0 votes

Vous devriez cependant vérifier que le fichier est >= 2KB.

5voto

mhawke Points 10385

Voici une version utilisant mmap Cela semble assez efficace. Le grand avantage est que mmap gérera automatiquement pour vous les besoins de pagination du fichier vers la mémoire.

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]

1 votes

C'est très bien ! J'aurais dû penser à mmap. Cela va un ordre de grandeur plus lentement que le mien sur mon test d'un très gros fichier d'une ligne, cependant, je suppose que c'est parce qu'il vérifie caractère par caractère dans le code Python.

0 votes

Oui, j'étais également préoccupé par la boucle "pure Python". La boucle pourrait éventuellement être rendue plus efficace que le code que j'ai fourni. Si l'objet mmap avait une méthode rfind(), cela aurait pu être bien mieux !

0 votes

Pour information : les objets mmap de Python v2.6.5 ont une fonction rfind() méthode.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X