FTPLib aktuelle Downloadgeschwindigkeit

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
CerealGuy
User
Beiträge: 2
Registriert: Donnerstag 30. Mai 2013, 21:28

Code: Alles auswählen

import ftplib
import time

class ftp_transfer_callback_handler():
    def __init__(self, filepath, callback = None, encoding = None):
        """
        Init class, if u want encoding set it :D
        Encoding just for blocksize, not for writing.
        """
        self.file = open(filepath, "wb")
        self.filepath = filepath
        self.callback = callback
        self.encoding = encoding
        self.status_starttime = time.time()
        self.status_totaltransfertime = 0
        self.status_downloadedbytes = 0

    def status(self):
        return [self.status_starttime, self.status_totaltransfertime, self.status_downloadedbytes]
        
    def transfer_callback(self, callback_data):
        """
        Let the ftplib call this method
        """
        self.file.write(callback_data)
        timenow = time.time()
        #print type(callback_data)
        if self.encoding == None:
            blocksize = len(callback_data)
        else:
            blocksize = len(callback_data.encode(encoding))
        self.status_downloadedbytes += blocksize
        self.status_totaltransfertime = timenow - self.status_starttime
        if self.callback != None:
            self.callback([self.status(),timenow, blocksize, self.encoding])

def test(status_data):
    print status_data[0][2]

if __name__ == "__main__":
    ftp = ftplib.FTP("ftp.de.debian.org")
    ftp.login("anonymous", "pythontest")
    ftp.cwd("/debian-archive/debian-amd64/pool/contrib/c/crafty-books-medtosmall")
    file_transfer = ftp_transfer_callback_handler("crafty-books-medtosmall_1.0-2_all.deb", callback = test) #Init callback_handler 
    ftp.retrbinary("RETR crafty-books-medtosmall_1.0-2_all.deb", file_transfer.transfer_callback)

Erst mal ein kleines stückchen code, und dann zu meinem Problem.
Im Grunde sind es mehrere Probleme.
Problem #1:
Ich will mit der klasse oben (ftp_transfer_callback_handler) den aktuellen status des file transfers an einen weiteren callback weitergeben, um später zBsp die aktuelle download geschwindigkeit und den fortschritt anzeigen zu können. Alles dreht sich irgendwie um die aktuell übertragenen bytes, welche ich via len(callback_data) herausfinde, in dann zu den vorherigen addiere. Das Problem dabei ist jedoch, dass ich damit ja schaue wie lange der string ist, also wieviele zeichen es sind, nicht aber wieviele bytes. Oftmals klappt das irgendwie mit dem len(str), aber hin und wieder dann doch nicht. Das lässt sich dann irgendwie mit dem Encoding ausbügeln, das ist mir aber zu wischiwaschi. Ist es vllt möglich, irgendwie herauszufinden, wie der string "decoded" ist, und ihn dann entsprechend zu encoden, um schlussendlich dann die richtigen bytes zu bekommen, oder gehts vllt anderst einfacher?

Problem #2:
Aktuellen downloadspeed anzeigen, also zBsp (k/m)bytes/S. Ich weiß nicht so genau wie ich das machen soll, da ich ja irgendwie herausfinden muss, wieviele bytes in einer sekunde durchgegangen sind. Stehe da auf dem Schlauch, wäre nett wenn man mir da helfen könnte.
BlackJack

@CerealGuy: Zeichenketten enthalten in Python 2.x Bytes, dass heisst ein `len()` gibt Dir immer die Anzahl der Bytes. Ich weiss nicht was Du da mit dem `encoding` eigentlich anzustellen versuchst.

So komische Namenszusätze wie `_handler` versuche ich mir in Python wenn es geht zu sparen. Du kannst die Klasse beispielsweise `FtpTransferCallback` nennen und die `transfer_callback()` in `__call__()` umbenennen. Dann kann man das Exemplar der Klasse als aufrufbares Objekt direkt übergeben.

Für die Geschwindigkeitsmessung müsstest Du Dir die aktuelle Zeit vom jeweils letzten Aufruf merken. Beim nächsten ermittelst Du die aktuelle Zeit und zusammen mit der Länge der Daten lässt sich dann ja ganz einfach ausrechnen wie viele Daten pro Zeit das waren.
CerealGuy
User
Beiträge: 2
Registriert: Donnerstag 30. Mai 2013, 21:28

