PyQt 14 – QNetwork, baixando arquivos

Este artigo é a parte 14 de 14 na série PyQt

Já faz muito tempo que não implementava nada em PyQt4 e, confesso, perdi muito da prática. Mas depois de algum sofrimento acho que estou pegando o jeito novamente. Neste post de número 14 vou implementar um diálogo simples para baixar arquivos via internet. Este processo é uma demanda antiga que vinha postergando já a muito tempo, e que geralmente resolvia utilizando a urllib. Sempre soube que havia ferramentas apropriadas para realizar esta tarefa em PyQy, mas nunca encontrei um exemplo claro para me auxiliar a resolver algumas pendências no assunto.

A documentação do PyQt, infelizmente, possui praticamente todos os códigos exemplo em C++, enquanto que os exemplos disponibilizados com o código fonte do PyQt utiliza classes mais antigas, como QHttp e QFtp.

Neste post irei utilizar as classes QNetworkAccessManager, QNetworkRequest e QNetworkReply, que são classes mais novas, acho que implementadas a partir da versão 4 da biblioteca qt. Estas classes trazem algumas vantagens sobre as anteriores, como uma API simples, mais poderosa e uma implementação de protocolo mais moderna.

Para este post irei implementar um diálogo simples para baixar arquivos da internet. O diálogo terá apenas uma widget QLineEdit para armazenar a url do arquivo e dois botões, sendo um para iniciar o download e outro para fechar o diálogo.

Dado a simplicidade do diálogo, escolhi por construí-lo manualmente. Adicionei um pouco de código diferente do apresentado até o momento e vou passar a uma breve explicação destas linhas antes de iniciar com as classes do módulo QNetwork. As linhas abaixo criam o diálogo e cuidam das funcionalidades básicas deste.

#!/bin/env python

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtNetwork import *

import sys

class downloadManager(QDialog):
    def __init__(self, str_url = None, parent = None):
        super(downloadManager, self).__init__(parent)

        # Build dialog
        # Label and Url LineEdit
        label = QLabel('Put here the full address of the file:')
        self.url_LineEdit = QLineEdit(str_url)

        # Buttons
        self.downloadButton = QPushButton('&Download')
        closeButton = QPushButton('&Close')

        # buttonBox
        buttonBox = QDialogButtonBox()
        buttonBox.addButton(self.downloadButton, QDialogButtonBox.ActionRole)
        buttonBox.addButton(closeButton, QDialogButtonBox.RejectRole)

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(label)
        layout.addWidget(self.url_LineEdit)
        layout.addWidget(buttonBox)
        self.setLayout(layout)

        self.setWindowTitle('Download File')
        self.setMinimumWidth(500)

        # connections
        closeButton.clicked.connect(self.close)
        self.downloadButton.clicked.connect(self.downloadFile)
        self.url_LineEdit.textChanged.connect(self.enableDownload)

        if not str_url:
            self.downloadButton.setEnabled(False)

        # Progress dialog
        self.progressDialog = QProgressDialog(self)
        self.progressDialog.canceled.connect(self.downloadCancel)

        ...

Na linha 23 utilizei a classe QDialogButtonBox, que é uma widget apropriada para organizar botões. Não cheguei a utilizar nada em especial desta widget, que não pudesse ser substituído por um QHBoxLayout. Por isto não vou entrar em detalhes aqui.

O que mudou mais visivelmente, com respeito aos códigos apresentados nos posts anteriores, foi a forma como são criados as conexões. Geralmente as criava usando uma sintaxe semelhante à linha a seguir:

self.connect(close_button, SIGNAL("clicked()"), self, SLOT("reject()"))

(post PyQt 02). A mesma linha pode ser reescrita na forma:

close_button.clicked.connect(self.reject)

A vantagem é que o código fica mais compacto, no entanto acaba se perdendo um pouco a visão da conexão. A sintaxe segue o padrão:

[OBJETO].[SINAL].connect([SLOT/Método])

Esta sintaxe é empregada nas linhas 38, 39, 40 e 47 deste código, além de outras conexões que aparecerão adiante. A tabela abaixo resume as ações das conexões declaradas no código acima:

[TABLE=44]

Utilizei também a classe QProgressDialog, que cria um diálogo com uma barra de progresso e um botão Cancel. Este é mais um diálogo pré-construído do Qt, ao estilo dos diálogos apresentados nos artigos PyQt 03, 04, 05 e 06. Não me lembro se cheguei a falar deste, mas não há muito o que dizer.

Os métodos downloadFile, enableDownload e downloadCancel serão apresentados mais adiante.

