Letzten Zeilen aus Datei lesen.

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
AndiArbeit
User
Beiträge: 21
Registriert: Freitag 11. Juli 2014, 13:26

Hallo liebes Python Forum,

ich möchte aus einer sehr großen Textdatei (<50mb) über Netzwerk die letzten 5 Zeilen der Datei auslesen.

Hierfür ist der readlines() Befehl suboptimal.

Code: Alles auswählen

      client_msg = paramiko.Transport((rechner, 22))
      client_msg.connect(username=login.name, password=login.passwort)
    
      sftp = paramiko.SFTPClient.from_transport(client_msg)
    
      my_file = sftp.open(datei, 'r')
      text = my_file.readlines()
      
      tkMessageBox.showinfo(self.name, text[-5:])
      
      my_file.close()
    
      client_msg.close()
Gibt es eine Möglichkeit von vornherein auschließlich das Ende einer Datei auszulesen?

Gruß,
AndiArbeit
Benutzeravatar
__blackjack__
User
Beiträge: 13185
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Falls `my_file` die `seek()`-Methode relativ zum Ende der Datei unterstützt, könntest Du versuchen abzuschätzen wie viel man mindestens lesen muss damit da die letzten 5 Zeilen bei sind. Und Du müsstest natürlich auch den Fall berücksichtigen wenn Du Dich verschätzt hast.

Edit: Und natürlich auch das es sich vielleicht um ein Encoding mit mehr als einem Byte pro Zeichen handelt, und man beim schätzen der Leseposition die auch mitten in einem Zeichen landen kann das durch mehr als ein Byte kodiert ist.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Sirius3
User
Beiträge: 17791
Registriert: Sonntag 21. Oktober 2012, 17:20

@__blackjack__: zum Glück ist beim am häufigsten eingesetzen Multibyte-Encoding (utf8) klar, ob es sich um ein Anfangsbyte oder ein Folgebyte handelt.

Alternativ könnte man mit ssh auch ein tail-Kommando absetzen.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1030
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Man könnte ungefähr schätzen wie viel Bytes insgesamt die n gewünschten Zeilen zusammen haben.

Code: Alles auswählen

def tail_file(file, n):
    fsize = os.path.getsize(file)
    buffersize = min(fsize, 4096)
    with open(file, 'rb') as fd:
        buffer = bytearray(buffersize)
        fd.seek(-buffersize, 2)
        amount = 0
        while amount < buffersize:
            chunk_size = fd.readinto(buffer)
            if not chunk_size:
                break
            amount += chunk_size
    lines = [chunk.decode() for chunk in buffer.splitlines()]
    return lines[len(lines) - n:]
Der Kernpunkt ist eigentlich nur fd.seek(x, 2). Das zweite Argument gibt an, dass eine relative Position vom Ende aus verwendet wird.
Negative Werte in der relativen Positionierung sind nur möglich, sofern die Datei im binären Modus geöffnet worden ist.
Nutzt man den Text-Modus, funktioniert die negative Rückwärtssuche vom Ende der Datei nicht.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

OT:
Mittels

Code: Alles auswählen

tail -1 wordlist.txt
spuckt mein lahmer RPi1 ratzfatz die letzte Zeile einer Datei, mit 1.5 Mio. Zeilen aus.
https://de.wikipedia.org/wiki/Tail_(Unix).
Ob eine Python-Lösung schneller/langsamer/gleich ist, kann ich aber nicht sagen.
narpfel
User
Beiträge: 646
Registriert: Freitag 20. Oktober 2017, 16:10

@lackschuh: Die einfachste Implementierung von `tail` in Python dürfte in etwa so aussehen:

Code: Alles auswählen

from collections import deque

def tail(path, line_count):
    with open(path) as lines:
        return list(deque(lines, line_count))
Das dürfte aber viel langsamer als `tail` sein.
lackschuh
User
Beiträge: 281
Registriert: Dienstag 8. Mai 2012, 13:40

@narpfel
die DeaD_EyE Funktion ist um ein vielfaches schneller als deine ;)
narpfel
User
Beiträge: 646
Registriert: Freitag 20. Oktober 2017, 16:10

@lackschuh: Die liest ja auch nicht die ganze Datei ein.
Sirius3
User
Beiträge: 17791
Registriert: Sonntag 21. Oktober 2012, 17:20

@DeaD_EyE: es ist davon auszugehen, das `read` immer bis zum Ende der Datei liest. Dein Code berücksichtigt weder, dass die letzten Zeilen länger als 4096 Bytes sein könnten, noch dass Du UTF8-Bytesequenzen zerschneidest. Außerdem dürfte os.path.getsize mit paramiko nicht funktionieren.
Mit den selben Fehlern, aber kürzer:

