Table des matières

Télécharger un fichier sur le web

Objectif

On va présenter ici un petit programme PyQt4 du genre “démo” pour télécharger un fichier du web et l'enregistrer sur disque, avec barre de progression et possibilité d'arrêt du téléchargement avant la fin.

Les différents problèmes résolus dans ce code seront principalement:

Code commenté

Le thread de téléchargement

Comme dit plus haut, ce thread est nécessaire pour que l'interface graphique ne soit pas figée pendant toute la durée du téléchargement.

On utilise ici le thread de PyQt4: QtCore.QThread. Son utilisation est quasi identique au Thread du module threading habituel du Python de base.

La méthode la plus sûre est de créer une classe qui hérite de QtCore.QThread. Elle est appelée ici “Telecharger”. Sa structure est classique:

class Telecharger(QtCore.QThread):
 
    def __init__(self, source, destination, parent=None):
        super(Telecharger,self).__init__(parent)
        #
        # ici, l'initialisation du thread
        #
 
    def run(self):
        #
        # ici, la partie qui s'exécute en même temps que l'interface graphique
        #

Et le lancement du thread, à partir de la fenêtre graphique est, là aussi, très classique:

self.telech = Telecharger(source, destination)
self.telech.start()

Bien sûr, il ne faut pas de méthode 'join()', puisque l'interface graphique doit pouvoir reprendre la main tout de suite!

Le téléchargement

Le téléchargement lui-même est lancé par:

filename, msg = urllib.urlretrieve(self.source, self.destination, reporthook=self.infotelech)

Et la méthode du thread 'infotelech' recevra 3 arguments:

Donc, le pourcentage téléchargé sera calculé par: telechbloc*taillebloc/totalblocs*100

A noter que, si la taille totale n'est pas connue, totalblocs=-1.

La communication par message entre le thread et la fenêtre graphique

Le thread ne doit jamais toucher à la partie graphique! Il faut donc que ce thread donne des infos exclusivement par messages à la partie graphique, et ceci pour 2 type d'évènements:

Cette communication par message se fait de la façon suivante:

juste avant le lancement du thread par start(), on prépare la fenêtre graphique à recevoir des messages de la part du thread, et à lancer une méthode à chaque fois:

self.connect(self.telech, QtCore.SIGNAL("infotelech(PyQt_PyObject)"), self.infotelech)
self.connect(self.telech, QtCore.SIGNAL("fintelech(PyQt_PyObject)"), self.fintelech)

à noter que:

Les 2 méthodes qui seront ainsi lancées à chaque réception de message seront:

Et, au sein du thread, les 2 messages seront émis par:

# pour transmettre les infos de progression du téléchargement:
self.emit(QtCore.SIGNAL("infotelech(PyQt_PyObject)"), [telechbloc, taillebloc, totalblocs])
 
# pour donner l'info de fin du téléchargement:
self.emit(QtCore.SIGNAL("fintelech(PyQt_PyObject)"), messagefin)

La barre de progression

La barre graphique de progression est définie par:

self.barre = QtGui.QProgressBar(self)
self.barre.setRange(0, 100)
self.barre.setValue(0)

Sa mise à jour des infos de progression sera donc:

p = int(telechbloc*taillebloc/totalblocs*100) # pourcentage
self.barre.setValue(p)

Petite particularité: certains téléchargements ne permettent pas de connaitre la taille totale: on ne peut donc pas calculer une progression! Dans ce cas, la barre de progression affiche une simple chenille qui signale simplement que le téléchargement est en cours. On fait cette chenille en fixant un minimum et un maximum tous les 2 égaux à zéro:

self.barre.reset()
self.barre.setRange(0, 0)

Avec le code présenté, la progression de la barre est un peu saccadée. Cela vient de la grande quantité de messages échangés (un par bloc téléchargé). On pourrait améliorer de la façon suivante: le calcul du pourcentage est fait dans le thread, et le message n'est émis que si ce pourcentage est supérieur au pourcentage précédent! Cela complique un peu le code, mais cela rend l'avancement de la barre très progressive.

L'arrêt du téléchargement avant la fin

On va profiter que la fonction du téléchargement appelle une méthode pour lui transmettre les informations de progression, pour placer dans cette méthode le test d'une variable, et si nécessaire un raise qui fera échouer le téléchargement:

if self.stop:
    raise Abort

Pour faire propre, on a ici créé une exception spécifique appelée Abort:

class Abort(Exception):
    pass

Et, bien sûr, la fonction de téléchargement est donc dans un try: except Abort: dans la méthode run du thread. La fin de la méthode run termine le thread.

Diverses précautions supplémentaires

Comme c'est un code simplifié, de nombreuses vérifications ne sont pas faites. Il y en a cependant plusieurs, liées au fait que l'utilisateur devrait pouvoir cliquer sur n'importe quoi sans que des dysfonctionnements graves apparaissent:

Pour le dernier point, on le fait en surchargeant la méthode closeEvent de la classe QtGui.QWidget, qui est déclenchée quelque soit la méthode de fermeture choisie (y compris: croix en haut de la fenêtre ou le “fermer” du petit menu système de la fenêtre).

Code complet

Voilà le code complet, ici en Python 2.7, que vous pouvez essayer en copier-coller. Il est multiplateforme (au moins Windows-Linux). Vous devez avoir installé avant PyQt4.