Antes de passar à conexão da rede, vou completar a parte da interface com mais duas pequenas peças de código:

    def enableDownload(self):
        if self.url_LineEdit.text():
            self.downloadButton.setEnabled(True)
        else:
            self.downloadButton.setEnabled(False)

Este pequeno pedaço de código é executado sempre que o conteúdo da widget self.url_LineEdit é editada (Tab. 01, conexão da linha 40). Este código verifica se a self.url_LineEdit.text() não está vazia e habilita o botão de download (self.downloadButton), em caso afirmativo.

Para finalizar, a última parte do código que se encarrega da chamada ao diálogo.

if __name__ == '__main__':
  app = QApplication(sys.argv)
  down = downloadManager(sys.argv[1])
  down.show()
  sys.exit(down.exec_())

Iniciando o Download

O restante do código trata, essencialmente, do gerenciamento do download do arquivo pela rede. Primeiro às declarações iniciais e alguns comentários:

        # Network variables
        self.networkAccess = QNetworkAccessManager(self)
        self.networkAccess.finished.connect(self.requestFinished)

        self.requestAborted = False

Inicialmente é criado a variável self.networkAccess, como sendo uma instância da classe QNetworkAccessManager. Esta classe é quem permite ao aplicativo enviar e receber solicitações através da rede.

Neste texto não vou explorar todas as capacidades da classe QNetworkAccessManager, mas para dar uma noção da amplitude desta classe, separei alguns sinais interessantes:

[TABLE=45]

Neste aplicativo, todo o acesso a rede é feita pelo trio QNetworkAccessManager, QNetworkRequest e QNetworkReply. Em poucas palavras inicialmente você deve montar uma requisição com o QNetworkRequest, em seguida solicitar o pacote na rede com o QNetworkAccessManager. Este por sua vez retorna um QNetworkReply, com os dados solicitados. A ideia é simples mas aconselho que dê uma lida na documentação para ter uma compreensão mais profunda do processo.

Voltando ao código, a linha 52 conecta o sinal finished ao módulo self.requestFinished. Como mostrado na tabela acima, este sinal será emitido quando o download do arquivo terminar. A linha 53 declara uma variável de controle, para armazenar o status de abortamento do download.

A próxima parte do código é a responsável por iniciar o processo de download do arquivo. Lembre-se que este código é acionado ao se pressionar o botão downloadButton, como declarado na conexão da linha 39. A primeira parte do código do método downloadFile é apresentado a seguir:

    def downloadFile(self):
        url = QUrl(self.url_LineEdit.text())
        fileInfo = QFileInfo(url.path())
        fileName = fileInfo.fileName()

        if QFile.exists(fileName):
            msg = 'The file {0} exists in current directory. Overwrite it?'.format(fileName)
            ans = QMessageBox.question(self, 'Download', msg, \
                  QMessageBox.Yes | QMessageBox.No, \
                  QMessageBox.Yes)

            if ans == QMessageBox.Yes:
                QFile.remove(fileName)
            else:
                return

        self.outFile = QFile(fileName)
        if not self.outFile.open(QIODevice.WriteOnly):
            QMessageBox.information(self, 'Download', \
                  'Unable to save {0}: {1}'.format(fileName, \
                  self.outFile.errorString()))
            self.outFile = None
            return

Neste código existe alguns elementos interessantes a começar pela linha 65. A variável url recebe o objeto criado pela classe QUrl. Esta classe é uma componente da biblioteca QtCore. Embora não a explore muito neste código, é importante notar que ela é bem versátil. As linhas abaixo mostra um pouco dos métodos e atributos desta classe:

python
Python 2.6.6 (r266:84292, Nov 27 2010, 17:27:14)
[GCC 4.5.1] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from PyQt4.QtCore import *
>>> url = QUrl('ftp://alberto:12345@localhost:28/home/ftp/distros/slack64-13-37.iso')
>>> url.password()
PyQt4.QtCore.QString(u'12345')
>>> url.userName()
PyQt4.QtCore.QString(u'alberto')
>>> url.host()
PyQt4.QtCore.QString(u'localhost')
>>> url.path()
PyQt4.QtCore.QString(u'/home/ftp/distros/slack64-13-37.iso')
>>> url.port()
28

Até onde eu pode verificar, esta classe premite fazer de tudo com uma url, por mais complexa que ela seja.

O QFileInfo, apresentado na linha 66, permite verificar vários atributos de um path, seja ele de um diretório ou arquivo. Neste código o uso como substituto da função basename do unix (na linha 67), onde apenas removo o nome do arquivo do path. No entanto suas capacidades vão muito além disto. É possível verificar se o path passado pertence a um arquivo, diretório, link, executável, …, proprietário, grupo, …, permissões, … Em suma, é bem útil.

