Archive for the ‘Python’ tag
Brincando com monkey patching
Segundo a Wikipedia, monkey patch é uma técnica utilizada para modificar atributos, funções e métodos em tempo de execução, ou seja, sem precisar mexer na implementação.
Somente algumas linguagens dinâmicas suportam monkey patching, como Ruby, JavaScript, Smalltalk e Perl. Há linguagens dinâmicas que não tem suporte, como é o caso do PHP (apesar de ter algumas formas de obter um resultado parecido).
Eis um exemplo simples de patching com Python:
print 'Hi, '+me
sayHiToMe('Herberth') # Hi, Herberth
def sayHello(me):
print 'Hello, '+me
sayHiToMe = sayHello
sayHiToMe('Herberth') # Hello, Herberth
Isto é possível porque o Python trata suas funções e métodos como objetos. Tanto é que você pode chamar uma função/método utilizando o método __call__:
print 'Hi, '+me
sayHiToMe.__call__('Herberth') #Hi, Herberth
Uma coisa mais interessante é modificar métodos de classe em tempo de execução e a modificação ficar valendo para todos os objetos:
def meuTeste(self):
print 'Testando...'
def meuOutroTeste(self):
print 'Testando denovo...'
primeiroTeste = Teste()
primeiroTeste.meuTeste() #Testando...
Teste.meuTeste = meuOutroTeste
primeiroTeste.meuTeste() #Testando denovo...
Este é um dos motivos pelos quais você tem que passar a referência do objeto para cada método da classe ![]()
Monkey patching pode ser muito útil quando necessitamos de um mock/stub para testar uma determinada classe/método: você pode criar o método que vai substituir o médoto em questão sem precisar perder a referência do método original. Exemplo:
def sendMail(self,from,to,subject,message):
print 'Enviando email...'
self.mailSent = True
def Cliente:
def cadastro(self,nome,telefone,email):
# salva o cliente no banco de dados e no fim envia um email
self.mail = Email()
self.mail.send('admin@loja.com','cliente@teste.com','blah','blah blah!')
# não queremos que o método da classe Email apresentada acima envie um email
# a cada de cadastro do cliente. Por isso, vamos fazer um patch para o método sendMail.
def sendFakeMail(self,from,to,subject,message):
self.mailSent = True
originalMail = Email.sendMail #salva a referencia original de sendMail
Email.sendMail = sendFakeMail
c = Cliente()
c.cadastro('Herberth','999-9999','meu@email.com')
assert c.mail.mailSent #verifica se o email foi enviado
Email.sendMail = originalMail # restaura o sendMail original
Legal, não? ![]()
Criando Web crawlers (distribuidos) em Python – Parte IV
Outros posts da série:
Na parte III eu mostrei como criar um simples sistema de recuperação de informações usando em áreas protegidas por senha, usando o legendas.tv como exemplo. Neste post, eu irei mostrar como criar um crawler distribuindo-o por vários computadores da sua rede (ou colocar várias instâncias rodando na mesma máquina).
Sistemas distribuídos sempre foi um dos meus assuntos favoritos à estudar. Ter à minha disposição um cluster para poder realizar alguns experimentos, ou desenvolver um software para rodar de forma distribuída sem precisar aumentar a complexidade do SO sempre me chamou atenção.
Neste post, eu vou mostrar como o PyRO (Python Remote Object) e seus artefatos funcionam e como tirar proveito deles para criar um crawler distribuído de forma bem fácil.
O PyRO tem uma arquitetura parecida com a RMI do Java e que lembra um pouco o CORBA. A diferença é que o PyRO tem uma arquitetura interna bem mais simples e a sua programação é bem menos verbosa, já que você não precisa de escrever quilos de código pra ter um exemplo funcional.
1 – Arquitetura básica
- PyRO server: responsável por instanciar o objeto e o distribuir pela rede. É importante notar que ele não instancia um novo objeto a cada requisição ou pra cada cliente: a mesma instância do objeto é utilizada sempre (à menos que você especifique o contrário).
- Rede: essa parte é obvia