Code: Alles auswählen

def tail_file(filename, n):
    fsize = os.path.getsize(filename)
    buffersize = min(fsize, 4096)
    with open(filename, 'rb') as fd:
        fd.seek(-buffersize, 2)
        buffer = fd.read()
    lines = buffer.decode().splitlines()
    return lines[-n:]
Benutzeravatar
DeaD_EyE
User
Beiträge: 1030
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Meine ursprüngliche Idee war es, die Datei in Chunks solange rückwärts einzulesen, bis 1. die gewünschte Anzahl der Zeilen erreicht ist und 2. keine Zeilen zerrissen werden.
Die Ausgabe sollte natürlich in richtiger Reihenfolge erfolgen.

tail_file muss lokal auf der Kiste ausgeführt werden.
Das du jetzt mein bytearray einfach so rausgenommen hast, macht mich jetzt ganz traurig :-(
Letztendlich bring es bei der geringen Datenmenge auch keinen Geschwindigkeitsvorteil.


Übrigens kann man Bytes gefahrenlos bei '\n' splitten, auch wenn diese mit UTF-8 kodiert sind.
Wenn du dir den Algorithmus ansiehst, wirst du es verstehen: https://de.wikipedia.org/wiki/UTF-8#Algorithmus
UTF-8 ist mit ASCII deckungsgleich. Sobald ein Zeichen aus mehreren bytes besteht, ist das MSB auf 1 gesetzt.
Da die Quelle der Datei uns unbekannt ist, kann man da nur raten.
Ich rate mal, dass die Ausgabe UTF-8 ist und lediglich '\n' als Newline verwendet wird.

Eine verbesserte Funktion würde natürlich alle Gegebenheiten berücksichtigen und Zeilentrennung erst nach einem decode durchführen.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
snafu
User
Beiträge: 6747
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@AndiArbeit:
Leider gibt es vorab keine Information, wieviele Zeilen eine Datei hat oder wo die Zeilenumbrüche stecken. Das Betriebssystem liefert sowas nicht. Also muss dein Programm sich zwangsläufig jede Zeile ansehen und überschüssige Zeilen wegwerfen, was bei großen Dateien relativ zeitaufwändig sein kann. Oder man geht - wie gezeigt - das Risiko, dass man zu einer Stelle springt, die vermutlich noch vor den letzten gewünschten Zeilen kommt und spart dadurch Zeit, weil man mitunter jede Menge Zeilen überspringen kann. Mit einer festen Positionsangabe würde ich dann aber nicht arbeiten. Eher mit einer maximalen Zeilenlänge (z.B. maximal 120 Zeichen pro Zeile) und die dann mit der Zeilenzahl multiplizieren, um daraus die Position zu berechnen. Das hängt aber stark von der vorliegenden Datei ab. Generiertes HTML zum Beispiel kann manchmal ziemlich lange Zeilen haben und 120 könnte dann viel zu wenig sein.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1030
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Ganz andere Idee: Lass doch ein Programm mitlaufen (auf dem Server selbst), dass die entsprechenden Zeilen filtert und getrennt zusätzlich in eine andere Datei ausgibt.
Das Format kann ja komplett identisch sein.

Ein Möglichkeit wäre es mit inotify die Logdatei zu überwachen.
Sobald eine neue Zeile geloggt wird, wird dann dein Programm aktiv und macht nur etwas, wenn die neue Zeile das beinhaltet, wonach du suchst.

Könnte dann so aussehen:

Code: Alles auswählen

import inotify.adapters


def filter_log(logfile, filter_func, encoding='utf-8'):
    last_size = os.path.getsize(logfile)
    notify = inotify.adapters.Inotify()
    notify.add_watch(logfile)
    for ev, ev_type, path, filename in notify.event_gen(yield_nones=False):
        if ev_type == ['IN_MODIFY']:
            with open(logfile, 'rb') as log:
                log.seek(last_size)
                new_data = log.read().decode(encoding)
            last_size = os.path.getsize(logfile)
            if filter_func(new_data):
                 yield new_data
Den Code kannst du ja zuvor mit der Datei /tmp/test ausprobieren.

Code: Alles auswählen

for data in filter_log('/tmp/test', lambda x: True):
    print(data, end='')
Dann in der Shell:

Code: Alles auswählen

echo -e "123" >> /tmp/test
echo -e "456" >> /tmp/test
echo -e "789" >> /tmp/test
Anstatt "lambda x: True" übergibst du deine eigene Filterfunktion.
Vielleicht lässt sich aber diese ganze Aufgabe auch deinem Loggingsystem zuweisen.
Über Regeln kann man da auch bestimmt einiges erreichen.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Antworten