Web Scraping com Scrapy – primeiros passos

Imagine que você queira extrair conteúdo da Web que não esteja em apenas uma página só: você precisa de uma maneira de “navegar” no site para as páginas que realmente contém as informações úteis. Por exemplo, você pode estar interessado nas notícias destaques do dia no Portal Brasil.gov.br, mas somente aquelas das seções “Infraestrutura” e “Ciência e Tecnologia”.

webpage-brasil-links

Bem, há uns tempos atrás, já mostramos aqui no blog como usar a biblioteca requests para acessar páginas disponíveis na Web usando nossa linguagem predileta. Também mostramos como usar a biblioteca BeautifulSoup para facilitar a extração do conteúdo útil da página, o que chamamos de Web Scraping. Hoje, vamos mostrar como usar o framework Scrapy, que contém todas essas funcionalidades e muitas outras mais, de maneira que agiliza bastante resolver problemas como esse da introdução. Tudo em Python, lógico! =)

Vale notar então, que o Scrapy busca resolver não só a extração de conteúdo das páginas (scraping), mas também a navegação para as páginas relevantes para a extração (crawling). Para isso, uma ideia central no framework é o conceito de Spider — na prática, objetos Python com algumas características especiais que você escreve o código e o framework aciona.

Só para você ter uma ideia de como se parece, dê uma olhada no código de um programa que usa Scrapy para extrair informações (link, título e visualizações) de um canal do YouTube abaixo. Não se preocupe em entender esse código ainda, estamos mostrando aqui só para você ter um feeling do código com Scrapy. Ao terminar esse tutorial, você será capaz de entender e escrever programas como esse. =)

import scrapy
from scrapy.contrib.loader import ItemLoader

class YoutubeVideo(scrapy.Item):
    link = scrapy.Field()
    title = scrapy.Field()
    views = scrapy.Field()

class YoutubeChannelLister(scrapy.Spider):
    name = 'youtube-channel-lister'
    youtube_channel = 'LongboardUK'
    start_urls = ['https://www.youtube.com/user/%s/videos' % youtube_channel]

    def parse(self, response):
        for sel in response.css("ul#channels-browse-content-grid > li"):
            loader = ItemLoader(YoutubeVideo(), selector=sel)

            loader.add_xpath('link', './/h3/a/@href')
            loader.add_xpath('title', './/h3/a/text()')
            loader.add_xpath('views', ".//ul/li[1]/text()")

            yield loader.load_item()

Mas antes de começarmos a falar mais sobre o Scrapy, certifique-se de tê-lo instalado em sua última versão (dependendo do caso, você pode precisar usar o comando sudo ou a opção –user para o pip install):

pip install --upgrade scrapy

Nota: dependendo do seu ambiente Python, a instalação pode ser um pouco enrolada por causa da dependência do Twisted. Se você usa Windows, confira as instruções específicas no guia de instalação oficial. Se você usa uma distribuição Linux baseada em Debian, pode querer usar o repositório APT oficial do Scrapy. Se você está usando o pip no Ubuntu, pode precisar instalar os pacotes libffi-dev, libssl-dev, libxml2-dev e libxslt1-dev antes.

Para seguir este tutorial, você precisará do Scrapy com número de versão 0.24 para cima. Você pode verificar a versão do Scrapy instalada com o comando:

python -c 'import scrapy; print('%s.%s.%s' % scrapy.version_info)'

A saída desse comando no ambiente que usamos para esse tutorial está assim:

$ python -c 'import scrapy; print("%s.%s.%s" % scrapy.version_info)'
0.24.2

A anatomia de uma aranha

spider_anatomy

Um Scrapy spider é responsável por definir como seguir os links “navegando” por um site (o que chamamos de crawling) e como extrair as informações das páginas em estruturas de dados Python. 

Para definir um spider mínimo, crie uma classe estendendo scrapy.Spider e dê um nome ao spider usando o atributo name:

import scrapy

class MinimalSpider(scrapy.Spider):
    """A menor Scrapy-Aranha do mundo!"""
    name = 'minimal'

Coloque isso em um arquivo com o nome minimal.py e rode o seu spider para conferir se está tudo certo, usando o seguinte comando:

scrapy runspider minimal.py

Caso estiver tudo certo, você verá na tela algumas mensagens do log marcadas como INFO e DEBUG. Caso houver alguma mensagem marcada com ERROR, significa que deu algo errado e você precisa conferir se tem algum erro no código do spider.

A vida de um spider começa com a geração de requisições HTTP (objetos do tipo Request) para o motor do framework acionar. A parte do spider responsável por isso é o método start_requests(), que retorna um iterable contendo as primeiras requisições a serem feitas para o spider.

