Herberth Amaral

Software development adventures

Archive for the ‘computação científica’ tag

Criando Web crawlers (distribuidos) em Python – Parte IV

with 8 comments

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

Arquitetura básica de um sistema distribuído com PyRO

Arquitetura básica de um sistema distribuído com PyRO

  1. 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).
  2. Rede: essa parte é obvia ;)
  3. 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:

# -*- coding: utf-8 -*-
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:

import Pyro.core

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:

PyRO nameserver

Arquitetura hipotética usando o nameserver do PyRO

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:

import Pyro.core

objeto = Pyro.core.getProxyForURI('PYRONAME://obj')

print "Servidor retornou:"+objeto.remoteMethod('Herberth')

Servidor:

# -*- coding: utf-8 -*-
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:

#crawl.py => salve com esse nome, ok?
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.core
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:

import Pyro.core

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:

  1. Pega o próximo link à baixar.
  2. Baixa-o.
  3. Reporta para o objeto que baixou o link.
  4. Adiciona mais links na fila de download.

Eis o sistema acima rodando com dois clientes:

Dois clientes rodando paralelamente

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!

Written by Herberth Amaral

May 6th, 2010 at 10:31 am

Python e computacao cientifica

with 3 comments

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 SciPyNumPy 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:

==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.

Written by Herberth Amaral

November 29th, 2009 at 10:28 pm

Posted in Python

Tagged with ,