PyQt 14 – QNetwork, baixando arquivos

junho 11th, 2012 by rudsonalves Leave a reply »

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.

Construindo o Dialogo

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/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:

Tab 01. Conexões para sinais da listagem acima.

Linha Objeto Sinal Slot/Método
38 closeButton clicked self.close
39 self.downloadButton clicked self.downloadFile
40 self.url_LineEdit textChanged self.enableDownload
47 self.progressDialog canceled self.downloadCancel

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:

57
58
59
60
61
    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.

132
133
134
135
136
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:

50
51
52
53
54
        # 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:

Tab 02. Sinais da classe QNetworkAccessManager

Sinal Descrição
authenticationRequired Este sinal é emitido sempre que um servidor final requer autenticação
finished Este sinal é emitido sempre que um requerimento da rede for concluído
networkAccessibleChanged Este sinal é emitido quando o valor do atributo networkAccessible muda
proxyAuthenticationRequired Este sinal é emitido sempre que uma solicitação de autenticação de de proxy e que QNetworkAccessManager não consegue encontrar uma credencial válida
sslErrors Este sinal é emitido se a sessão SSL/TLS encontrou erros durante a configuração, incluindo erros de verificação de certificados
   

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:

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
    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:

rudson@khelben:$ 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.

88
89
90
91
92
93
94
95
96
97
        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:

Tab 03. Sinais da classe QNetworkReply

Sinal Descrição
downloadProgress este sinal é emitido para informa o progresso do download. Ele envia duas variáveis: bytesReceived e bytesTotal
error este sinal é emitido quando é encontrado algum erro no processo
finished este sinal equivale ao finished da classe QNetworkAccessManager
metaDataChanged como o nome diz, é emitido quando o metadata muda
sslErrors Este sinal é emitido se a sessão SSL/TLS encontrou erros durante a configuração, incluindo erros de verificação de certificados
uploadProgress este sinal é emitido para informa o progresso do upload. Ele envia duas variáveis: bytesReceived e bytesTotal

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

Tab 04. Alguns métodos da classe QNetworkReply

Método Descrição
abort () aborta o processo de download/upload, fechando as conxões
error () retorna o erro ocorrido durante o processo de requisição
isFinished () retorna True se o request estiver terminado
isRunning () retorna True se o request estiver rodando
manager () retorna o QNetworkAccessManager utilizado para criar este objeto QNetworkReply
request () retorna o QNetworkRequest utilizado para criar este objeto QNetworkReply
url () retorna o QUrl utilizado para criar este objeto QNetworkReply

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:

100
101
102
103
104
    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.

107
108
109
110
111
112
113
114
115
    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.

118
119
120
    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.

123
124
125
126
127
128
129
    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:

rudson@khelben:$ ./down22.py "http://a.url.do.arquivo"

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

Advertisement

7 comments

  1. klick hier disse:

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

  2. wesley disse:

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

  3. Herculys disse:

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

  4. Pedro Monteiro disse:

    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

  5. Diego disse:

    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 uma resposta