Adicionando esse elemento ao nosso spider mínimo, ficamos com:

import scrapy

class MinimalSpider(scrapy.Spider):
    """A menor Scrapy-Aranha do mundo!"""
    name = 'minimal'

    def start_requests(self):
        return [scrapy.Request(url)
                for url in ['http://www.google.com', 'http://www.yahoo.com']]

O método start_requests() deve retornar um iterable de objetos scrapy.Request, que representam uma requisição HTTP a ser acionada pelo framework (incluindo URL, parâmetros, cookies, etc) e definem uma função a ser chamada para quando a requisição completar — uma função callback. 

Nota: Caso esteja familiarizado com implementar AJAX com JavaScript, essa maneira de trabalhar disparando requisições e registrando callbacks pode soar familiar.

No nosso exemplo, retornamos uma lista de requisições simples para o site do Google e do Yahoo, mas o método start_requests() também poderia ser implementado como um Python generator.

Se você tentou executar o exemplo como está agora, pode ter notado que ainda está faltando coisa, o Scrapy irá cuspir duas mensagens marcadas como ERROR, reclamando que um método não foi implementado:

....
  File "/home/elias/.virtualenvs/scrapy/local/lib/python2.7/site-packages/scrapy/spider.py", line 56, in parse
    raise NotImplementedError
exceptions.NotImplementedError:

Isso ocorre porque, como não registramos nenhuma função callback para os objetos Request, o Scrapy tentou chamar o callback padrão, que é o método parse() do objeto Spider. Vamos adicionar esse método ao nosso spider mínimo, para podemos executar o spider:

import scrapy

class MinimalSpider(scrapy.Spider):
    """A menor Scrapy-Aranha do mundo!"""
    name = 'minimal'

    def start_requests(self):
        return (scrapy.Request(url)
                for url in ['http://www.google.com', 'http://www.yahoo.com'])

    def parse(self, response):
        self.log('ACESSANDO URL: %s' % response.url)

Se você executar novamente agora o spider com o comando: scrapy runspider minimal.py deverá observar na saída algo semelhante a:

2014-07-26 15:39:56-0300 [minimal] DEBUG: Crawled (200) <GET http://www.google.com.br/?gfe_rd=cr&amp;ei=_PXTU8f6N4mc8Aas1YDABA> (referer: None)
2014-07-26 15:39:56-0300 [minimal] DEBUG: ACESSANDO URL: http://www.google.com.br/?gfe_rd=cr&amp;ei=_PXTU8f6N4mc8Aas1YDABA
2014-07-26 15:39:57-0300 [minimal] DEBUG: Redirecting (302) to <GET https://br.yahoo.com/?p=us> from <GET https://www.yahoo.com/>
2014-07-26 15:39:58-0300 [minimal] DEBUG: Crawled (200) <GET https://br.yahoo.com/?p=us> (referer: None)
2014-07-26 15:39:58-0300 [minimal] DEBUG: ACESSANDO URL: https://br.yahoo.com/?p=us

Para deixar o código do nosso spider ainda mais enxuto, podemos nos aproveitar do funcionamento padrão do método start_requests(): caso você não o defina, o Scrapy irá criar requisições para uma lista de URLs num atributo com o nome start_urls — exatamente o que estamos fazendo. Portanto, podemos reduzir o código acima e manter o mesmo funcionamento, usando:

import scrapy

class MinimalSpider(scrapy.Spider):
    """A menor Scrapy-Aranha do mundo!"""
    name = 'minimal'
    start_urls = [
        'http://www.google.com',
        'http://www.yahoo.com',
    ]

    def parse(self, response):
        self.log('ACESSANDO URL: %s' % response.url)

Como no método parse() mostrado acima, todos os callbacks recebem o conteúdo da resposta da requisição HTTP como argumento (em um objeto Response). É dentro do callback, onde já temos o conteúdo das páginas que nos interessam, que fazemos a extração das informações, ou seja, o data scraping propriamente dito.

excited-scrapy

Callbacks, Requests & Items

As funções registradas como callback associadas às requisições podem retornar um iterable de objetos, em que cada objeto pode ser:

  • um objeto de uma classe scrapy.Item que você define para conter os dados coletados da página
  • um objeto da classe scrapy.Request representando ainda outra requisição a ser acionada (possivelmente registrando outro callback)