Na linha 69 uso a classe QFile para verificar se o arquivo em fileName existe no diretório corrente. Esta classe substitui a classe file padrão do python, que além de manusear (remover e renomear) um arquivo ainda pode cria, abrir para leitura e escrita, entre outras.

Na linha 76 o QFile é empregado para remover o arquivo fileName, enquanto que na linha 80 ele é usado para criar o arquivo fileName. Por fim, na linha 81 o arquivo fileName é aberto para escrita. As linhas seguintes emitem uma mensagem de erro caso encontre algum problema na abertura do arquivo.

        self.request = QNetworkRequest(url)

        self.progressDialog.setWindowTitle('Download')
        self.progressDialog.setLabelText('Downloading {0}.'.format(fileName))
        self.reply = self.networkAccess.get(self.request)

        self.reply.downloadProgress.connect(self.downloadProgress)
        self.reply.sslErrors.connect(self.sslErrors)

        self.progressDialog.show()

Na linha 88 é onde inicia a preparar o processo de download. Inicialmente é criado um request (uma requisição) para o arquivo no endereço passado pela variável url. Esta requisição será passada ao objeto networkAccess, instância da classe QNetworkAccessManager. A classe QNetworkRequest faz parte da API de acesso à rede e é a classe que detém as informações necessárias para enviar um pedido através da rede.

As linhas 90 e 91 apenas preparam o título e o texto a serem apresentados no diálogo progressDialog. Exceto pelo texto, o título poderia ter sido declarado anteriormente.

A requisição do arquivo, propriamente dita, é feita na linha 92, pelo objeto networkAccess, através do método get. O retorno desta linha é um objeto da classe QNetworkReply, que contém os dados e o header da requisição enviada pelo QNetworkAccessManager. Para entender melhor apresento a seguir os sinais emitidos por este objeto:

[TABLE=46]

A próxima tabela apresenta um pouco dos métodos desta classe:

[TABLE=47]

A lista completa dos métodos e atributos você encontra na documentação. A intenção aqui é apenas mostras um pouco do que a classe pode lhe oferecer.

Retornando ao aplicativo, na linha 94 é feito uma conexão ao sinal downloadProgress, emitido pelo objeto self.reply (Tab 03. sinal na linha 1). Este sinal é quem permite preencher a barra de progresso do progressDialog. O método self.downloadProgress que implementa isto será detalhado mais adiante.

Na linha 95 é feito uma segunda conexão, agora ao sinal sslErrors, também emitido pelo objeto self.reply, ao método self.sslErrors. conectar este sinal é necessário para fazer o tratamento dos erros que podem ocorrer durante o processo de transferência do arquivo.

Por fim, na linha 97 o diálogo progressDialog é apresentado.

Registrando o Progresso

As linhas a segui são a implementação do sinal downloadProgress, emitido pelo objeto self.reply, conectados na linha 94:

    def downloadProgress(self, bytesReceived, bytesTotal):
        if self.requestAborted:
            return

        self.progressDialog.setValue(100*bytesReceived/bytesTotal)

Observe que duas variáveis são enviadas a este método pelo sinal downloadProgress: bytesReceived e bytesTotal. A primeira (bytesReceived) traz o total de bytes baixados, em um inteiro longo, enquanto que bytesTotal traz o total de bytes a serem baixados.

Inicialmente, na linha 101, é verificado se uma requisição para abortar o processo foi iniciada, em caso contrário, o percentual de bytes transferidos é escrito na barra de progresso do diálogo self.progressDialog, usando o método setValue, linha 104.

Verificando Erros

O método a seguir implementa a análise de erros para os sinais emitidos pelo objeto self.reply (sinal sslErrors), conexão realizada na linha 95.
Este código não chega a fazer nenhuma análise dos erros encontrados, apenas apresenta a lista de erros (contida na variável errors) através de uma QMessageBox, e solicita se deve ignorar os erros.

    def sslErrors(self, errors):
        errorString = ', '.join([str(error.errorString()) for error in errors])

        ret = QMessageBox.warning(self, 'Download', \
                'One or more SSL errors has ocurred: {0}'.format(errorString), \
                QMessageBox.Ignore | QMessageBox.Abort)

        if ret == QMessageBox.Ignore:
            self.reply.ignoreSslErrors()

Quando o método ignoreSslErrors é invocado, erros de SSL relacionadas com conexão de rede são ignorados, incluindo erros de validação de certificado. É possível passar também uma lista com os erros a serem ignorados.

Cancelando o Download

As linhas a seguir são executadas quando o botão cancel em self.progressDialog é pressionado. Esta implementação responde à conexão realizada na linha 47.

    def downloadCancel(self):
        self.requestAborted = True
        self.reply.abort()