Pour essayer, vous pouvez utiliser le téléchargement de la doc de Python (et la consulter ne fera pas de mal… :-D ):


#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
# Python 2.7
 
import sys, os
import urllib
from PyQt4 import QtCore, QtGui
 
#############################################################################
class Abort(Exception):
    """classe d'exception crée pour l'arrêt du téléchargement avant la fin"""
    pass
 
#############################################################################
class Telecharger(QtCore.QThread):
    """Thread de téléchargement"""
 
    #========================================================================
    def __init__(self, source, destination, parent=None):
        super(Telecharger,self).__init__(parent)
        self.source = source
        self.destination = destination
        self.stop = False
 
    #========================================================================
    def run(self):
        # lancement du téléchargement
        try:
            filename, msg = urllib.urlretrieve(self.source, self.destination, 
                                                     reporthook=self.infotelech)
            messagefin = u"Téléchargement terminé\n\n" + unicode(msg)
        except Abort:
            messagefin = u"Téléchargement avorté"
        # fin du thread: émission du message de fin
        self.emit(QtCore.SIGNAL("fintelech(PyQt_PyObject)"), messagefin)
 
    #========================================================================
    def infotelech(self, telechbloc, taillebloc, totalblocs):
        """reçoit les infos de progression du téléchargement"""
        # nécessaire pour stopper le téléchargement avant la fin
        if self.stop:
            raise Abort
        # envoie les infos de progression à la fenêtre graphique
        self.emit(QtCore.SIGNAL("infotelech(PyQt_PyObject)"), 
                                          [telechbloc, taillebloc, totalblocs])
 
    #========================================================================
    def stoptelech(self):
        # permet l'arrêt du téléchargement avant la fin
        self.stop = True
 
#############################################################################
class Fenetre(QtGui.QWidget):
 
    #========================================================================
    def __init__(self, parent=None):
        super(Fenetre,self).__init__(parent)
 
        self.label1 = QtGui.QLabel(u"Adresse web du fichier à télécharger:", self)
        self.fichierweb = QtGui.QLineEdit(self)
 
        self.label2 = QtGui.QLabel(u"Emplacement sur le disque:", self)
        self.fichier = QtGui.QLineEdit(self)
 
        self.label3 = QtGui.QLabel(u"Lancement/arrêt du téléchargement:", self)
 
        self.depart = QtGui.QPushButton(u"Départ", self)
        self.depart.clicked.connect(self.depart_m)
 
        self.stop = QtGui.QPushButton(u"Stop", self)
        self.stop.clicked.connect(self.stop_m)
 
        self.barre = QtGui.QProgressBar(self)
        self.barre.setRange(0, 100)
 
        self.barre.setValue(0)
 
        posit = QtGui.QGridLayout()
        posit.addWidget(self.label1, 0, 0, 1, 2)
        posit.addWidget(self.fichierweb, 1, 0, 1, 2)
        posit.addWidget(self.label2, 2, 0, 1, 2)
        posit.addWidget(self.fichier, 3, 0, 1, 2)
        posit.addWidget(self.label3, 4, 0, 1, 2)
        posit.addWidget(self.depart, 5, 0)
        posit.addWidget(self.stop, 5, 1)
        posit.addWidget(self.barre, 6, 0, 1, 2)
        self.setLayout(posit)        
 
        self.telech = None
 
    #========================================================================
    def depart_m(self):
        if self.telech==None or not self.telech.isRunning():
            # initialisation de la barre de progression
            self.barre.reset()
            self.barre.setRange(0, 100)
            self.barre.setValue(0)
            # démarre le téléchargement
            source = unicode(self.fichierweb.text())
            destination = unicode(self.fichier.text())
            self.telech = Telecharger(source, destination)
            self.connect(self.telech, QtCore.SIGNAL("infotelech(PyQt_PyObject)"), 
                                                            self.infotelech)
            self.connect(self.telech, QtCore.SIGNAL("fintelech(PyQt_PyObject)"), 
                                                            self.fintelech)
            self.telech.start()
 
    #========================================================================
    def infotelech(self, msg):
        """lancé à chaque réception d'info sur la progression du téléchargement"""
        telechbloc, taillebloc, totalblocs = msg
        if totalblocs > 0:
            # on a la taille maxi: on peut mettre à jour la barre de progression
            p = int(telechbloc*taillebloc/totalblocs*100)
            self.barre.setValue(p)
            QtCore.QCoreApplication.processEvents() # force le rafraichissement
        else:
            # taille maxi inconnue: la barre sera une chenille sans progression
            if self.barre.maximum > 0:
                self.barre.reset()
                self.barre.setRange(0, 0)
 
    #========================================================================
    def fintelech(self, msg):
        """Lancé quand le thread se termine (normalement ou pas)"""
        QtGui.QMessageBox.information(self,
            u"Téléchargement",
            msg)
 
    #========================================================================
    def stop_m(self):
        """demande l'arrêt du téléchargement avant la fin"""
        if self.telech!=None and self.telech.isRunning():
            self.telech.stoptelech()
 
    #========================================================================
    def closeEvent(self, event):
        """lancé à la fermeture de la fenêtre quelqu'en soit la méthode"""
        self.stop_m() # arrête un éventuel téléchargement en cours
        event.accept()
 
#############################################################################
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    fen = Fenetre()
    fen.show()
    sys.exit(app.exec_())


Amusez-vous bien!