it-swarm.com.de

Die effizienteste Möglichkeit, die letzten x Zeilen einer Datei in Python zu suchen

Ich habe eine Datei und weiß nicht, wie groß sie sein wird (sie kann ziemlich groß sein, aber die Größe variiert stark). Ich möchte die letzten 10 Zeilen durchsuchen, um zu sehen, ob eine von ihnen mit einer Zeichenfolge übereinstimmt. Ich muss das so schnell und effizient wie möglich tun und habe mich gefragt, ob es etwas Besseres gibt als:

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

Hier ist eine Antwort wie bei MizardX, jedoch ohne das offensichtliche Problem, im schlimmsten Fall quadratische Zeit in Anspruch zu nehmen, indem der Arbeitsstring wiederholt nach neuen Zeilen durchsucht wird, wenn Brocken hinzugefügt werden.

Verglichen mit der Activestate-Lösung (die auch quadratisch zu sein scheint), bläst dies bei einer leeren Datei nicht auf und es wird ein Lesezugriff pro Block anstelle von zwei gesucht.

Verglichen mit dem Laichen von "Schwanz" ist dies in sich abgeschlossen. (Aber "Schwanz" ist am besten, wenn Sie es haben.)

Verglichen damit, ein paar kB vom Ende zu nehmen und zu hoffen, dass es ausreicht, funktioniert dies für jede Leitungslänge.

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)

Um es wie gewünscht zu verwenden:

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

Edit: map () in itertools.imap () in head () geändert. Edit 2: vereinfachte reversed_blocks (). Bearbeiten Sie 3: vermeiden Sie das erneute Scannen von Zeilenumbrüchen. Edit 4: schrieb reversed_lines () um, weil str.splitlines () ein abschließendes '\ n' ignoriert, wie BrianB bemerkt hat (Danke).

Beachten Sie, dass in sehr alten Python-Versionen die Zeichenfolgenverkettung in einer Schleife hier quadratisch dauert. CPython aus mindestens den letzten Jahren vermeidet dieses Problem automatisch.

33
Darius Bacon

Wenn Sie Python auf einem POSIX-System ausführen, können Sie 'tail -10' verwenden, um die letzten Zeilen abzurufen. Dies ist möglicherweise schneller als das Schreiben Ihres eigenen Python-Codes, um die letzten 10 Zeilen zu erhalten. Öffnen Sie die Datei nicht direkt, sondern öffnen Sie eine Pipe über den Befehl 'tail -10 filename'. Wenn Sie sich jedoch der Protokollausgabe sicher sind (zum Beispiel wissen Sie, dass es never sehr lange Zeilen gibt, die hunderte oder tausende von Zeichen lang sind), dann würde die Verwendung einer der "Read the last 2KB" -Ansätze verwendet sei gut.

8
Myrddin Emrys

Wenn Sie die letzten 2 KB der Datei lesen, sollten Sie sicher sein, dass Sie 10 Zeilen erhalten und nicht zu viel Ressourcen benötigen.

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

Hier ist eine Version mit mmap, die ziemlich effizient erscheint. Der große Vorteil ist, dass mmap die Auslagerungsvoraussetzungen für Dateien automatisch für Sie übernimmt.

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

Ich stieß auf dieses Problem, analysierte die letzten Stunden der großen Syslog-Dateien und nutzte diese Funktion von Activestates Rezeptseite ... ( 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)

Es funktioniert wirklich gut und ist viel effizienter als alles andere als fileObj.readlines () [- 10:], wodurch Python die gesamte Datei in den Speicher liest und dann die letzten zehn Zeilen abschneidet.

2
user32716

Wenn Sie sich in einer Unix-Box befinden, ist os.popen("tail -10 " + filepath).readlines() wahrscheinlich der schnellste Weg. Ansonsten hängt es davon ab, wie robust Sie es haben möchten. Die bisher vorgeschlagenen Methoden werden alle auf die eine oder andere Weise herunterfallen. Für Robustheit und Geschwindigkeit in den meisten Fällen möchten Sie wahrscheinlich etwas wie eine logarithmische Suche: Verwenden Sie file.seek, um zum Ende der Datei abzüglich 1000 Zeichen zu gelangen, lesen Sie sie ein, prüfen Sie, wie viele Zeilen sie enthält, und dann EOF minus 3000 Zeichen, lesen Sie 2000 Zeichen, zählen Sie die Zeilen, dann EOF minus 7000, lesen Sie 4000 Zeichen ein, zählen Sie die Zeilen usw., bis Sie so viele Zeilen haben, wie Sie benötigen. Wenn Sie jedoch sicher sind, dass Dateien immer mit vernünftigen Leitungslängen ausgeführt werden, benötigen Sie dies möglicherweise nicht.

Einige Anregungen finden Sie auch im source code für den Unix-Befehl tail.

2
Alex Coventry

Ich glaube, ich erinnere mich, dass ich den Code von diesem Blogpost von Manu Garg angepasst habe, als ich etwas Ähnliches machen musste.

2
Daryl Spitzer

Ich nahm mhawkes Vorschlag zur Verwendung von mmap und schrieb eine Version, die rfind verwendet:

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

Sie können die Zeilen auch zählen, wenn Sie die Datei durchlaufen, anstatt einen Byte-Versatz zu erraten. 

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)

Nun ist die Datei in einer guten Position, um readlines() auszuführen. Sie können auch die Strings zwischenspeichern, die Sie beim ersten Mal lesen, um zu vermeiden, dass derselbe Teil der Datei zweimal gelesen wird.

1
JimB

Sie könnten etwa 1000 Byte große Blöcke vom Ende der Datei in einen Puffer einlesen, bis Sie 10 Zeilen haben.

1
Robert Gamble

Zuerst eine Funktion, die eine Liste zurückgibt:

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

Zweitens eine Funktion, die die Zeilen in umgekehrter Reihenfolge durchläuft:

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

Für dein Beispiel:

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

Bearbeiten: Ermittelt jetzt automatisch die Dateigröße
Edit2: Iteriert jetzt nur noch für 10 Zeilen.

0
Markus Jarderot

Diese Lösung liest die Datei nur einmal, verwendet jedoch zwei Objektobjektzeiger, um die letzten N Zeilen der Datei zu erhalten, ohne sie erneut lesen zu müssen:

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

lesen Sie die letzten Ks der Datei und teilen Sie diese in Zeilen auf, um nur die letzten 10 zurückzugeben.

es ist ziemlich unwahrscheinlich, dass der Anfang dieses Blocks auf eine Liniengrenze fällt, aber Sie werden die ersten Zeilen trotzdem verwerfen.

0
Javier

Dank der Lösung von 18 Darius Bacon, aber mit einer um 30% schnelleren Implementierung und dem Einbetten in die Klasse 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)

Ein Beispiel

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

Vielleicht könnte dies nützlich sein:

import os.path

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

Ich persönlich wäre in der Versuchung, zur Shell aufzubrechen und tail -n10 anzurufen, um die Datei zu laden. Aber dann bin ich nicht wirklich ein Python-Programmierer;)

0
Gareth