A variável self.requestAborted recebe True para indicar que o processo de download foi abortado, no entanto a abortamento de fato ocorre na linha 120, quando o método abort(), do objeto self.reply é invocado.

Terminando o Request

As linhas a seguir implementam o sinal requestFinished, emitido pelo objeto self.networkAccess (instância da classe QNetworkAccessManager), gerado quando o download termina. Este método responde à conexão declarada na linha 52.

    def requestFinished(self, networkReply):
        bytArray = networkReply.readAll()
        self.outFile.write(bytArray)
        self.outFile.close()
        self.progressDialog.hide()
        if self.requestAborted:
            self.outFile.remove()

Inicialmente os bytes baixados são carregados para a variável bytArray, linha 124, e depois são escritos no arquivo de saída self.outFile, linha 125, que ao final é fechado.

A linha 126 esconde o self.progressDialog e por fim é verificado se uma requisição de abortamento foi iniciada. Em caso afirmativo o arquivo é apagado do disco, linhas 127 e 128.

Bye

Este código me deu uma certa satisfação pessoal, pois se tratava de uma demanda antiga. No entanto meu trabalho aqui foi uma adaptação de códigos e informações de três fontes distintas. Boa parte do código é originário do exemplo http.py, que é distribuído juntamente com o código fonte do PyQt, onde tive que substituir a classe QHttp pelo QNetworkAccessManager.

Outra peça de código, a qual foi fundamental para conseguir desenrolar o processo, foi encontrado na lista de discussão PyQt ([PyQt] QNetworkAccessManager and “finished” signal), postada pelo Andreas Neumann, enquanto criava um código para baixar uma imagem pela rede.

Outra fonte inestimável de informação continua sendo a documentação oficial do PyQt. Minha única lástima vem por conta dos exemplos que acompanham a documentação, os quais a grande maioria ainda continuam em C++.

Bom, espero que isto poupe algumas horas de pesquisa a terceiros. O código completo está disponível aqui: pyqt-14

10-07-2013 – Estava a 3 dias procurando como fazer isto e acabei achando em meu próprio site. Tristemente devo confessar que não me lembrava mais de ter feito este código. Apenas uma nota a adicionar: por algum motivo fiz com que o código em pyqt-14.zip necessite de um parâmetro de entrada, portanto para usá-lo tente algo como:

./down22.py "http://a.url.do.arquivo"

De outra forma ele deve retornar o erro IndexError: list index out of range.

Este post tem 7 comentários

  1. klick hier

    This paragraph will assist the internet users for creating new weblog or even a weblog from
    start to end.

  2. wesley

    Parabéns, estou feliz por ter voltado a postar seu site foi minha fonte de consulta pra construir meu 1º software usando pyqt. Valeu!!! 😉

    1. rudsonalves

      Valeu Wesley, espero ter tempo para escrever mais sobre PyQt, é algo que me diverte muito.

  3. Herculys

    Esses artigos estão me ajudando muito no desenvolvimento do meu primeiro software.

  4. Pedro Monteiro

    Oi rudsonalves, eu queria pedir uma ajuda sua, se possível, dentro de uma QGraphicsScene eu coloquei um item, um retangulo (http://imgur.com/E1zYfwJ), consigo movê-lo mas eu queria também redimensioná-lo com o mouse, e após perguntar em fóruns e afins, não consegui adquirir nenhuma resposta, não entendo como ninguém sabe como fazer, eles falam o que deve ser feito, mas é muito abstrato, não dão uma resposta precisa e ainda por cima não sabem pyqt, só qt em c++, por isso queria pedir sua ajuda, comecei aprendendo pyqt4 por aqui, enfim, vc sabe como posso fazer para redimensionar um QGraphicsItem com o mouse?
    O mais próximo que cheguei da resposta foi esse link: http://www.davidwdrell.net/wordpress/?page_id=46

    1. rudsonalves

      Sinto muito Pedro, mas nunca usei uma QGraphicsScene e no momento nem mesmo tenho tempo para dar uma olhada.

  5. Diego

    Cara, seguinte, tô num longo caminho de aprendizagem em Python. No começo achei esquisito, mais tô me acostumando (Comecei a programar com C e Java). Minha meta é programar tanto pra Web quanto pra desktop. Tenho usado a versão 3 e dividi o aprendizado em base (sintaxe), estruturas, classes e objetos, conexão (CRUD básico), design partt..(MVC) e agora GUI e futuramente Web. Andei pesquisando e achei a TKInter, só que me acostumei tanto com Swing (Java) que achei estranho aqueles botões meio primitivos, daí vi GTK e agora PyQt. Obrigado por tá postando isso tudo cara!! Vai me salvar muito! Parabéns. Vou pro PyQt hehe!

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.