- Client: consome a “instância” do objeto criado pelo server
Falando como um programador, a estrutura acima ficaria mais ou menos assim:
Código do servidor:
import Pyro.core
#objeto que será compartilhado
class SharedObject(Pyro.core.ObjBase):
def __init__(self):
Pyro.core.ObjBase.__init__(self)
def remoteMethod(self,message):
return 'Olá '+message
class Main:
def __init__(self):
Pyro.core.initServer()
daemon = Pyro.core.Daemon()
objeto = SharedObject() #cria a instância do objeto
uri = daemon.connect(SharedObject(),'obj')
print 'Porta do daemon',daemon.port
print 'URI do Objeto',uri
daemon.requestLoop()
if __name__=='__main__':
Main()
Código do cliente:
objeto = Pyro.core.getProxyForURI('PYROLOC://localhost:7766/obj')
#vc pode manipular os objetos como se eles estivessem locais
print "Servidor retornou:"+objeto.remoteMethod('Herberth')
Simples, não? Basta colocar o cliente pra rodar logo depois que o servidor for iniciado. Os objetos distribuídos com o PyRO tem somente uma característica que importante notar: você consegue compartilhar objetos normalmente, mas não consegue acessar suas propriedades diretamente. Se você precisar acessar uma propriedade, você terá que criar um getter/setter para essa propriedade (eu também achei meio feio, mas nem tudo é perfeito
.
Se você tiver o olho afiado vai perceber que à medida que mudamos de servidor precisamos dar um jeito de mudar o nosso script cliente pra atender à nova mudança. E se você tiver múltiplos servidores de objetos, terá que especificar cada um nos clientes. Isso, junto com outros motivos, podem criar complicações. Uma forma de resolver isso é com o PyRO Nameserver.
2 – PyRO Nameserver.
O principal papel do servidor de nomes é tirar a responsabilidade dos clientes saberem onde estão os seus servers. Os clientes devem ser os mais simples possível.
O servidor de nomes do PyRO na verdade são dois servidores: um servidor TCP/IP e um listener de broadcast. O listener broadcast serve para que os clientes descubram a localização do servidor de nomes e para que os servidores publiquem seus objetos.
Numa arquitetura com servidor de nomes, nosso sistema ficaria mais ou menos assim:
O nameserver deve ser iniciado antes dos servidores e antes dos clientes da sua aplicação. Para inicia-lo execute o comando python -m Pyro.naming ou execute-o diretamente digitando pyro-ns ou pyro-ns.cmd se você tiver usando Windows.
Eis um exemplo simples de sistema distribuído usando PyRO e o PyRO-ns:
Cliente:
objeto = Pyro.core.getProxyForURI('PYRONAME://obj')
print "Servidor retornou:"+objeto.remoteMethod('Herberth')
Servidor:
import Pyro.core
import Pyro.naming
class SharedObject(Pyro.core.ObjBase):
said = False
def __init__(self):
Pyro.core.ObjBase.__init__(self)
def isSaid(self):
return self.said
def remoteMethod(self,message):
self.said = True
return 'Olá '+message
class Main:
def __init__(self):
Pyro.core.initServer()
daemon = Pyro.core.Daemon()
ns = Pyro.naming.NameServerLocator().getNS()
daemon.useNameServer(ns) #publica o servidor no nameserver
objeto = SharedObject()
uri = daemon.connect(SharedObject(),'obj')
print 'Porta do daemon',daemon.port
print 'URI do Objeto',uri
daemon.requestLoop()
if __name__=='__main__':
Main()
Pouca coisa mudou no código, mas agora você não precisa especificar manualmente a localização do seu objeto: o nameserver faz o serviço pra você.
A inicialização do servidor e do cliente fica um pouco mais lenta depois que se passa a utilizar o servidor de nomes. O tempo extra é devido à busca do servidor de nomes no início da execução do cliente e do servidor.
Putz, falei demais sobre o PyRO, mas ainda tem muita coisa que eu não falei e que pode ser visto na documentação oficial. Agora vou abordar um pouco de como distribuir nosso crawler através dele.
3 – Distribuindo seu crawler
O crawler nada mais será do que um objeto distribuído. Sim, só isso ![]()
A teoria pode ser fácil, mas há algumas considerações que devem ser feitas em um crawler usando a abordagem distribuída:
- Como dividir o trabalho entre os clientes/peers de forma eficiente?
- Como assegurar que os crawlers não baixem a mesma página duas vezes?
- O que acontece com o controle de concorrência quando usamos um paradigma distribuído?
- Sobre single points of failure: se o servidor morre, os clientes não podem fazer nada. O que fazer nesses casos?
Infelizmente, eu não posso responder todos esses aspectos. Muita pesquisa baseada nessas perguntas têm sido feita e se você vai ficar impressionado se quiser aprofundar mesmo no assunto (como eu tou fazendo pro meu TCC).
A princípio, vamos fazer um crawler simples, que obtém todas as páginas de um determinado domínio, reporta as páginas baixadas e os links encontrados nas respectivas páginas. Já vimos como usar o BeautifulSoup e como criar crawlers básicos em outros posts. Aqui segue o exemplo de um pra usarmos na versão distribuída:
import urllib2
import Pyro.core
from BeautifulSoup import BeautifulSoup
class Crawl(Pyro.core.ObjBase):
base_url = "http://sourceforge.net"
queue = []
downloaded_links = {}
links_in_process = []
def __init__(self):
Pyro.core.ObjBase.__init__(self)
self.queue.extend(self.getLinksAndTitle(self.base_url)['links'])
def getLinksAndTitle(self,url):
#eliminar busca em dominios externos
if not self.isLinkInDomain(url):
return
if url.startswith("http://"):
req = urllib2.Request(url)
else:
req = urllib2.Request(self.base_url+url)
response = urllib2.urlopen(req).read()
soup = BeautifulSoup(response)
a = soup.findAll('a')
links = []
for i in a:
link = i.get('href')
if link != None and self.isLinkInDomain(link):
links.append(i.get('href'))
return {'links':links,'title':soup.find('title').text}
def getNextLinkToDownload(self):
if self.queue.__len__() == 0:
return False
link = self.queue[0]
del self.queue[0]
self.links_in_process.append(link)
return link
def reportDownloadedLink(self,link,title):
"""
Reporta um link baixado e seu respectivo titulo
"""
if link in self.links_in_process and \
not self.downloaded_links.has_key(link):
self.downloaded_links[link] = title
#tira o link da fila de processamento
del self.links_in_process[self.links_in_process.index(link)]
def addLinks(self,links):
for link in links:
if link not in self.queue and \
link not in self.downloaded_links and \
link not in self.links_in_process:
self.queue.append(link)
def isLinkInDomain(self,link):
return (link.startswith("http://") and \
link.startswith(self.base_url)) or \
not link.startswith('http://')
No meu caso, eu estou usando o SourceForge.net por que ele possui um robots.txt bem amigável e é um bom exemplo. Esse exemplo é bem simples (pra uma versão distribuída), mas serve perfeitamente pra ilustrar um crawler trabalhando de forma descentralizada.
Vamos ao server:
import Pyro.naming
from crawl import Crawl #nosso crawler
class Main:
def __init__(self):
Pyro.core.initServer()
daemon = Pyro.core.Daemon()
ns = Pyro.naming.NameServerLocator().getNS()
daemon.useNameServer(ns) #publica o servidor no nameserver
objeto = Crawl()
uri = daemon.connect(objeto,'crawl')
print 'Porta do daemon',daemon.port
print 'URI do Objeto',uri
daemon.requestLoop()
if __name__=='__main__':
Main()
Nenhuma novidade no servidor. Apenas colocamos a instância do crawler como objeto distribuído.
Agora o código do cliente:
crawl = Pyro.core.getProxyForURI('PYRONAME://crawl')
while True:
link = crawl.getNextLinkToDownload()
if not link:
break #nenhum link encontrado. fim do crawling
if link.endswith('download'): #so queremos paginas, sem baixar arquivos
continue
print 'Obtendo link: ',link
try:
result = crawl.getLinksAndTitle(link)
crawl.reportDownloadedLink(link,result['title'])
crawl.addLinks(result['links'])
except:
print 'Nao foi possivel obter o link ',link
Super simples. Faço algumas validações básicas e exibo a página atual que o cliente está buscando. Ele basicamente faz os seguintes passos:
- Pega o próximo link à baixar.
- Baixa-o.
- Reporta para o objeto que baixou o link.
- Adiciona mais links na fila de download.
Eis o sistema acima rodando com dois clientes:
Você pode perceber que alguns links podem se repetir. Eu ainda não estudei o PyRO à fundo, mas eu acredito que seja algo relacionado ao tempo sincronização dos objetos dos clientes com o servidor.
Espero que tenham gostado!
Criando Web crawlers em Python – Parte III
Ir para Criando Web crawlers em Python – Parte I
Ir para Criando Web crawlers em Python – Parte II
Na parte II desta série de tutoriais, eu mostrei como criar um mini crawler para o SourceForge.net, com suporte à pesquisa e download de softwares. Neste post, eu irei mostrar como gerenciar sessões com o Python e um downloader de legendas do legendas.tv como exemplo de uso.
Bem, antes de mais nada, o legendas.tv não tem um robots.txt (pelo menos não ao momento que escrevo este post) e como tudo que não é expressamente proibido é permitido, eu posso sem nenhum problema, criar um crawler para baixar legendas de lá. No entanto, eu mandei um email falando sobre a minha experiencia e pedindo permissão. Felizmente, o pessoal do legendas.tv permitiu que eu fizesse o crawler usando-os como cobaias ![]()
1 – Gerenciando sessões no Python.
HTTP é um protocolo stateless. Isso quer dizer que ele não guarda o estado da aplicação da mesma forma que um programa que você tem instalado no seu computador faz, por exemplo. Podemos enviar nosso nome de usuário e senhas pro legendas.tv (ou para qualquer outro site). No entanto, há formas de guardar o estado da sua aplicação Web e uma dessas formas é através de cookies. O cookielib do Python faz esse trabalho de gerenciar cookies e faz de uma forma relativamente fácil. Eis um exemplo adaptado da página de documentação do cookielib:
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
urllib2.install_opener(opener)
request = urllib2.Request("http://www.example.com")
Apesar do código acima ser um pouco obscuro pra entender, ele basicamente cria um handler (o opener) para lidar com os cookies. A partir daí é só usar o opener para fazer requisições. Podemos enviar nosso nome de usuário e senhas para o portal e automaticamente o opener enviará os cookies comprovando que você está logado. Simples, não? Agora é só enviar nosso usuário e senha para o legendas.tv e sair crawleando:
import cookielib, urllib2,urllib
from BeautifulSoup import BeautifulSoup
import sys
import re
base_url = 'http://legendas.tv'
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
urllib2.install_opener(opener)
username = raw_input('Login: ')
password = raw_input('Senha:')
login_data = urllib.urlencode({'txtLogin':username,'txtSenha':password})
request = urllib2.Request(base_url+'/login_verificar.php',login_data)
response = urllib2.urlopen(request).read()
if response.__contains__('Dados incorretos'):
print 'Ooops dados incorretos...'
raw_input('Pressione uma tecla para sair...')
sys.exit()
else:
print 'Logado com sucesso!'
Whoa! Conseguimos logar e estamos prestes a fazer a busca!
2 – Fazendo a busca
A busca consiste em três campos:
Assim como fizemos no login, faremos agora na busca: colocar os dados da busca num dicionário e passar como parâmetro para o urllib2.Request:
import cookielib, urllib2,urllib
from BeautifulSoup import BeautifulSoup
import sys
import re
base_url = 'http://legendas.tv'
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
urllib2.install_opener(opener)
username = raw_input('Login: ')
password = raw_input('Senha:')
login_data = urllib.urlencode({'txtLogin':username,'txtSenha':password})
request = urllib2.Request(base_url+'/login_verificar.php',login_data)
response = urllib2.urlopen(request).read()
if response.__contains__('Dados incorretos'):
print 'Ooops dados incorretos...'
raw_input('Pressione uma tecla para sair...')
sys.exit()
else:
print 'Logado com sucesso!'
print 'Fazendo busca:'
print '--------------'
busca = raw_input('Buscar por: ')
tipo = raw_input('Tipo:(1 - Release,2 - Filme, 3- Usuário):')
idioma = raw_input('Idioma: (1 - Português,2 - Ingles,99 - Todos)')
search_dict = {'txtLegenda':busca,'selTipo':tipo,'int_idioma':idioma}
search_data = urllib.urlencode(search_dict)
request = urllib2.Request(base_url+'/index.php?opcao=buscarlegenda',search_data)
response = urllib2.urlopen(request)
page = response.read()
soup = BeautifulSoup(page)
Eu não coloquei todas as opções de idioma para manter o código simples. Só lembrando que esses códigos foram retirados do atributo value dos options do select int_idioma.
A partir de agora é só brincar um pouco com o BeautifulSoup que conseguiremos extrair todos os dados que quisermos. Eis o exemplo:
i = 0
list_download_ids = []
for span in span_results:
# há dois tipos de span na busca. Este não contem o que a gente quer
if span.attrs == [('class', 'brls')]:
continue
td = span.find('td',{'class':'mais'})
#parent encontra o elemento pai.
#ex: td.parent aponta para tr e tr.parent aponta para table
release = td.parent.parent.find('span',{'class':'brls'}).contents[0]
sub_name = td.contents[0].contents[0]
downloads = td.contents[5]
comentarios = td.contents[7]
avaliacao = td.contents[10]
data = span.findAll('td')[2].contents[0]
#recupera o ID do download que tá no código javascript
download_id_js = td.parent.parent.attrs[1][1]
download_id = re.search('[a-z0-9]{32}',download_id_js).group(0)
list_download_ids.append(download_id)
i+=1
print 'Opção: '+str(i)
print 'Serie: '+sub_name
print 'Release: '+release
print 'Downloads: '+downloads
print 'Comentarios: '+comentarios
print 'Avaliacao: '+avaliacao
print 'Data: '+data
print '-------------------------------------'
A estrutura da página do legendas.tv é um pouco complicada, por isso nós temos que fazer alguns workarounds (nome bonito para gambiarra) para conseguirmos extrair todos os dados. Um exemplo disso é como eu fiz para pegar o id dentro do código JS inline na página. Isso deixa nosso mini-crawler um pouco mais complexo de entender (de fato é um dos mais complexos que já mexi ate hoje).
3 – O código final
Como o legendas.tv não tem link direto para download de legendas e o usuário tem que estar logado para baixa-la, eu preferi não usar o wget para isso, pois iria aumentar a complexidade, já que teríamos que salvar o cookie num arquivo e passar pra ele como parametro para ele baixar a legenda. Então eu preferi deixar tudo com urllib mesmo com alguns tricks com headers. Este é o exemplo final e funcional:
import cookielib, urllib2,urllib
from BeautifulSoup import BeautifulSoup
import sys
import re
base_url = 'http://legendas.tv'
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
urllib2.install_opener(opener)
username = raw_input('Login: ')
password = raw_input('Senha:')
login_data = urllib.urlencode({'txtLogin':username,'txtSenha':password})
request = urllib2.Request(base_url+'/login_verificar.php',login_data)
response = urllib2.urlopen(request).read()
if response.__contains__('Dados incorretos'):
print 'Ooops dados incorretos...'
raw_input('Pressione uma tecla para sair...')
sys.exit()
else:
print 'Logado com sucesso!'
print 'Fazendo busca:'
print '--------------'
busca = raw_input('Buscar por: ')
tipo = raw_input('Tipo:(1 - Release,2 - Filme, 3- Usuário):')
idioma = raw_input('Idioma: (1 - Português,2 - Ingles,99 - Todos)')
search_dict = {'txtLegenda':busca,'selTipo':tipo,'int_idioma':idioma}
search_data = urllib.urlencode(search_dict)
request = urllib2.Request(base_url+'/index.php?opcao=buscarlegenda',search_data)
response = urllib2.urlopen(request)
page = response.read()
soup = BeautifulSoup(page)
span_results = soup.find('td',{'id':'conteudodest'}).findAll('span')
i = 0
list_download_ids = []
for span in span_results:
# há dois tipos de span na busca.
#Este não contem o que nós gente queremos
if span.attrs == [('class', 'brls')]:
continue
td = span.find('td',{'class':'mais'})
#parent encontra o elemento pai.
#ex: td.parent aponta para tr e tr.parent aponta para table
release = td.parent.parent.find('span',{'class':'brls'}).contents[0]
sub_name = td.contents[0].contents[0]
downloads = td.contents[5]
comentarios = td.contents[7]
avaliacao = td.contents[10]
data = span.findAll('td')[2].contents[0]
#recupera o ID do download que tá no código javascript
download_id_js = td.parent.parent.attrs[1][1]
download_id = re.search('[a-z0-9]{32}',download_id_js).group(0)
list_download_ids.append(download_id)
i+=1
print 'Opção: '+str(i)
print 'Serie: '+sub_name
print 'Release: '+release
print 'Downloads: '+downloads
print 'Comentarios: '+comentarios
print 'Avaliacao: '+avaliacao
print 'Data: '+data
print '-------------------------------------'
download_op = int(raw_input('Qual opção deseja baixar? '))
url_request = base_url+'/info.php?d='+list_download_ids[download_op-1]+'&c=1'
request = urllib2.Request(url_request)
response = urllib2.urlopen(request)
legenda = response.read()
#eu acho que todas as legendas estão em formato rar
#mas só pro caso de eu estar errado.
fname = str(download_op)
if response.info().get('Content-Type').__contains__('rar'):
fname += '.rar'
else:
fname += '.zip'
f = open(fname,'w')
f.write(legenda)
f.close()
print 'Legenda '+fname+' salva com sucesso!'
Como o intuito desse post é mostrar como poderia ser um crawler para o legendas.tv, eu não me preocupei tanto com a estrutura do script (notem que ele repete algumas coisas algumas vezes) e não me preocupei com validação dos dados.
No próximo post eu vou dar umas dicas para melhora de performance e escalabilidade do crawler, mas provavelmente demorarei um pouco para escrever (Trabalho de Conclusão de Curso é complicado ..).
Espero que tenham gostado e gostaria de ouvir de vocês as suas impressões
See ya!
Criando Web crawlers em Python – Parte II
Ir para Criando Web crawlers em Python – Parte I
Ir para Criando Web crawlers em Python – Parte III
No post anterior, eu mostrei como recuperar informações básicas de uma página da Web usando urllib, urllib2 e BeautifulSoup. Neste post eu mostrarei como enviar dados via GET e POST.
Há um excelente guia sobre urllib2: o urllib2 – The Missing Manual que foi de grande valia nos meus estudos. Nele, pode-se encontrar as informações sobre envio de informações do tipo GET e POST para o servidor. Isso será útil, pois é assim que a busca do SourceForge (e da maioria dos outros portais) funcionam.
Vamos dar uma olhada na URL do na pesquisa por “Python” no SF:
http://sourceforge.net/search/?type_of_search=soft&words=python
Bem, isso indica que temos que enviar duas variáveis para o SF.net: type_of_search (sempre igual à soft) e words (que é a nossa busca). Um exemplo de código que faria essa busca poderia ser esse:
base_url = 'http://sourceforge.net'
busca = raw_input('Pesquisar por: ')
variaveis_get = urllib.urlencode({'type_of_search':'soft','words':busca})
req = urllib2.Request(base_url+'/search/',variaveis_get)
response = urllib2.urlopen(req).read()
print response
Se der tudo certo, você deveráver o código fonte da página do SF que contém os resultados da pesquisa.
Feito isso, vamos analisar como a marcação do SF é organizada na busca:
O Firebug nos ajuda a não perder tanto tempo assim analisando código. De acordo com o que pudemos obter, podemos dividir as informações que queremos dessa forma:
- O resultado da busca está dentro de uma tabela com o id=”searchtable”.
- Um resultado da busca está sempre dentro de um td com classe description.
- O nome do projeto está dentro de um link dentro de um h2 dentro desse td “description”.
- O link de download se encontra na próxima td da mesma linha da tabela.
Exibindo as informações dos projetos
Agora nós temos como buscar nossa informação e como filtra-la. Como disse no post anterior, eu iria mostrar alguns usos mais avançados do BeautifulSoup, principalmente no que diz respeito à percorrer o documento. O próximo passo será mostrar as informações dos projetos que aparecem no resultado da nossa busca. Podemos fazer isso da seguinte forma:
import urllib,urllib2
base_url = 'http://sourceforge.net'
busca = raw_input('Pesquisar por: ')
variaveis_get = urllib.urlencode({'type_of_search':'soft','words':busca})
req = urllib2.Request(base_url+'/search/',variaveis_get)
response = urllib2.urlopen(req).read()
soup = BeautifulSoup(response)
# procura pela tabela com id=searchtable
tabela = soup.find('table',{'id':'searchtable'})
#retorna uma lista com todas as linhas (<tr>) da tabela
linhas_tabela = tabela.findAll('tr')
i=0
for linha in linhas_tabela:
i+=1
#encontra a primeira coluna (descricao) da linha
coluna_descricao = linha.find('td')
#o atributo contents contem uma lista com o conteudo da tag
nome_projeto = coluna_descricao.find('a').contents[0]
descricao_projeto = coluna_descricao.contents[2].strip()
print 'Projeto '+str(i)+': '+nome_projeto
print descricao_projeto
print '--------------------------------'
Legal, não? Isso é uma demonstração básica do poder que o BeautifulSoup tem para percorrer e extrair informações em (X)HTML. O mais interessante de tudo é que quase todos os métodos da classe BeutifulSoup.Tag retornam a própria referência, o que quer dizer que podemos fazer chains (cadeias) de comando como essas:
nome_do_primeiro_projeto_da_pesquisa = soup.find('table',{'id':'searchtable'}).findAll('tr')[0].find('td').find('a').contents[0]Isso é realmente muito útil quando se quer economizar código. E o mais interessante de tudo: é bem legível (mas não necessariamente fácil de interpretar… quem ler seu código assim terá que ter uma boa noção do BeautifulSoup).
Baixando os arquivos do projeto
Essa parte é relativamente fácil. O SF é um serviço muito bom, até para o nosso pequeno experimento ![]()
A URL que o link “Download Now” aponta sofre 3 redirects até o arquivo final. Tudo isso serve para o SF determinar a sua localização e apontar o mirror mais próximo de você. Então a única coisa que precisamos fazer é passar esse link para o wget (um programa bem útil para download de arquivos na Web presente na maioria das distribuições Linux. Um clone para Windows pode ser encontrado aqui) e ele irá baixar o projeto para nós. Simples, não?
Então vamos ao código:
import urllib,urllib2
from subprocess import call
base_url = 'http://sourceforge.net'
busca = raw_input('Pesquisar por: ')
variaveis_get = urllib.urlencode({'type_of_search':'soft','words':busca})
req = urllib2.Request(base_url+'/search/',variaveis_get)
response = urllib2.urlopen(req).read()
soup = BeautifulSoup(response)
# procura pela tabela com id=searchtable
tabela = soup.find('table',{'id':'searchtable'})
#retorna uma lista com todas as linhas da tabela
linhas_tabela = tabela.findAll('tr')
i=0
links_download = []
for linha in linhas_tabela:
i+=1
coluna_descricao = linha.find('td')
nome_projeto = coluna_descricao.find('a').contents[0]
link_download = linha.find('a',{'class':'downloadnow'})['href']
links_download.append(link_download)
descricao_projeto = coluna_descricao.contents[2].strip()
print 'Projeto '+str(i)+': '+nome_projeto
print descricao_projeto
print '--------------------------------'
opcao = int(raw_input('Qual projeto gostaria de baixar? '))
opcao_url = base_url+links_download[opcao-1]
call(['wget',opcao_url]) #chama o wget com o link para download
A única coisa que eu fiz de novo foi adicionar o link de download em uma lista e perguntar ao usuário qual projeto ele deseja baixar. Então, eu passei o link de download para o wget que fez o serviço de baixar o arquivo pra mim. Pronto! Nosso crawler de exemplo do SF.net está pronto ![]()
No proximo post eu mostrarei como guardar valores de sessão para percorrermos páginas protegidas por senha. See ya!
Criando Web crawlers em Python – Parte I
Ir para: Criando Web Crawlers em Python – Parte II
Ir para: Criando Web Crawlers em Python – Parte III
Iniciemos com a definição de Web crawler:
Um Web crawler é um programa de computador que navega pela World Wide Web de maneira metódica e automatizada. [Wikipedia]
Os Web crawlers (ou simplesmente crawlers) têm um papel importante na nossa vida hoje. Como exemplos clássicos de Web crawlers, podemos citar o Google Bot, o Yahoo! Slurp e o MsnBot. Sem esses crawlers, não teríamos a busca do Google, do Yahoo! ou do Bing.
O objetivo dessa série de tutoriais não é ensinar criar crawlers de alta disponibilidade, alta performance e distribuídos mas mostrar como eles podem ser construidos sem muito custo ou complexidade, de forma que atenda uma necessidade específica. Entenda necessidade específica como download de imagens de um conjunto de páginas, um sistema de download de arquivos de hosts como o Rapidshare ou MegaUpload (sim, é possível!) ou uma busca e categorização de informações distribuídas entre diversos sites (como editais de concursos em portais).
Aspectos legais e ética de crawlers:
Antes de continuar lendo esta série, gostaria de informar que não é todo site que lhe dá permissão de acesso por meios de robores de busca. Procure sempre ler as políticas de uso e sempre respeite o robots.txt: se não é pra você crawlear determinada página, não o faça.
O processo:
Preste atenção aos componentes do crawler:
- Fila: pode conter uma ou mais URLs. Dependendo do tamanho da fila, a mesma pode estar no disco, ao invés de somente na memória.
- Downloader: geralmente costuma-se colocar um downloader multithreaded. No nosso primero exemplo vamos fazer um downloader singlethreaded para evitar complexidade.
- Parser: esse é o mais interessante. Ele vai extrair informações da página baixada pelo downloader segundo as especificações passadas pra ele. É pra ele que você irá dizer que quer extrair todas as imagens de uma determinada página.
- Storage: pode ser qualquer tipo de armazenamento em qualquer tipo de mídia. Não iremos persistir os dados nessa primeira parte do tutorial.
Nota-se que esse diagrama não representa um crawler de alta performance, pois ele mal diz sobre a arquitetura do mesmo, não diz sobre hardware, links de rede, paralelismo e tudo mais, mas serve com o intuito de explicar o básico do sistema.
Maos à obra: fazendo um downloader com uma fila simples
Para download de páginas e arquivos na Web, nós usaremos o urlib e a urlib2 do Python. Apesar das versões das libs, uma complementa a outra. Por exemplo, só se pode mudar os headers HTTP na urllib2 e o método urlencode só está disponível na urllib. Contudo, nós usaremos mais a urllib2. O código:
url = "http://www.google.com"
request = urllib2.Request(url)
response = urllib2.urlopen(request)
document = response.read()
print document
Execute esse código e se tudo der certo você verá o código fonte da página inicial do Google!
Bem, só ter o código fonte da página não é muito útil. Precisamos extrair informações dela. Uma forma de que podemos adotar é por Regex (expressão regular), mas usar regex dificultaria nosso trabalho e fugiríamos da idéia de deixar as coisas simples.
Um grande problema da Web hoje é que muitos desenvolvedores escrevem código (X)HTML mal formado e isso dificulta um pouco nossa vida. Felizmente isso não é um impedimento grave e podemos contorna-lo com o BeautifulSoup. O BeautifulSoup faz um excelente trabalho transformando o código de marcação (HTML, XML ou XHTML) em objetos que podem ser facilmente manipulados. De acordo com o esquema mostrado apresentado aqui, o BeautifulSoup faz parte do parser.
Façamos algo útil: vamos listar todos os links da página inicial do Google.
from BeautifulSoup import BeautifulSoup
url = "http://www.google.com"
request = urllib2.Request(url)
response = urllib2.urlopen(request)
document = response.read()
#normaliza o documento para que o mesmo seja acessível via objetos
soup = BeautifulSoup(document)
# retorna uma lista com todos os links do documento
links = soup.findAll('a')
for link in links:
print link
Fácil não?!
Se tudo correr bem, você deve ver uma saída igual a essa:
<a href="http://images.google.com.br/imghp?hl=pt-BR&tab=wi">Imagens</a><a href="http://video.google.com.br/?hl=pt-BR&tab=wv">Vídeos</a><a href="http://maps.google.com.br/maps?hl=pt-BR&tab=wl">Mapas</a><a href="http://news.google.com.br/nwshp?hl=pt-BR&tab=wn">Notícias</a><a href="http://www.orkut.com/Home.aspx?hl=pt-BR&tab=w0">Orkut</a><a href="http://mail.google.com/mail/?hl=pt-BR&tab=wm">Gmail</a><a href="http://www.google.com.br/intl/pt-BR/options/" style="text-decoration:none"><u>mais</u> »</a><a href="/url?sa=p&pref=ig&pval=3&q=http://www.google.com.br/ig%3Fhl%3Dpt-BR%26source%3Diglk&usg=AFQjCNEufhwNAC9POZqcS5r7r07CUPbvAA">iGoogle</a><a href="/preferences?hl=pt-BR">Configurações da pesquisa</a><a href="https://www.google.com/accounts/Login?hl=pt-BR&continue=http://www.google.com.br/">Fazer login</a><a href="/search?q=Vancouver+2010&ct=olympics10-sskating-hp&oi=ddle"><img src="/logos/olympics10-sskating-hp.png" width="523" height="170" border="0" alt="Vancouver 2010" title="Vancouver 2010" id="logo" onload="window.lol&&lol()" /></a><a href="/advanced_search?hl=pt-BR">Pesquisa avançada</a><a href="/language_tools?hl=pt-BR">Ferramentas de idiomas</a><a href="/intl/pt-BR/ads/">Soluções de publicidade</a><a href="/services/">Soluções empresariais</a><a href="/intl/pt-BR/about.html">Tudo sobre o Google</a><a href="http://www.google.com/ncr">Google.com in English</a><a href="/intl/pt-BR/privacy.html">Privacidade</a><a href="http://images.google.com.br/imghp?hl=pt-BR&tab=wi">Imagens</a><a href="http://video.google.com.br/?hl=pt-BR&tab=wv">Vídeos</a><a href="http://maps.google.com.br/maps?hl=pt-BR&tab=wl">Mapas</a><a href="http://news.google.com.br/nwshp?hl=pt-BR&tab=wn">Notícias</a><a href="http://www.orkut.com/Home.aspx?hl=pt-BR&tab=w0">Orkut</a><a href="http://mail.google.com/mail/?hl=pt-BR&tab=wm">Gmail</a><a href="http://www.google.com.br/intl/pt-BR/options/" style="text-decoration:none"><u>mais</u> »</a><a href="/url?sa=p&pref=ig&pval=3&q=http://www.google.com.br/ig%3Fhl%3Dpt-BR%26source%3Diglk&usg=AFQjCNEufhwNAC9POZqcS5r7r07CUPbvAA">iGoogle</a><a href="/preferences?hl=pt-BR">Configurações da pesquisa</a><a href="https://www.google.com/accounts/Login?hl=pt-BR&continue=http://www.google.com.br/">Fazer login</a><a href="/search?q=Vancouver+2010&ct=olympics10-sskating-hp&oi=ddle"><img src="/logos/olympics10-sskating-hp.png" width="523" height="170" border="0" alt="Vancouver 2010" title="Vancouver 2010" id="logo" onload="window.lol&&lol()" /></a><a href="/advanced_search?hl=pt-BR">Pesquisa avançada</a><a href="/language_tools?hl=pt-BR">Ferramentas de idiomas</a><a href="/intl/pt-BR/ads/">Soluções de publicidade</a><a href="/services/">Soluções empresariais</a><a href="/intl/pt-BR/about.html">Tudo sobre o Google</a><a href="http://www.google.com/ncr">Google.com in English</a><a href="/intl/pt-BR/privacy.html">Privacidade</a>
Na verdade, a linha
não retorna uma lista de strings, mas uma lista de objetos que representam tags em HTML (classe BeautifulSoup.Tag). Logo, há outras propriedades e métodos que podemos explorar. Digamos que você só queira as URLs dos links e não está interessado no texto ou na marcação. O nosso código ficaria assim:
from BeautifulSoup import BeautifulSoup
url = "http://www.google.com"
request = urllib2.Request(url)
response = urllib2.urlopen(request)
document = response.read()
#normaliza o documento para que o mesmo seja acessível via objetos
soup = BeautifulSoup(document)
# retorna uma lista com todos os links do documento
links = soup.findAll('a')
for link in links:
print link['href']
Como saída, você pode obter algo do tipo:
http://images.google.com.br/imghp?hl=pt-BR&tab=wi http://video.google.com.br/?hl=pt-BR&tab=wv http://maps.google.com.br/maps?hl=pt-BR&tab=wl http://news.google.com.br/nwshp?hl=pt-BR&tab=wn http://www.orkut.com/Home.aspx?hl=pt-BR&tab=w0 http://mail.google.com/mail/?hl=pt-BR&tab=wm http://www.google.com.br/intl/pt-BR/options/ /url?sa=p&pref=ig&pval=3&q=http://www.google.com.br/ig%3Fhl%3Dpt-BR%26source%3Diglk&usg=AFQjCNEufhwNAC9POZqcS5r7r07CUPbvAA /preferences?hl=pt-BR https://www.google.com/accounts/Login?hl=pt-BR&continue=http://www.google.com.br/ /search?q=Vancouver+2010&ct=olympics10-sskating-hp&oi=ddle /advanced_search?hl=pt-BR /language_tools?hl=pt-BR /intl/pt-BR/ads/ /services/ /intl/pt-BR/about.html http://www.google.com/ncr /intl/pt-BR/privacy.html
Como você pode ver, o BeautifulSoup é quase uma mãe quando o assunto é extrair informações de HTML
.
No próximo tutorial, eu abordarei usos mais avançados do BeautifulSoup e mostrarei como usar os métodos GET e POST e também mostrarei uma pequena aplicação para procurar e baixar arquivos do SourceForge.net. Até o próximo!
Python e computacao cientifica
Há algum tempo eu venho notando algumas aplicações desenvolvidas em Python e desde então eu venho me perguntando: será mesmo possível utilizar Python, de forma eficiente, no meio científico? Bem, minha pesquisa me leva a crer que sim, absolutamente.
O Guido Van Rossum, criador e atual mantenedor da linguagem, postou recentemente em seu blog sobre a Py4Science, um workshop que visou demonstrar como o Python está sendo útil na prática em pesquisas científicas.
Eu sabia que Python era perfeitamente aplicável à pesquisas científicas, seja pelo poder da linguagem ou pelas bibliotecas e frameworks que ela provê para facilitar o desenvolvimento de aplicações científicas, mais notavelmente o SciPy, NumPy e o MatPlotLib. O me impressionou foi ver o nível dos projetos envolvidos:
- O time do Telescópio Hubble utiliza Python há mais de 10 anos
- O pessoal da Universidade de Washington está trabalhando no SAGE, um software com o objetivo de servir de alternativa viável ao Matlab, Mathematica, Maple e Magma.
- Nipype, software para criação de interfaces de softwares de neuroimagem.
- SymPy, biblioteca para matemática simbólica, escrita em Python puro.
- Um projeto de $1 bilhão do governo indiano para melhorar a educação na Índia. Inclui o Departamento de Engenharia Aeroespacial em IIT Bombay na educação de Ciência e Engenharia. Mais em http://fossee.in/
- Biopython, conjunto de ferramentas para computação biológica.
- Vários outros.
Recentemente, eu vi uma discussão no Google Groups em que o autor pergunta por que o SciPy é melhor que o Matlab. Tá certo que uma pergunta dessas é bem tendenciosa (um amigo disse que aconteceria o mesmo na lista do Matlab), mas eu li toda a discussão e vi coisas bastante interessante, principalmente quando dizem sobre o f2py. Para quem tem código legado em Fortran, quer mudar e tá com qualquer dificuldade, essa é uma ótima oportunidade.
Algo que eu não posso deixar de falar é da Python in Science Conference (SciPy Conference), uma super conferência anual sobre SciPy e Python em computação científica que acontece no Caltech, Califórnia. Ainda não vi nenhum trabalho brasileiro lá. Já é hora, hein?
Eu sou longe de ser um cientista de renome para falar como o Python tem me ajudado em minhas pesquisas científicas. Mas, como acadêmico, eu posso dizer que serve perfeitamente para meus trabalhos universitários e eu percebo que a vida para outros acadêmicos poderia ser mais simples se eles utilizassem Python. Somente o fato de não precisar lidar com ponteiros ou gerenciamentos de baixo nível atrai algumas pessoas. Outra coisa importante: Python é livre e gratuito. Você não precisa piratear ou comprar uma cópia cara como algumas pessoas fazem com o Matlab e outros softwares ![]()
Links úteis:
- http://groups.google.it/group/scipy-user/browse_thread/thread/0eb1517095963c9a#
- http://fperez.org/py4science/index.html
- http://www.scipy.org
- http://conference.scipy.org/
- http://neopythonic.blogspot.com
==EDIT==
Por algum motivo ainda desconhecido, o título não está aparecendo com os devidos acentos. Irei alterar quando eu souber o que está acontecendo.






