PyQt 14 – QNetwork, baixando arquivos
- PyQt 01 – O Primeiro Programa
- PyQt 02 – Criando uma caixa de mensagem
- PyQt 03 – Diálogos com QMessageBox
- PyQt 04 – Diálogos com QInputDialog
- PyQt 05 – Diálogo QFileDialog
- PyQt 06 – Mais diálogos
- PyQt 07 – QLabel e Qt Designer
- PyQt 08 – QLineEdit e mais Qt Designer
- PyQt 09 – QPushButton, apertando os botões
- PyQt 10 – QCheckBox e QRadioButton: checando as opções
- PyQt 11 – QButtonGroup e QGroupBox: mais opções
- PyQt 12 – QComboBox
- PyQt 13 – QSpinBox, QProgressBar e + sinais
- PyQt 14 – QNetwork, baixando arquivos
Índice
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.
#!/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
.
7 Comentários