BlackJack hat geschrieben:@CerealGuy: Zeichenketten enthalten in Python 2.x Bytes, dass heisst ein `len()` gibt Dir immer die Anzahl der Bytes. Ich weiss nicht was Du da mit dem `encoding` eigentlich anzustellen versuchst.

So komische Namenszusätze wie `_handler` versuche ich mir in Python wenn es geht zu sparen. Du kannst die Klasse beispielsweise `FtpTransferCallback` nennen und die `transfer_callback()` in `__call__()` umbenennen. Dann kann man das Exemplar der Klasse als aufrufbares Objekt direkt übergeben.

Für die Geschwindigkeitsmessung müsstest Du Dir die aktuelle Zeit vom jeweils letzten Aufruf merken. Beim nächsten ermittelst Du die aktuelle Zeit und zusammen mit der Länge der Daten lässt sich dann ja ganz einfach ausrechnen wie viele Daten pro Zeit das waren.
Vielen dank, auf sowas habe ich gehofft! Ich habe das ganze schonmal gehabt, aber dort kam ich irgendwie nicht auf das richtige. Also encoding raus und len einfach lassen. Vielen dank! Ich werde später den code nochmal hier reineditieren, denke das problem haben vllt mehrere.

Code: Alles auswählen

import ftplib
import time

class FtpDownloadTransferCallback():
    def __init__(self, filepath, callback = None):
        """
        Set relative filetpath (with filename) and if you want callback to another
        function which should handle status of transfer.
        """
        self.file = open(filepath, "wb")
        self.filepath = filepath
        self.callback = callback
        self.status_starttime = time.time()
        self.status_totaltransfertime = 0
        self.status_downloadedbytes = 0
        self.last_callback = 0

    def status(self, timenow, blocksize):
        """
        0  -  Download Start time
        1  -  Total Transfertime (timenow - downloadstarttime)
        2  -  Total transfered bytes
        3  -  last callback time
        4  -  starttime of this callback
        5  -  blocksize
        """
        return [self.status_starttime, self.status_totaltransfertime, self.status_downloadedbytes, self.last_callback, timenow, blocksize]
        
    def __call__(self, callback_data):
        """
        Let the ftplib call this method
        """
        self.file.write(callback_data)
        timenow = time.time()
        blocksize = len(callback_data)


        # Status update
        self.status_downloadedbytes += blocksize
        self.status_totaltransfertime = timenow - self.status_starttime

        #Callback
        if self.callback != None:
            self.callback(self.status(timenow, blocksize))

        self.last_callback = timenow

def test(status_data):
    
    print status_data[2], (1 / (status_data[4] - status_data[3])) * status_data[5] / 1024

if __name__ == "__main__":
    ftp = ftplib.FTP("ftp.de.debian.org")
    ftp.login("anonymous", "pythontest")
    ftp.cwd("/debian-archive/debian-amd64/pool/contrib/c/crafty-books-medtosmall")
    file_transfer = FtpDownloadTransferCallback("crafty-books-medtosmall_1.0-2_all.deb", callback = test) #Init callback_handler 
    ftp.retrbinary("RETR crafty-books-medtosmall_1.0-2_all.deb", file_transfer)
So klappt das aber immernoch nicht ganz mit dem download speed. 1 durch die Differenz zwischen dieser callback zeit und der letzten mal der aktuellen blocksize geht zwar in die richtige richung, ist aber doch ungenau, vorallem bei dem letzten block, welcher ja meistens der kleinste ist. Das heißt ich brauche einen längere zeit (~1 sekunde), welche ich dann halt irgendwie den durchschnitt errechne. Morgen mal bisschen weiter den kopf drüber zermattern :D da könnte ja ne kleine if abfrage helfen. If differenz >= 1:.... morgen ist auch noch ein tag ^^ Ich teste und editiere dann ;) Danke schonmal!
BlackJack

@CerealGuy: Was ist denn das Problem mit der Geschwindigkeitsberechnung? Klar ist der letzte Block oft kleiner, aber dafür ist der Zeitabstand beim letzten Block doch auch kleiner. Die Formel stimmt zwar, aber ich hätte sie anders aufgeschrieben, so dass man das Menge pro Zeit besser sehen kann. So etwas wie ``(status.bytes / 1024.0) / (status.timestamp - status.previous_timestamp)``.

Womit wir beim Thema Rückgabewert von der `status()`-Methode wären: So eine Sequenz mit magischen Indexzahlen ist schwer zu lesen, fehleranfällig und nicht gut veränderbar. Da würde ich mindestens ein `collections.namedtupel` verwenden, oder gleich eine eigene Klasse für schreiben. In der könnte man dann auch das berechnen von der Downloadgeschwindigkeit unterbringen.

