Archive for the ‘Python’ Category
Quando uma deficiência pode se tornar uma feature numa linguagem de programação
Estava lendo um antigo post do Obie Fernandez onde ele lista várias razões do porquê Java é uma porcaria (em tom de brincadeira, claro =). Numa das razões citadas por ele, há o seguinte trecho:
8 ) Java has good debugging support
Java has great and powerful debugging (which is very necessary since most Java code sucks so much ass). Ruby has ridiculously pitiful debugging support, which means our code actually needs to be well-tested and readable.
Traduzindo:
8 ) Java possui um bom suporte a debugging
Java possui um excelente e poderoso debugging (o que é necessário, já que boa parte de códigos em Java são muito ruins). Ruby tem um suporte a debugging lamentavelmente ridículo, o que significa que nosso código realmente precisa ser bem testado e legível.
Vendo por este lado, Ruby é uma excelente linguagem para quem sabe fazer direito e uma droga pra quem está acostumado com IDEs com Intellisense e checagem de erros pelo compilador.
Outras linguagens possuem problemas que são resolvidos facilmente com o uso de testes unitários. Um problema clássico que acontece com o PHP, o White Screen of Death (um erro que acontece com o script que nenhuma informação ou mensagem de erro é apresentada), pode ser resolvido com o uso de testes.
Alguns “problemas” advindos com linguagens dinâmicas é com a mudança de tipo de uma determinada variável, por exemplo:
x = 1
y = 3
def soma(self):
return self.x + self.y
op = Op()
op.y = "aha!"
op.soma() #Ooopss... TypeError
Este é um tipo de problema que vem com a checagem de tipo em tempo de execução, ao invés de ser feito em tempo de compilação. Muitas pessoas que não possuem a mesma visão do Obie, encaram isto como um problema.
Opinionated Software
Um trecho muito conhecido do Getting Real é o trecho que fala sobre opinionated software:
Your app should take sides
Some people argue software should be agnostic. They say it’s arrogant for developers to limit features or ignore feature requests. They say software should always be as flexible as possible.
We think that’s bullshit. The best software has a vision. The best software takes sides. When someone uses software, they’re not just looking for features, they’re looking for an approach. They’re looking for a vision. Decide what your vision is and run with it.
Traduzindo:
Seu aplicativo deve tomar partido
Algumas pessoas defendem que o software deve ser agnóstico. Dizem que é arrogante da parte dos desenvolvedores limitar a funcionalidade ou ignorar pedidos de novos recursos. Dizem que o software deve ser sempre o mais flexível possível.
Para nós isso é papo-furado. O melhor software traz consigo uma visão. O melhor software toma partido. Quando alguém usa um software, não está procurando apenas recursos, está procurando uma abordagem. Está procurando uma visão. Decida qual é sua visão e atenha-se a ela.
Esta mesma idéia permeia linguagens de programação. Você não achará um linguagem que sirva bem para todos os tipos de casos. Python, por exemplo, não é uma boa linguagem se você quiser usar threads pesadamente. Erlang, apesar de ser uma excelente escolha para programação distribuída e paralela, enfrenta um certo preconceito por ser funcional e não ter suporte à orientação a objetos. C#.NET é uma boa linguagem para criação de aplicativos comerciais, mas não é totalmente portável para todas as plataformas (o Mono mal suporta o .NET 3.5 e o framework já está na versão 4).
Conclusão: antes de reclamar de uma linguagem de programação, saiba que toda linguagem tem um approach, um foco e veja se esta “falha” pode ser algo útil… muitas vezes o é ![]()
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? ![]()
Apresentando o Scrapy: Framework Python para crawling.
Se você acompanha este blog, deve conhecer as séries sobre crawling que aqui foram apresentadas (Parte 1, Parte 2, Parte 3, Parte 4). Por ser apenas uma evolução, mas não necessariamente uma seqüência dos outros 4 posts, este não é a parte 5 ![]()
Criar soluções temporárias e/ou triviais para extração de dados de forma estruturada na Web é uma tarefa relativamente fácil e divertida se você usar Python e uma biblioteca user-friendly como o BeautifulSoup ou o lxml. O problema é quando o projeto começa a ficar sério e crescer mais do que você previu. Nestas situações, um framework que lhe forneça alguma padronização de código e com várias abstrações e helpers para tarefas mais comuns em crawling vem muito bem a calhar.
Aí que entra o Scrapy: um framework para crawling com várias funcionalidades bem legais:
- Abstração das estranhezas da urllib e urllib2.
- Uso do lxml, já que o BeautifulSoup está depreciada.
- Seletores usando XPath.
- Suporte nativo para exportação de dados em XML, JSON e CSV (Protocol Buffers em breve!)
- Download automático de imagens.
- Arquitetura django-like.
- Monitoramento do crawling usando telnet ou um webservice.
- Shell Script interativo para você testar seu crawler na prática.
- Documentação completa e de fácil compreensão.
Eu pretendo utilizar o Scrapy no meu trabalho de conclusão de curso e com isso contribuir com o projeto ativamente (reunião com o orientador hoje para terminar de decidir isso), provavelmente com a adição de templates de crawling (algo parecido com o WPT – Website Parse Template) utilizando Protocol Buffers para adicionar algumas flexibilidades aos templates. Você pode acompanhar esta minha investida no meu branch no repositório do Scrapy no Github.
See ya!
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!