Com esse esquema de requisições e callbacks que podem gerar novas requisições (com novos callbacks), você pode programar a navegação por um site gerando requisições para os links a serem seguidos, até chegar nas páginas com os itens que nos interessam. Por exemplo, para um spider que precise extrair produtos de um site de compras navegando em páginas por categorias, você poderia usar uma estrutura como a seguinte:

import scrapy

class SkeletonSpider(scrapy.Spider):
    name = 'spider-mummy'
    start_urls = ['http://www.someonlinewebstore.com']

    def parse(self, response):
        for c in [...]:
            url_category = ...
            yield scrapy.Request(url_category, self.parse_category_page)

    def parse_category_page(self, response):
        for p in [...]:
            url_product = ...
            yield scrapy.Request(url_product, self.parse_product)

    def parse_product(self, response):
        ...

Na estrutura acima, o callback padrão — método parse() — trata a resposta da primeira requisição ao site da loja e gera novas requisições para as páginas das categorias, registrando outro callback para tratá-las — o método parse_category_page(). Este último faz algo parecido, gerando as requisições para as páginas dos produtos, desta vez registrando um callback que extrai os objetos itens com os dados do produto.

Por que eu preciso definir classes para os itens?

O Scrapy propõe que você crie algumas classes que representem os itens que você pretende extrair das páginas. Por exemplo, se você deseja extrair os preços e detalhes de produtos de uma loja virtual, poderia representar uma classe como a seguinte:

import scrapy

class Produto(scrapy.Item):
    descricao = scrapy.Field()
    preco = scrapy.Field()
    marca = scrapy.Field()
    categoria = scrapy.Field()

Como pode ver, são simples subclasses de scrapy.Item, em que você adiciona os campos desejados (objetos da classe scrapy.Field). Você pode usar uma instância dessa classe como se fosse um dicionário Python:

>>> p = Produto()
>>> p['preco'] = 13
>>> print p
{'preco': 13}

A maior diferença para um dicionário tradicional é que um Item, por padrão, não permite você atribuir um valor para uma chave que não foi declarada como campo:

>>> p['botemo'] = 54
...
KeyError: 'Produto does not support field: botemo'

A vantagem de definir classes para os itens é que isso permite você aproveitar outros recursos do framework que funcionam para essas classes. Por exemplo, o recurso de exportação de dados possibilita escolher entre exportar os itens coletados para JSON, CSV, XML, etc. Ou ainda, o esquema de pipeline de itens, que permite você plugar outros processamentos em cima dos itens coletados (coisas tipo, validar o conteúdo, remover itens duplicados, armazenar no banco de dados, etc).

Let’s do some scraping!

Para fazer o scraping propriamente dito, isto é, a extração dos dados da página, é legal você conhecer XPath, uma linguagem feita para fazer consultas em conteúdo XML — base do mecanismo de seletores do framework. Caso não conheça XPath você pode usar seletores CSS no Scrapy, mas encorajamos você a conhecer XPath mais de perto, pois ela permite expressões mais poderosas do que CSS (de fato, as funções de CSS no Scrapy funcionam convertendo os seletores CSS para expressões com XPath).

Você pode testar o resultado de expressões XPath ou CSS para uma página usando o Scrapy shell. Rode o comando:

scrapy shell http://pt.stackoverflow.com

Esse comando dispara uma requisição para a URL informada e abre um shell Python (ou IPython, caso o tenha instalado) disponibilizando alguns objetos para você explorar. O objeto mais importante é o response, que contém a resposta da requisição HTTP e equivale ao argumento response recebido pelas funções de callback.

dog-excited-tem_ate_um_shell

>>> response.url
'http://pt.stackoverflow.com'
>>> response.headers
{'Cache-Control': 'public, no-cache="Set-Cookie", max-age=60',
 'Content-Type': 'text/html; charset=utf-8',
 'Date': 'Fri, 01 Aug 2014 02:27:12 GMT',
 'Expires': 'Fri, 01 Aug 2014 02:28:12 GMT',
 'Last-Modified': 'Fri, 01 Aug 2014 02:27:12 GMT',
 'Set-Cookie': 'prov=cf983b7c-a352-4713-9aa8-6deb6e262b01; domain=.stackoverflow.com; expires=Fri, 01-Jan-2055 00:00:00 GMT; path=/; HttpOnly',
 'Vary': '*',
 'X-Frame-Options': 'SAMEORIGIN'}

Você pode usar os métodos xpath() e css() do objeto response para executar uma busca no conteúdo HTML da resposta:

>>> response.xpath(&amp;amp;amp;quot;//title&amp;amp;amp;quot;) # obtem o elemento &amp;amp;amp;lt;title&amp;amp;amp;gt; usando XPath
[<Selector xpath='//title' data=u'<title>Stack Overflow em Portugu\xeas</titl'>]
>>> response.css('title') # obtem o elemento <title> com seletor CSS
[<Selector xpath=u'descendant-or-self::title' data=u'<title>Stack Overflow em Portugu\xeas</titl'>]
>>> len(response.css('div')) # conta numero de elementos <div>
252

O resultado de chamar um desses métodos é um objeto lista que contém os objetos seletores resultantes da busca e possui um método extract() que extrai o conteúdo HTML desses seletores. Os objetos seletores contidos nessa lista, por sua vez, além de possuírem o método extract() para extrair o conteúdo dele, também possuem métodos xpath() e css() que você pode usar fazer uma nova busca no escopo de cada seletor.

Veja os exemplos abaixo ainda no mesmo Scrapy shell, que ajudam a esclarecer as coisas.

Extrai conteúdo HTML do elemento , acionando método extract() da lista de seletores (repare como o resultado é uma lista Python):

>>> response.xpath("//title").extract()
[u'<title>Stack Overflow em Portugu\xeas</title>']

Guarda o primeiro seletor do resultado numa variável, e aciona o método extract() do seletor (veja como agora o resultado é uma string):

>>> title_sel = response.xpath('//title')[0]
>>> title_sel.extract()
u'<title>Stack Overflow em Portugu\xeas</title>'

Aplica a expressão XPath text() para obter o conteúdo texto do seletor, e usa o método extract() da lista resultante:

>>> title_sel.xpath('text()').extract()
[u'Stack Overflow em Portugu\xeas']

Imprime a extração do primeiro seletor resultante da expressão XPath text() aplicada no seletor da variável title_sel:

>>> print title_sel.xpath('text()')[0].extract()
Stack Overflow em Português

Bem, dominando essa maneira de trabalhar com seletores, a maneira simples de extrair um item é simplesmente instanciar a classe Item desejada e preencher os valores obtidos usando essa API de seletores.

Veja abaixo o código de um spider usando essa técnica para obter as perguntas mais frequentes do StackOverflow brazuca:

import scrapy
import urlparse

class Question(scrapy.Item):
    link = scrapy.Field()
    title = scrapy.Field()
    excerpt = scrapy.Field()
    tags = scrapy.Field()

class StackoverflowTopQuestionsSpider(scrapy.Spider):
    name = 'so-top-questions'

    def __init__(self, tag=None):
        questions_url = 'http://pt.stackoverflow.com/questions'
        if tag:
            questions_url += '/tagged/%s' % tag

        self.start_urls = [questions_url + '?sort=frequent']

    def parse(self, response):
        build_full_url = lambda link: urlparse.urljoin(response.url, link)

        for qsel in response.css("#questions > div"):
            it = Question()

            it['link'] = build_full_url(
                qsel.css('.summary h3 > a').xpath('@href')[0].extract())
            it['title'] = qsel.css('.summary h3 &amp;amp;amp;gt; a::text')[0].extract()
            it['tags'] = qsel.css('a.post-tag::text').extract()
            it['excerpt'] = qsel.css('div.excerpt::text')[0].extract()

            yield it

Como você pode ver, o spider declara uma classe Item com o nome Question, e usa a API de seletores CSS e XPath para iterar sobre os elementos HTML das perguntas (obtidos com o seletor CSS #questions > div), gerando um objeto Question para cada com os campos preenchidos (link, título, tags e trecho da pergunta).

Duas coisas são interessantes que você note na extração feita no callback parse(): a primeira é que usamos um pseudo-seletor CSS ::text para obter o conteúdo texto dos elementos, evitando as tags HTML. A segunda é como usamos a função urlparse.urljoin() combinando a URL da requisição com conteúdo do atributo href para ter certeza que o resultado seja uma URL absoluta.

Coloque esse código em um arquivo com o nome top_asked_so_questions.py e execute-o usando o comando:

scrapy runspider top_asked_so_questions.py -t json -o perguntas.json

Se tudo deu certo, o Scrapy vai mostrar na tela os itens que extraiu e também escrever um arquivo perguntas.json contendo os mesmos itens. No fim da saída, devem aparecer algumas estatísticas da execução, incluindo a contagem dos itens extraídos:

2014-08-02 14:27:37-0300 [so-top-questions] INFO: Dumping Scrapy stats:
    {'downloader/request_bytes': 242,
     'downloader/request_count': 1,
     ...
     'item_scraped_count': 50,
     'log_count/DEBUG': 53,
     'log_count/INFO': 8,
     ...
     'start_time': datetime.datetime(2014, 8, 2, 17, 27, 36, 912002)}
2014-08-02 14:27:37-0300 [so-top-questions] INFO: Spider closed (finished)

question_block_little_dudes-are_belong_to_us

Argumentos aracnídeos

Talvez você notou que a classe do spider tem um construtor aceitando um argumento tag opcional. Podemos passar esse argumento para o spider para obter as perguntas frequentes com a tag python, usando a opção -a:

scrapy runspider top_asked_so_questions.py -t json -o perguntas.json -a tag=python

Usando esse truque você pode fazer spiders mais genéricos, que você passe alguns parâmetros e obtém um resultado diferente. Por exemplo, você poderia fazer um spider para sites que possuam a mesma estrutura HTML, parametrizando a URL do site. Ou ainda, um spider para um blog em que os parâmetros definam um período desejado para extrair posts e comentários.

Juntando tudo

Nas seções anteriores, você viu como fazer crawling com o Scrapy, navegando entre as páginas de um site usando o mecanismo de “navegação” criando requisições com funções callback. Viu também como usar a API de seletores para extrair o conteúdo da página em itens e executar o spider usando o comando scrapy runspider.

Agora, vamos juntar tudo isso em um spider que resolve o problema que apresentamos na introdução: vamos fazer scraping das notícias destaques do Portal Brasil, oferecendo uma opção para informar o assunto (Infraestrutura, Educação, Esporte, etc). Dessa forma, se apenas executar o spider, ele deve fazer scraping das notícias destaques na página inicial; caso informe um assunto, ele deve fazer scraping dos destaques da página daquele assunto.

Nota: Antes de começar a escrever um spider, é útil explorar um pouco as páginas do site usando o navegador e a ferramenta scrapy shell, assim você pode ver como o site é organizado e testar alguns seletores CSS ou XPath no shell. Existem também extensões para os browsers que permitem você testar expressões XPath em uma página: XPath Helper para o Chrome e XPath Checker para o Firefox. Descobrir a melhor maneira de extrair o conteúdo de um site com XPath ou CSS é mais uma arte do que uma ciência, e por isso não tentaremos explicar aqui, mas vale dizer que você aprende bastante com a experiência.

Veja como fica o código do spider:

import scrapy
import urlparse

class Noticia(scrapy.Item):
    titulo = scrapy.Field()
    conteudo = scrapy.Field()
    link = scrapy.Field()
    data_publicacao = scrapy.Field()

class PortalBrasilDestaques(scrapy.Spider):
    name = 'portal-brasil'

    def __init__(self, assunto=None):
        main_url = 'http://www.brasil.gov.br'
        if assunto:
            self.start_urls = ['%s/%s' % (main_url, assunto)]
        else:
            self.start_urls = [main_url]

    def parse(self, response):
        """Recebe a pagina com as noticias destaques, encontra os links
        das noticias e gera requisicoes para a pagina de cada uma
        """
        links_noticias = response.xpath(
            "//div/h1/a/@href"
            " | //div/h3/a/@href[not(contains(.,'conteudos-externos'))]"
        ).extract()

        for link in links_noticias:
            url_noticia = urlparse.urljoin(response.url, link)
            yield scrapy.Request(url_noticia, self.extrai_noticia)

    def extrai_noticia(self, response):
        """Recebe a resposta da pagina da noticia,
        e extrai um item com a noticia
        """
        noticia = Noticia()

        noticia['link'] = response.url
        noticia['titulo'] = response.xpath(&amp;amp;amp;quot;//article/h1/text()&amp;amp;amp;quot;)[0].extract()
        noticia['conteudo'] = response.xpath(
            "string(//div[@property='rnews:articleBody'])")[0].extract()
        noticia['data_publicacao'] = ''.join(
            response.css('span.documentPublished::text').extract()).strip()

        yield noticia

Da mesma forma como antes, você pode rodar o spider com:

scrapy runspider portal_brasil_destaques.py -t json -o destaques-capa.json

E para obter os destaques de cada seção, pode usar comandos como:

scrapy runspider portal_brasil_destaques.py -t json -o destaques-infraestrutura.json -a assunto=infraestrutura
scrapy runspider portal_brasil_destaques.py -t json -o destaques-ciencia-e-tecnologia.json -a assunto=ciencia-e-tecnologia

O código desse spider é bem semelhante ao anterior na sua estrutura, com o suporte a argumentos no construtor.

A principal diferença é que neste, o primeiro callback (método parse()) gera outras requisições para as páginas das notícias, que são tratadas pelo segundo callback: o método extrai_noticia(), que faz a extração do conteúdo da notícia propriamente dita.

A extração de conteúdo nesse último spider também está um pouco mais complexa, considerando a expressão XPath usada para obter os links das notícias, que filtra os links que contenham a string ‘conteudos-externos’ em seu endereço, pois não são links de notícias. Note como aproveitamos que Python concatena strings literais para quebrar a expressão XPath em duas linhas.

Conclusão

Se você chegou até aqui, parabéns! Aqui vai um troféu pra você:

trofeu-scrapy

Agora que você já aprendeu a escrever spiders Scrapy e está habilitado a baixar a Internet inteira no seu computador, tente não ser banido nos sites por aí! 😀

Visite a documentação oficial do Scrapy, tem bastante coisa legal lá, desde um tutorial ensinando a criar projetos Scrapy completos, perguntas frequentes, dicas para crawlings grandes, como depurar um spider, dicas para evitar ser banido e muito mais.

 

Links úteis:

Obrigado pela revisão, Valdir e Zé!
Anúncios

Desenvolvendo com Python e Google App Engine

Importante: a documentação oficial do Google App Engine aqui referenciada está disponível em português. Basta selecionar o idioma no canto inferior direito, caso esteja em outro idioma.

Sumário do post:

No último post nós vimos como desenvolver um serviço web bem simples usando Python e CGI. Mas, conforme vimos no próprio post, CGI não é o mecanismo mais adequado para dar suporte a um aplicativo web.

Do seu início até final da década de 90, a web era usada principalmente para a publicação de conteúdo em HTML. Porém, algum tempo depois ela passou a ser usada também como um ambiente para execução de aplicações completas.

O crescimento das aplicações possibilitou a observação de que boa parte dos projetos web tinham alguns componentes básicos em comum, gerando uma grande quantidade de retrabalho a cada novo projeto, o que acarretaria um alto custo para cada projeto, e também aumentaria as chances de o sistema apresentar defeitos. Visando eliminar boa parte do retrabalho, foram criados os frameworks de desenvolvimento, que fornecem vários serviços básicos para serem reutilizados pela aplicação, de forma que esta não precise reimplementá-los. (obs.: framework poderia ser traduzido como arcabouço)

A segurança de um aplicativo é um dos aspectos para os quais os frameworks possuem um conjunto de serviços para oferecer ao desenvolvedor. Isso é muito importante, pois não é todo programador que sabe como lidar com aspectos de segurança de uma forma adequada. Gerenciamento do banco de dados é outro serviço comumente oferecidos por frameworks para desenvolvimento web. Enfim, eles oferecem um ferramental bem interessante para que o desenvolvedor de uma aplicação web se preocupe principalmente com as funcionalidades e com a lógica de negócio do seu aplicativo, deixando um pouco “de lado” alguns aspectos que ficam sob responsabilidade do framework.

Alguns dos principais frameworks para desenvolvimento web em Python são:

O que nós vamos utilizar neste post não é apenas um framework, mas sim um conjunto completo para desenvolvimento, implantação e execução de aplicativos web, fornecido pela Google: o Google App Engine. Neste post, iremos apresentar o desenvolvimento de um aplicativozinho web que retorna alguma frase famosa para o usuário, algo como a frase do dia. Mas antes disso, vamos ver do que se trata o Google App Engine.

O Google App Engine

gaelogo

O Google App Engine (GAE) é uma plataforma para desenvolvimento de aplicativos web para serem hospedados na infraestrutura da Google. Além de oferecer uma interface bem interessante para gerenciamento dos aplicativos web (veja na figura abaixo), o GAE fornece de lambuja o balanceamento de carga da aplicação, espalhando a execução dela por vários servidores se assim for necessário, e alocando automaticamente mais recursos para a aplicação que estiver rodando nessa infraestrutura. Além da disponibilizar e gerenciar a infraestrura para o desenvolvedor, o GAE ainda provê uma série de facilidades para o desenvolvimento, como um framework para persistência de dados, para tratamento de requisições, para caching de dados, dentre outras coisas legais. E, pra ficar mais interessante ainda, é gratuito para quem quiser testar ou desenvolver aplicativos sem maiores pretensões.

http.5.1

Painel de controle do GAE

Bom, chega de propaganda e vamos ao que interessa!

Instalando e configurando o GAE

Como já foi comentado, a hospedagem de aplicativos escritos para o GAE fica por conta da Google, mas para podermos testar localmente nosso app, é preciso que instalemos um ambiente próprio para isso. Siga os passos abaixo:

  1. Baixe o arquivo correspondente à sua plataforma clicando aqui;
  2. Descompacte o arquivo acima no diretório de sua preferência;

Mais informações sobre a instalação do ambiente podem ser encontradas aqui.

Desenvolvendo nosso app

Vamos começar agora o desenvolvimento de nosso aplicativo de frase do dia. A primeira coisa a fazer é criar um diretório onde nosso app ficará localizado. Podemos chamar tal diretório de frasedodia, ou o nome que você achar mais conveniente. Dentro do diretório recém criado, crie um arquivo chamado frasedodia.py e outro chamado app.yaml. O primeiro é a nossa aplicação em si e o último é o arquivo que irá conter as configurações do nosso projeto.
A estrutura do nosso projeto ficará assim:

frasedodia/
  frasedodia.py
  app.yaml

Agora, precisamos escrever as configurações do nosso projeto. Para isso, cole o seguinte conteúdo dentro do arquivo app.yaml:

application: frasedodia
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: frasedodia.py

O mais importante a entender neste momento é a opção handlers. Dentro dela, temos um item chamado url com valor /.* e um atributo script com o nome do nosso arquivo .py como valor. Essa configuração significa que requisições para qualquer recurso dentro do projeto (/.*) serão encaminhadas para o arquivo frasedodia.py. Se por acaso quiséssemos que somente as requisições para os recursos contidos em uma pasta app fossem encaminhadas ao arquivo handlers.py, faríamos o seguinte:

handlers:
- url: /app/.*
  script: handlers.py

Assim, uma requisição para frasedodia.appspot.com/app/qualquercoisa seria encaminhada para o script definido na configuração acima (handlers.py). Agora vamos nos concentrar no código que irá atender às requisições do usuário.

Manipulador de requisições

Se quisermos tomar proveito das vantagens que o GAE nos fornece, é interessante que não usemos CGI. Vamos utilizar um framework para web bem simples chamado webapp2 que já é fornecido juntamente com o GAE.

O primeiro passo que devemos realizar é definir quais classes terão suas instâncias responsáveis por manipular requisições para determinadas URLs. Uma vez que já definimos que todas as requisições para o aplicativo serão direcionadas a frasedodia.py, devemos agora definir, dentro desse arquivo, o mapeamento de URLs para classes. Insira o código a seguir em frasedodia.py:

import webapp2
from google.appengine.ext.webapp.util import run_wsgi_app

mapeamento = [
    ('/', FraseDoDia)
]
app = webapp2.WSGIApplication(mapeamento)
run_wsgi_app(app)

No código acima, definimos que toda requisição para a raiz de nosso app será tratada por instâncias da classe FraseDoDia. Mas, a classe FraseDoDia não é uma classe qualquer. Ela deve obrigatoriamente estender a classe webapp2.RequestHandler, para que suas instâncias sejam também instâncias de RequestHandler. Nosso app irá criar uma instância da classe FraseDoDia e irá chamar nessa instância o método apropriado para tratar a requisição (get() para o caso de requisição GET ou post() para o caso de requisição POST). Esse método irá tratar a requisição e devolver uma resposta ao cliente. Tudo isso é feito automagicamente pelo framework de suporte, ficando ao nosso cargo apenas criar as classes que irão ser responsáveis por tratar as requisições.

Objetos do tipo RequestHandler possuem dois atributos muito importantes para o desenvolvedor:

  • self.request: é uma instância de Request, contendo a requisição recebida do usuário;
  • self.response: é uma instância de Response, contendo a mensagem a ser enviada como resposta à requisição do usuário.

Dessa forma, instâncias da classe RequestHandler devem fazer o seguinte, a cada requisição recebida:

  1. Tratar os dados recebidos através do objeto self.request;
  2. Gravar os dados a serem enviados ao usuário no objeto self.response.

Assim, devemos então implementar nossa classe FraseDoDia como uma subclasse de RequestHandler e nela implementar o método get() para retornar a frase para o cliente. Vamos então adicionar o código da classe ao nosso arquivo frasedodia.py:

import webapp2
from google.appengine.ext.webapp.util import run_wsgi_app

class FraseDoDia(webapp2.RequestHandler):

    def get(self):
        self.response.headers['Content-Type'] = 'text/json'
        self.response.out.write('{"frase": "Uma frase qualquer!"}')

mapeamento = [
    ('/', FraseDoDia)
]
app = webapp2.WSGIApplication(mapeamento)
run_wsgi_app(app)

Vendo o código acima, dá pra entender que quando ocorre uma requisição GET para o recurso / de nossa aplicação, um objeto do tipo FraseDoDia será designado pelo framework de suporte para tratar tal requisição, executando o método get(). Esse método, por sua vez, faz duas coisas:

  1. Adiciona ao cabeçalho da resposta HTTP — self.response.headers — o tipo do conteúdo a ser enviado como resposta ao cliente (text/json), de forma que o navegador saiba o que fazer com o conteúdo recebido;
  2. Escreve no corpo da resposta HTTP — self.response — o conteúdo a ser enviado ao cliente, que são dados representados em formato JSON.

Tudo o que precisamos fazer é implementar a classe que vai tratar das requisições à determinadas URLs, sem a necessidade de chamá-las nem nada. Quem faz isso é o ambiente de suporte provido pelo framework webapp.

Tendo implementado uma versão básica de nosso serviço, vamos colocar nosso ambiente de testes em funcionamento.

Executando o ambiente de testes

Vá até o local onde você extraiu o ambiente do GAE em sua máquina. Dentro dessa pasta, execute o seguinte comando:

./dev_appserver.py /caminho/para/o/projeto/frasedodia/

O caminho mostrado acima, passado como argumento para o programa dev_appserver.py, é referente ao diretório onde criamos os arquivos app.yaml e frasedodia.py.

O comando acima iniciou o servidor local para desenvolvimento e testes na porta 8080. Podemos acessar o serviço através da URL: http://localhost:8080/. Ao acessar essa URL, você terá como resposta o conteúdo JSON retornado pelo nosso serviço Web:

{"frase": "Uma frase qualquer!"}

Por enquanto estamos retornando apenas uma frase constante, e a idéia do serviço é retornar uma frase aleatória. Para tanto, temos algumas opções:

  • Manter algumas strings em memória em uma lista;
  • Manter registros no banco de dados e buscar um ao receber uma requisição.

Por fins de simplicidade, vamos seguir a primeira opção. Vamos adicionar uma nova classe chamada FraseAleatoriaDoDia, implementar o método get(), já que essa classe vai estender RequestHandler, e adicionar a nova classe no mapeamento de URLs, relacionado a URL /random. Veja o código completo abaixo:

import random
import webapp2
from google.appengine.ext.webapp.util import run_wsgi_app

class FraseDoDia(webapp2.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/json'
        self.response.out.write('{"frase": "Uma frase qualquer!"}')

class FraseAleatoriaDoDia(webapp2.RequestHandler):
    frases = [
        ('Insanity: doing the same thing over and over again and expecting different results', 'Albert Einstein'),
        ('The world is a dangerous place to live; not because of the people who are evil, but because of the people who don\'t do anything about it.', 'Albert Einstein'),
        ('A person who never made a mistake never tried anything new.', 'Albert Einstein'),
        ('Love all, trust a few, do wrong to none.', 'William Shakespeare'),
        ('A fool thinks himself to be wise, but a wise man knows himself to be a fool.', 'William Shakespeare'),
        ('Darkness cannot drive out darkness; only light can do that. Hate cannot drive out hate; only love can do that.', 'Martin Luther King, Jr.')
    ]

    def get(self):
        self.response.headers['Content-Type'] = 'text/json'
        i = random.randint(0, len(self.frases)-1)
        self.response.out.write('{"frase": "%s", "autor": "%s"}' % (self.frases[i][0], self.frases[i][1]))

mapeamento = [
    ('/', FraseDoDia),
    ('/random', FraseAleatoriaDoDia),
]
app = webapp2.WSGIApplication(mapeamento)
run_wsgi_app(app)

Rode o servidor de aplicação local com./dev_appserver.py /caminho/para/o/projeto/frasedodia/ e acesse http://localhost:8080/random para obter uma das frases.

Com o app funcionando, agora você pode registrar e fazer upload do mesmo para os servidores da Google, se assim quiser. Se desejar colocar em produção o app que implementamos aqui, siga as instruções contidas na documentação oficial, na seção que trata sobre o upload de um app.

Depois de fazer o upload de seu app, você poderá acessá-lo através de uma URL dentro de .appspot.com, como: http://id_da_aplicacao.appspot.com. Para ver informações sobre a sua aplicação, você pode acessar o painel de controle da sua conta no GAE através do endereço: appengine.google.com/.

O Google App Engine é uma ótima opção para dar suporte a uma aplicação web, visto que com ele eliminamos a necessidade de contratar um plano de hospedagem, de gerenciar o servidor web, de cuidar do balanceamento de carga, etc. Vale a pena dar uma estudada mais a fundo no assunto. 🙂

Aprenda mais