Das lokale Dateiobjekt wird ungünstig gehandhabt. Die Datei wird nirgends geschlossen, und dort wo sie geöffnet wird, lässt sich nicht wirklich schön „symmetrisch” eine Operation zum Schliessen unterbringen. Ich würde `FtpDownloadTransferCallback` in der `__init__()` statt eines Dateinamens eher ein Dateiobjekt übergeben. Dann kann man zum einen ohne zusätzlichen Aufwand ``with`` benutzen und zum anderen könnte man auch in andere „dateiähnliche” Objekte speichern lassen.

Code: Alles auswählen

    filename = 'crafty-books-medtosmall_1.0-2_all.deb'
    with open(filename, 'wb') as save_file:
        ftp.retrbinary(
            'RETR ' + filename,
            FtpDownloadTransferCallback(save_file, callback=test)
        )
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Hallo,

ich klinke mich hier mal ein, wenn das kein Problem ist.

Ich möchte auch die aktuelle Downloadgeschwindigkeit haben, arbeite allerdings mit Paramiko und ohne Klassen.

Mein Code sieht so aus:

Code: Alles auswählen

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from __future__ import division, print_function

import os
import sys
import time

try:
    import paramiko
except ImportError:
    print("Bitte das Modul paramiko installieren.")
    sys.exit(1)

try:
    import pyperclip
except ImportError:
    print("Bitte die Datei pyperclip.py in den Ordner kopieren.\nhttp://coffeeghost.net/src/pyperclip.py")
    sys.exit(1)
    
if sys.platform == "win32":
    windows = True
else:
    windows = False

#Verbindungsdaten
hostname = "192.168.1.10"
port = 22
benutzername = "pi"
passwort = "raspberry"
entferntes_verzeichnis = "/home/pi/uploads/"
hinzufuegen_skript = "/home/pi/hinzufuegen.py"
aufrufverzeichnis = "http://192.168.0.10/" #z.B. als Webserver-Adresse.


def prozentanzeige(bytes_uebertragen, bytes_gesamt): 
    global letzter_aufruf
    global letzte_bytes
    
    bytes_differenz = bytes_uebertragen - letzte_bytes
    zeit_differenz = time.time() - letzter_aufruf
    geschwindigkeit = (bytes_differenz / zeit_differenz) / 1024

    prozent = (bytes_uebertragen / bytes_gesamt) * 100
    sys.stdout.write("\r{0:.1f} % übertragen. Geschwindigkeit: {1:.0f} KiB/s".format(prozent, geschwindigkeit))
    sys.stdout.flush()
    letzter_aufruf = time.time()
    letzte_bytes = bytes_uebertragen
    
def zeit():
    return int(time.time())
    
def groesse_lesbar(dateipfad):
    dateigroesse = os.path.getsize(dateipfad)

    if dateigroesse < 1024:
            return "{0} B".format(dateigroesse)
    elif dateigroesse < (1024*1024):
            return"{0:.2f} KiB".format(dateigroesse/1024)
    elif dateigroesse < (1024*1024*1024):
            return "{0:.2f} MiB".format(dateigroesse/(1024*1024))
    else:
            return "{0:.2f} GiB".format(dateigroesse/(1024*1024*1024))



def hochladen(dateipfad, ablaufzeit):
    dateiname = os.path.split(dateipfad)[1]
    
    print("Verbinde mit Server {0}...".format(hostname))
    paramiko.util.log_to_file("paramiko.log")
    sshclient = paramiko.SSHClient()
    sshclient.set_missing_host_key_policy(paramiko.WarningPolicy())
    sshclient.load_system_host_keys()
    sshclient.connect(hostname, username = benutzername, password=passwort)
    
    #Gucke ob Datei schon existiert
    try:
        sftpverbindung = sshclient.open_sftp()
        sftpverbindung.stat("{0}{1}".format(entferntes_verzeichnis, dateiname))
        #TODO: os.path.join benutzen, das soll dann aber auch unter Windows funktionieren.
        print("Datei existiert bereits, breche ab.")
        #TODO: Fragen ob gelöscht werden soll.
        sys.exit(1)
    except IOError:
        pass
    
    #Datei hochladen
    print("Lade Datei {0} ({1}) hoch...".format(dateiname, groesse_lesbar(dateipfad)))
    zeit_vorher = zeit()
    sftpverbindung = sshclient.open_sftp()
    
    global letzter_aufruf
    global letzte_bytes
    
    letzter_aufruf = time.time()
    letzte_bytes = 0
    sftpverbindung.put(dateipfad, "{0}{1}".format(entferntes_verzeichnis, dateiname),  callback=prozentanzeige)
    sys.stdout.write("\n")
    
    zeitdauer = zeit() - zeit_vorher
    if zeitdauer == 0: 
        zeitdauer = 1
    geschwindigkeit = os.path.getsize(dateipfad) / zeitdauer
    if geschwindigkeit < 1024:
        geschwindigkeit = "{0:.0f} B/s".format(geschwindigkeit)
    elif geschwindigkeit < 1024*1024:
        geschwindigkeit = "{0:.0f} KiB/s".format(geschwindigkeit/1024)
    else:
        geschwindigkeit = "{0:.0f} MiB/s".format(geschwindigkeit/(1024*1024))
    
    print("Datei in {0} Sekunden hochgeladen ({1})".format(zeitdauer, geschwindigkeit))
    
    #In Datenbank eintragen.
    sshclient.exec_command("{0} {1}{2} {3}".format(hinzufuegen_skript, entferntes_verzeichnis, dateiname, ablaufzeit))
    
    #in Zwischenablage speichern
    pyperclip.copy("{0}{1}".format(aufrufverzeichnis, dateiname))

if __name__ == "__main__":
    if len(sys.argv) == 1:
        dateipfad = raw_input("Keine Datei angegeben, bitte angeben: ")
    else:
        dateipfad = sys.argv[1]
    
    ablauf = "1d"
    ablauf = raw_input("Wann soll die Datei ablaufen? (z.B. 1d, 5h) Leer für 1 Tag.")
    
    while True:
        frage = raw_input("Soll die Datei {0} hochgeladen werden? [J/N] ".format(dateipfad))
        if frage == "j" or frage == "ja":
            break
        elif frage == "n" or frage == "nein":
            sys.exit(0)
        else:
            print("Bitte nur j oder n eintippen.")
    
    hochladen(dateipfad, ablauf)
    
    #Damit das Fenster geöffnet bleibt.
    if windows: input()
Ich kriege hier auch eine Anzeige, aber irgendwie scheint die nicht ganz zu stimmen. Die Ausgabe von dem Skript ist dann:

Code: Alles auswählen

Lade Datei test.mp3 (5.23 MB) hoch...
100 % übertragen. Geschwindigkeit: 10289 KiB/s
Datei in 5 Sekunden hochgeladen (1 MiB/s)


Die 2. Zeile wechselt halt immer bei jedem Aufruf so dass diese 10289 KiB/s auch immer ein wenig schwanken. Allgemein ist die Anzeige aber wesentlich zu hoch. Ich habe ein wenig das Gefühl, dass das time.time() wahrscheinlich zu undeutlich ist und so die Berechnung nicht korrekt ist. Außerdem wird die Anzeige bei jedem Callback-Aufruf aktualisiert, so dass sie praktisch nicht lesbar ist. Kann man das wohl irgendwie machen, dass sich die Geschwindigkeitsanzeige nur bspweise jede Sekunde verändert? Dann würde wahrscheinlich auch die Geschwindigkeit etwas genauer werden, oder?

Danke!


(Ja, wie im anderen Thread angemerkt, werde ich die schlechten if-Abfragen noch ersetzen :D)
BlackJack

@Hellstorm: Du solltest mit Klassen arbeiten. Denn bevor das ``global`` da nicht verschwunden ist, braucht man IMHO über den Rest gar nicht reden.
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Hm, hier muss ich also mit Klassen arbeiten? Hatte mir nach meinem letzten (gescheiterten) Programmieranlauf nämlich gedacht, dass OOP für mich noch etwas zu kompliziert ist, und ich es deswegen erst mal etwas simpler probieren wollte.
BlackJack

@Hellstorm: Das Problem hier ist, dass das Problem offenbar nicht mehr simpel genug ist wenn man ``global`` verwenden muss. Wenn man Zustände über Funktionsaufrufe hinweg behalten will/muss, dann muss man entweder diesen Zustand immer als Argument übergeben und den (möglicherweise) geänderten Zustand als Rückgabewert zurück geben, oder eben mit Klassen arbeiten. Beides ist besser und sauberer als globalen Zustand einzuführen, und Klassen sind in einer objektorientierten Programmiersprache „natürlicher” als die der funktionale Ansatz.
Antworten