Acessando a API REST do Twitter

Dando sequência ao post anterior sobre APIs REST, este post irá mostrar como utilizar uma API REST “de verdade”: a API do Twitter.

A API do Twitter

A API REST do twitter, em sua versão 1.1, fornece acesso a vários recursos, dentre os quais podemos destacar: Tweets, Search, Direct Messages, Users, Friends & Followers e Trends. Para uma lista exaustiva dos recursos, veja a documentação oficial em dev.twitter.com/docs/api/1.1.

Porém, boa parte dos recursos oferecidos pela API necessita de autenticação para que sejam fornecidos para sua aplicação. Dessa forma, vamos ver rapidamente como sua aplicação pode se autenticar junto ao serviço do Twitter.

Autenticação

A API REST do twitter permite a autenticação através de dois mecanismos baseados no padrão OAuth:

  1. Autenticação application-only: a autenticação não fica vinculada a um usuário específico, mas sim a uma aplicação previamente registrada. Quando autenticado com esse mecanismo, sua aplicação não poderá realizar algumas operações típicas de um usuário, como postar tweets, por exemplo. Esse tipo de autenticação é mais indicado para aplicações que não terão um usuário interagindo com a rede social. Um exemplo seria um app que vai extrair dados do twitter para realizar análises.
  2. Autenticação de usuário: a autenticação se dá diretamente por um usuário, de forma que a aplicação possa realizar operações comuns a usuários. Esse tipo de mecanismo é mais indicado para o caso de aplicativos que vão interagir com a rede social pelo usuário, como um app que posta as músicas mais ouvidas pelo usuário na semana.

Neste post, usaremos somente o primeiro tipo de autenticação (application only).

Autenticação Application-only

Antes de mais nada, precisamos conhecer alguns conceitos básicos de autenticação usando o padrão OAuth. Para que possamos nos identificar como usuários de um serviço que utiliza um mecanismo de autenticação baseado em OAuth, é preciso que tenhamos os seguintes dados:

  1. API Key: uma chave utilizada para que o nosso app se identifique perante o Twitter.
  2. API Secret: um segredo usado pelo nosso app para provar que é o dono da API Key.
  3. Access Token: depois de identificado junto ao serviço, nosso app precisa enviar o access token para que o serviço possa verificar qual é o nível de acesso que o app possui.
  4. Access Token Secret: segredo usado pelo nosso app para provar que é o dono do access token.

Como Obter Essas Chaves com o Twitter

Para obter as chaves de acesso ao serviço é preciso primeiramente registrar um aplicativo junto ao Twitter. Acesse apps.twitter.com e clique no botão “Create new app” para registrar um aplicativo para acesso à API. Primeiramente, você terá que preencher alguns dados básicos, como mostrado na imagem abaixo:

twitter1

Após isso, você será redirecionado para a página de gerenciamento do seu app, como mostra a imagem abaixo.

twitter2

Vá até a aba “API Keys” para acessar a página de gerenciamento das chaves de acesso do seu app ao serviço REST do twitter. Você verá uma página semelhante à da imagem abaixo.

twitter3

Nela, clique no botão “Create my access token” (em vermelho na imagem acima) para que seja criado o token de acesso do app ao serviço. A imagem abaixo mostra a aba “API Keys” após termos criado as chaves de acesso necessárias.

 

twitter4

Agora que já temos todos os dados que precisamos para autenticar nosso aplicativo, vamos ver como fazer a autenticação junto ao serviço.

Autenticando no serviço

Para nos autenticarmos no serviço do Twitter, precisaremos da biblioteca requests-oauthlib (instalável via pip install requests_oauthlib). De posse dela e das chaves geradas na etapa anterior, vamos criar uma sessão OAuth da seguinte forma:

>>> from requests_oauthlib import OAuth1Session
>>> session = OAuth1Session(API_KEY, API_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)

Agora, podemos usar o objeto session recém criado para realizar acesso à API.

Acessando os recursos de busca da API

Vamos começar fazendo uma busca pelo termo #python, usando o recurso search fornecido pela API. De acordo com a documentação, uma busca simples pode ser feita passando um parâmetro q à URL https://api.twitter.com/1.1/search/tweets.json. Veja no código abaixo que estamos executando as requisições sobre o objeto session criado anteriormente:

>>> response = session.get('https://api.twitter.com/1.1/search/tweets.json?q=%23python')
>>> print response.status_code
200

Observe que passamos a string "%23python" ao parâmetro q.  "%23" é a representação do caractere "#" no esquema de codificação de URLs usado na web, por isso a utilizamos na URL enviada ao serviço ao invés do caractere "#". Para não termos que codificar a URL “na mão”, podemos utilizar a função requests.utils.quote() para codificar os caracteres da URL pra gente:

>>> print requests.utils.quote("#python")
%23python
>>> url = "https://api.twitter.com/1.1/search/tweets.json?q=%s"
>>> url = url % (requests.utils.quote("#python"))
>>> response = session.get(url)

O conteúdo retornado pelo serviço é uma string em formato JSON, e podemos decodificá-la usando a função json.loads:

>>> import json
>>> tweets = json.loads(response.content)

A chamada à função loads retorna um dicionário:

>>> print tweets.keys()
[u'search_metadata', u'statuses']

Os tweets encontrados são representados em uma lista dentro do dicionário, na posição de chave statuses:

>>> print len(tweets['statuses'])
15

Cada tweet é um dicionário dentro dessa lista:

>>> print type(tweets['statuses'][0])
<type 'dict'>

E para cada dicionário representando um tweet, temos os seguintes atributos:

>>> print tweets['statuses'][0].keys()
[u'contributors', u'truncated', u'text', u'in_reply_to_status_id', u'id',
 u'favorite_count', u'source', u'retweeted', u'coordinates', u'entities',
 u'in_reply_to_screen_name', u'in_reply_to_user_id', u'retweet_count', u'id_str',
 u'favorited', u'user', u'geo', u'in_reply_to_user_id_str', u'possibly_sensitive',
 u'lang', u'created_at', u'in_reply_to_status_id_str', u'place', u'metadata']

Podemos ver alguns atributos interessantes, como text e user:

>>> print tweets['statuses'][0]['text']
#python wanna *split* a #Flask application into separates modules?blueprints: 1)create it; 2)register in the *root* http://t.co/DlZxJrw1qq

Já o atributo user contém outro dicionário, com as informações do usuário:

>>> print tweets['statuses'][0]['user']['description']
Algorithms. Program languages theory. UML. Design pattens. Python. C. C++. PHP. Perl. vim. PostgreSQL. MariaDB. Unix. Me.

Enfim, de cada tweet podemos tirar várias informações interessantes. Podemos, por exemplo, imprimir os tweets ordenados pela quantidade de retweets (do mais para o menos retweetado). Cada tweet possui um atributo chamado "retweet_count", que usaremos na ordenação da lista de tweets contida em tweets["statuses"]. Veja o código abaixo:

>>> for t in sorted(tweets["statuses"], key=lambda x: x["retweet_count"], reverse=True):
        print t["text"]

Limites no acesso

Para evitar abusos, a maioria dos provedores de serviços web impõem limites na quantidade de requisições que um cliente pode fazer dentro de uma janela de tempo. Cada serviço possui suas regras, portanto é bom conhecê-las para que seu app não deixe de funcionar de forma inesperada. Esse tipo de limite é por vezes chamado de rate limiting e você pode descobrir como isso funciona no twitter lendo a seção da documentação deles que fala sobre isso.

Além desses limites, requisições de busca como as apresentadas anteriormente possuem limites na quantidade de valores que retornam por vez. Para evitar respostas muito grandes, o serviço do Twitter não retorna mais do que 100 tweets como resposta a uma única requisição. Por padrão, são retornados 15 tweets, mas esse valor pode ser alterado através do parâmetro count. Para obter 80 resultados nas requisições de pesquisa usadas anteriormente, poderíamos adicionar o parâmetro count com valor 80 à URL do serviço:

>>> url = "https://api.twitter.com/1.1/search/tweets.json?q=%s&count=%d"
>>> url = url % (requests.utils.quote("#python"), 80)
>>> response = session.get(url)
>>> tweets = json.loads(response.content)
>>> print len(tweets['statuses'])
80

E se precisarmos mais do que 100 resultados?

Se precisamos obter os primeiros 1000 resultados, teremos que fazer 10 requisições solicitando 100 resultados de cada vez. Porém, 10 requisições à mesma URL possuem boas chances de terem como resultado os mesmos tweets. Para que não recebamos sempre os mesmos 100 resultados, a API possibilita que informemos o id do tweet mais antigo recebido na última resposta, de forma que, na próxima resposta, somente serão inclusos aqueles tweets que possuírem id menor que o especificado. Assim, basta pegar o menor id dentro do grupo de tweets recebidos para solicitar os próximos 100 tweets. Assim como anteriormente, vamos solicitar os 100 primeiros tweets contendo a palavra "python":

>>> url = 'https://api.twitter.com/1.1/search/tweets.json?q=python&count=100'
>>> response = session.get(url)
>>> tweets = json.loads(response.content)
>>> print len(tweets['statuses'])
100

Para obtermos os próximos 100 tweets, precisamos descobrir o id do tweet mais antigo retornado na última resposta:

>>> oldest = min( for tweet in tweets['statuses']])-1

Vamos passar esse valor na próxima requisição em um parâmetro chamado max_id, que indica ao serviço o id do tweet mais recente que queremos na requisição. Ou seja, iremos pegar agora os 100 tweets seguintes aos 100 que já obtivemos anteriormente. Para isso, basta:

>>> url = 'https://api.twitter.com/1.1/search/tweets.json?q=python&count=100&max_id='
>>> response = session.get(url + str(oldest))
>>> tweets = json.loads(response.content)
>>> print len(tweets['statuses'])
100

E assim, sucessivamente. Para obter os próximos 100 tweets, buscaríamos novamente o tweet de menor id do conjunto de resultados e passaríamos o mesmo como max_id da próxima requisição.

O campo next_results

Para facilitar a vida do desenvolvedor, o serviço já retorna um campo next_results dentro do campo search_metadata no resultado de uma requisição. Esse campo já contém a parte da URL relativa aos parâmetros prontinha, com o max_id corretamente configurado para pegarmos os próximos resultados. Veja um exemplo:

>>> response = session.get('https://api.twitter.com/1.1/search/tweets.json?q=brazil&count=100')
>>> tweets = json.loads(response.content)
>>> print tweets['search_metadata']['next_results']
?max_id=508446478730551295&q=python&count=100&include_entities=1

O valor de max_id é o mesmo que obteríamos pegando o menor id da requisição anterior e subtraindo 1. Agora, podemos pegar os próximos 100 resultados usando next_results:

>>> url = 'https://api.twitter.com/1.1/search/tweets.json'
>>> response = session.get(url + tweets['search_metadata']['next_results'])
>>> tweets = json.loads(response.content)
>>> print len(tweets['statuses'])
100

Obtendo os trending topics

Outra possibilidade (dentre muitas) é obter os trending topics de determinada região através do recurso trends. Sua utilização é muito simples, bastando passar à URL https://api.twitter.com/1.1/trends/place.json um identificador indicando o local de interesse. Esse identificador deve ser do tipo WOEID (Where On Earth IDentifier). Nesse esquema de representação, o id 1 representa o mundo inteiro, enquanto que o Brasil é representado como 23424768. Abaixo, obtemos os 10 primeiros trending topics do mundo todo:

>>> response = session.get("https://api.twitter.com/1.1/trends/place.json?id=1")
>>> worlds = json.loads(response.content)[0]["trends"]
>>> for trend in worlds:
        print trend["name"]

E também do Brasil:

>>> response = session.get("https://api.twitter.com/1.1/trends/place.json?id=23424768")
>>> brazils = json.loads(response.content)[0]["trends"]
>>> for trend in brazils:
        print trend["name"]

Agora que temos acesso aos dados, poderíamos começar a brincar com eles. Por exemplo, obter a lista de tópicos que estão no topo tanto no mundo quanto no Brasil:

>>> set([t['name'] for t in worlds]).intersection([t['name'] for t in brazils])

Mas é meio chato ter que ficar fazendo chamadas HTTP a cada vez que queremos obter dados do Twitter em nosso código. Que tal escrevermos uma classe com alguns métodos para nos auxiliar?

Uma classe para acesso ao Twitter

Vamos agora criar uma classe Python que implemente algumas tarefas pré-definidas pra nós, como fazer uma busca por palavras-chave.

import json
from requests_oauthlib import OAuth1Session

MAX_TWEETS = 100
BASE_URL = "https://api.twitter.com/1.1/search/tweets.json"

class MyTwitterSearchClient(object):
    # preencha com os dados do seu app
    API_KEY = "sua API KEY"
    API_SECRET = "sua API SECRET"
    ACCESS_TOKEN = "SEU ACCESS TOKEN"
    ACCESS_TOKEN_SECRET = "SEU ACCESS TOKEN SECRET"
    
    
    def __init__(self):
        self.session = OAuth1Session(self.API_KEY,
                                     self.API_SECRET,
                                     self.ACCESS_TOKEN,
                                     self.ACCESS_TOKEN_SECRET)
    
    
    def get_tweets(self, keyword, n=15, max_id=None):
        if n > 0:
            url = BASE_URL + ("?q=%s&count=%d" % (keyword, n))
            if max_id is not None:
                url = url + "&max_id=%d" % (max_id)
            response = self.session.get(url)
            if response.status_code == 200:
                tweets = json.loads(response.content)
                oldest_id = min( for tweet in tweets['statuses']])-1
                return tweets['statuses'] + \
					self.get_tweets(keyword, n-MAX_TWEETS, oldest_id)
        return []

Agora ficou mais fácil de buscar as informações que desejamos. Para obter os últimos 500 tweets contendo a palavra "python", basta fazer:

>>> client = MyTwitterSearchClient()
>>> tweets = client.get_tweets("python", 500):

Assim como criamos uma classezinha para facilitar a nossa vida, existem alguns wrappers para a API REST do Twitter. Usando eles, não é preciso fazer requisições HTTP explicitamente. Basta invocar métodos em objetos Python para obter os dados desejados. Se quiser conhecer essas ferramentas, siga os links abaixo:

Anúncios

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é!

Acessando APIs REST com Python

Um serviço web (web service) é um mecanismo utilizado para comunicação entre dois ou mais programas através da infraestrutura da internet. Apesar do que o nome pode sugerir, um serviço web não oferece funcionalidades diretamente para o usuário final, mas sim para outros programas que precisam de sua ajuda para realizar alguma tarefa, seja para computar algo ou para fornecer dados úteis.

Você conhece o serviço de busca de endereços postais por CEP? O site dos correios fornece aos cidadãos um serviço que permite que estes façam consultas à base de dados de endereços através de uma página web. Esse não é um exemplo de web service, pois tem como foco servir diretamente ao usuário final. Um web service equivalente poderia ser um serviço oferecido pelos próprios correios para que outros programas, como por exemplo o software de alguma loja virtual, possam verificar qual é o endereço do CEP preenchido pelo usuário. Nesse caso, o software faz uma requisição ao web service, aguarda a resposta, decodifica a mesma e então inclui a resposta obtida na página mostrada ao usuário final.

Um web service pode ser fornecido de várias maneiras. Entretanto, foram definidos alguns padrões para facilitar a interoperabilidade entre programas de diferentes origens, sendo REST e SOAP os mais conhecidos. Este post irá mostrar como utilizar APIs web baseadas no padrão REST.

APIs REST

Uma API (application programming interface) é uma especificação que define como componentes de software devem interagir entre si (thanks, wikipedia!). APIs REST se utilizam do protocolo HTTP para fornecer determinadas funcionalidades aos seus clientes. Essas funcionalidades são descritas por conjuntos de recursos que podem ser acessados remotamente pelos clientes do serviço, através de requisições HTTP comuns.

Em uma API REST existem dois conceitos principais: os recursos (resources) e as coleções (collections). Um recurso é uma unidade que representa um objeto (composto por dados, relacionamentos com outros recursos e métodos). Já uma coleção é um conjunto de recursos que pode ser obtido acessando uma URL. Tal coleção poderia representar a coleção de todos os registros de determinado tipo, ou então, todos os registros que possuem relacionamento com determinado objeto, ou todos os registros que atendem à determinada condição, etc.

A API do twitter, por exemplo, fornece acesso a alguns recursos como os tweets enviados pelos usuários. Com ela, nossa aplicação pode enviar uma requisição HTTP ao twitter solicitando os últimos tweets de um determinado usuário ou até mesmo postar uma mensagem em nome do usuário autenticado.

Por exemplo, para obter uma lista com os últimos 10 tweets postados por mim, basta enviar uma requisição HTTP do tipo GET ao endereço https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=stummjr&count=10. Para que isso funcione, porém, é preciso de autenticação utilizando o mecanismo descrito na documentação da API.

Outro exemplo, com a API do próprio twitter, é o serviço de busca de tweets. Se quisermos buscar os últimos 20 tweets que contenham a hashtag #python, podemos enviar uma requisição à URL: https://api.twitter.com/1.1/search/tweets.json?q=#python&count=20. Como resposta às requisições, receberemos coleções de dados estruturados em formato JSON. A resposta dada por uma chamada a uma API REST normalmente é composta por dados em algum formato estruturado, como JSON ou XML. Esses formatos permitem a interoperabilidade entre plataformas e linguagens diferentes, pois o conteúdo é nada mais do que texto puro codificado com algum esquema de caracteres.

Muitos sites famosos (como twitter, facebook, reddit, etc) fornecem APIs REST para que terceiros possam escrever aplicativos que utilizem os dados armazenados nesses sites. Com essas APIs, fica fácil criar programas que interajam com redes sociais, lendo e postando dados para as mesmas.

Como você pôde ver, uma API REST nada mais é do que uma API que fornece acesso remoto a recursos via HTTP. Para podermos entender melhor e fazer requisições HTTP a um serviço REST, precisamos conhecer um pouquinho mais sobre o protocolo HTTP e como seus métodos são utilizados em uma API REST.

HTTP e seus métodos

O protocolo HTTP define que uma requisição de um cliente para um servidor é composta por:

  1. Uma linha descrevendo a requisição, composta por:
    1. Método: indica o que desejamos fazer com o recurso. Pode ser: GET, POST, PUT, DELETE, além de outros menos utilizados.
    2. URL: o endereço do recurso que se deseja acessar.
    3. Versão: a versão do protocolo a ser usada (1.1, atualmente).
  2. O corpo da requisição, que pode conter informações como o nome do host de onde desejamos obter o recurso solicitado, dados a serem enviados do cliente para o servidor, etc.

O exemplo abaixo mostra uma requisição HTTP do tipo GET para um recurso na web:

GET /r/programming/.json HTTP/1.1
Host: http://www.reddit.com

O método de uma requisição HTTP indica a ação que pretendemos realizar com aquela requisição, e cada método tem um significado próprio:

  • GET: utilizado para a obtenção de dados. É o tipo de requisição que o navegador faz a um servidor quando você digita uma URL ou clica em um link.
  • POST: utilizada na web para o envio de dados do navegador para o servidor, principalmente quando esses dados vão gerar alguma alteração no último. É o tipo de requisição que o navegador faz a um servidor quando você preenche um formulário na web e clica em “Enviar” (embora existam formulários na web que utilizem outros tipos de requisição, como GET).
  • PUT: serve para solicitar a criação de objetos no servidor caso esses ainda não existirem. Na prática, a maioria das páginas utiliza o método POST para isso também.
  • DELETE: serve para indicar que o usuário (emissor da requisição em questão) deseja apagar determinado recurso do servidor.

Após executar a requisição do cliente, o serviço responde com uma mensagem de resposta HTTP. O protocolo HTTP define que as mensagens de resposta devem possuir um campo indicando o status da requisição. O status mais conhecido na web é o 404 (not found – recurso não encontrado), mas existem vários, como: 200 (OK), 401 (not authorized – indicando falta de autenticação), 500 (internal server error – erro no servidor), dentre outros. Por ser baseado em HTTP, o padrão REST define que as mensagens de resposta devem conter um código de status, para que o cliente do serviço web possa verificar o que aconteceu com a sua requisição.

A seguir veremos como emitir requisições HTTP “programaticamente” em Python, acessando uma API REST disponível na web.

Acessando uma API REST

Para entender melhor, vamos utilizar como exemplo a API REST JSONPlaceHolder, disponível em jsonplaceholder.typicode.com, que é uma API fake criada para ser usada por quem estiver usando REST em seu programa e precisando de dados falsos (dummy data) para testes.

O JSONPlaceHolder disponibiliza acesso a alguns recursos, como: posts, comments, albums, photos, todos e users. Cada um dos recursos está disponível em uma URL específica:

Em nosso exemplo, vamos usar somente o recurso comments, mas o exemplo será válido para qualquer um dos recursos acima.

Como já foi mencionado anteriormente, as APIs REST fornecem suas funcionalidades através dos métodos existentes no protocolo HTTP (GET, POST, PUT e DELETE). Por exemplo, para listar todos os comments existentes, basta enviar uma requisição HTTP do tipo GET à URL http://jsonplaceholder.typicode.com/comments. Para listar algum registro comment em específico, basta enviar um GET à mesma URL, passando como parâmetro o id do comment que queremos obter: http://jsonplaceholder.typicode.com/comments/1. Uma requisição HTTP usando o método DELETE à URL http://jsonplaceholder.typicode.com/comments/1 irá remover o objeto comment em questão. Também é possível alterar um objeto através do método HTTP PUT ou incluir um novo objeto com o método POST.

Podemos resumir a semântica dos métodos HTTP em uma API REST da seguinte forma:

  • GET: obtenção de dados (seja um conjunto de objetos ou um em específico).
  • POST: criação de dados.
  • PUT: alteração de dados existentes.
  • DELETE: remoção de dados.

Obviamente, as APIs REST utilizam mecanismos de autenticação para evitar que alguém altere ou acesse dados de forma indevida.

Mãos na massa

Atenção: esta seção supõe que você tem uma certa familiaridade com JSON. Caso não conheça o formato, veja aqui um post anterior sobre o assunto.

Agora que já temos uma ideia sobre como uma API REST funciona, vamos ver na prática como nosso programa poderia utilizar uma API desse tipo para obtenção e manipulação de dados externos. Para fazer as requisições HTTP ao serviço, vamos utilizar a biblioteca requests (instalável via pip install requests) e para manipular o JSON retornado pelo serviço, vamos usar a biblioteca json (inclusa na biblioteca padrão).

Primeiramente, vamos importar as bibliotecas necessárias:

>>> import json, requests

Obtendo dados

Vamos começar testando a leitura de registros usando o método HTTP GET, que está disponível na requests através do método get().

>>> response = requests.get("http://jsonplaceholder.typicode.com/comments")
>>> print response.status_code
200
>>> print response.content
   [
    {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    "email": "Eliseo@gardner.biz",
    "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem     quasi\nreiciendis et nam sapiente accusantium"
    },
    {
    "postId": 1,
    "id": 2,
    "name": "quo vero reiciendis velit similique earum",
    "email": "Jayne_Kuhic@sydney.com",
    "body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et"
    }
    ...
    ,
    {
    "postId": 100,
    "id": 500,
    "name": "ex eaque eum natus",
    "email": "Emma@joanny.ca",
    "body": "perspiciatis quis doloremque\nveniam nisi eos velit sed\nid totam inventore voluptatem laborum et eveniet\naut aut aut maxime quia temporibus ut omnis"
    }
   ]

Agora que vimos que nossa requisição HTTP foi executada com sucesso (código 200) e que a string retornada como resposta está em formato JSON, vamos empacotar o resultado em um objeto Python para que possamos manipular os dados com maior facilidade:

>>> comments = json.loads(response.content)

A função json.loads() transformou a string JSON em um objeto Python de tipo correspondente, em nosso caso, um objeto list contendo vários dict dentro, onde cada dict representará um dos registros existentes no servidor.

>>> print comments[0]
    {u'body': u'laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium', u'email': u'Eliseo@gardner.biz', u'postId': 1, u'id': 1, u'name': u'id labore ex et quam laborum'}

>>> print comments[0]['body']
     laudantium enim quasi est quidem magnam voluptate ipsam eos
     tempora quo necessitatibus
     dolor quam autem quasi
     reiciendis et nam sapiente accusantium

Listando os nomes dos 10 primeiros comments:

>>> for comment in comments[0:10]:
        print comment['name']
     labore ex et quam laborum
     quo vero reiciendis velit similique earum
     odio adipisci rerum aut animi
     alias odio sit
     vero eaque aliquid doloribus et culpa
     et fugit eligendi deleniti quidem qui sint nihil autem
     repellat consequatur praesentium vel minus molestias voluptatum
     et omnis dolorem
     provident voluptas
     eaque et deleniti atque tenetur ut quo ut

Além da listagem de todos os objetos, também podemos obter um objeto em específico:

>>> response = requests.get("http://jsonplaceholder.typicode.com/comments/1")
>>> response.content
     '{\n  "postId": 1,\n  "id": 1,\n  "name": "id labore ex et quam laborum",\n  "email": "Eliseo@gardner.biz",\n  "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium"\n}'
>>> comment = json.loads(response.content)
>>> print comment['name']
     labore ex et quam laborum

Se quisermos descobrir a qual post que o comment acima se refere, basta fazer uma requisição GET à http://jsonplaceholder.typicode.com/posts/X, sendo X o valor do campo postId do comment retornado. Veja:

>>> post = requests.get("http://jsonplaceholder.typicode.com/posts/%d" % comment['postId'])
>>> post.content
     '{\n  "userId": 1,\n  "id": 1,\n  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",\n  "body": "quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"\n}'
>>> post = json.loads(post.content)
>>> post
     {u'body': u'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',
     u'id': 1,
     u'title': u'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
     u'userId': 1}
>>> post['title']
     u'sunt aut facere repellat provident occaecati excepturi optio reprehenderit'

Dessa forma, podemos navegar entre registros, obter objetos relacionados, etc.

Inserindo dados

Para a inserção de dados em um serviço que oferece uma API REST, precisamos utilizar o método POST do HTTP, disponível através da função post da requests. Como queremos inserir um novo registro no servidor, é necessário que passemos esse registro junto à requisição HTTP. Isso pode ser feito passando um dicionário com os dados ao parâmetro data:

>>> dados = data={"postId": 1, "name": "John Doe", "email": "john@doe.com", "body": "This is it!"}
>>> response = requests.post("http://jsonplaceholder.typicode.com/comments/", data=dados)

Feito isso, agora podemos verificar qual a resposta do serviço para a nossa requisição. O valor esperado depende de quem projetou a API, pois ele pode enviar uma resposta contendo o novo registro, a URL de acesso ao registro recém-criado, ou outra informação. Além disso, o código de resposta HTTP também pode variar (alguns serviços respondem com 200 — OK, outros com 201 – Created, embora o último faça muito mais sentido). O serviço que estamos usando para os exemplos envia uma resposta com o código 200 e o registro recém inserido como conteúdo.

Como ocorreu tudo dentro do esperado, o serviço respondeu com o registro criado (repare que foi adicionado um campo id que não havia nos dados que enviamos):

>>> print response.status_code
     200
>>> print response.content
     {
     "body": "This is it!",
     "postId": "1",
     "name": "John Doe",
     "email": "john@doe.com",
     "id": 501
     }

Para ter certeza sobre o funcionamento da API que você estiver usando, é preciso ler a especificação dela para descobrir o que esperar como resultado em caso de sucesso ou de erro. Como o protocolo HTTP já possui um conjunto pré-definido de códigos de status, os serviços baseados em REST devem usar tais códigos para indicar o status da requisição.

Alterando registros

O padrão REST define que o método HTTP PUT deve ser utilizado sobre um determinado recurso quando desejarmos alterá-lo. A biblioteca requests fornece a função put para o envio de requisições HTTP que utilizam o método PUT. Vamos a um exemplo, onde vamos alterar o campo email do comentário de id 10:

>>> dados = {"email": "john@doe.com"}
>>> response = requests.put("http://jsonplaceholder.typicode.com/comments/10", data=dados)

Ou seja, enviamos uma requisição PUT à URL que representa o comentário que queremos alterar, e passamos também um dicionário contendo o novo valor para o campo que desejamos alterar. Como resposta, obtivemos o recurso alterado.

Removendo registros

Para apagar um registro, o padrão REST define que uma requisção HTTP usando o método DELETE deve ser enviada ao serviço, passando como recurso o registro que deve ser removido. Para apagar o comment de id 10, utilizamos a função delete da requests:

>>> response = requests.delete("http://jsonplaceholder.typicode.com/comments/10")

Acessando recursos aninhados

Como já vimos pela estrutura dos dados retornados para os comments, cada registro desse tipo está associado a um registro post. Assim, uma necessidade que surge naturalmente é a de obter todos os comments pertencentes a um determinado post. O web service que estamos usando permite consultas a recursos relacionados. Para obter todos os comments relacionados ao post de id 2, fazemos:

>>> response = requests.get("http://jsonplaceholder.typicode.com/posts/2/comments")

Enfim

Apesar de o exemplo que seguimos ter focado em um web service específico, cada serviço possui uma interface de acesso própria. Ou seja, algumas APIs podem não permitir acesso a recursos aninhados, ou não permitir a remoção de registros, etc. É importante que você, antes de utilizar uma API REST, leia a documentação da mesma para saber o que é possível fazer com ela.

Apesar dessas diferenças entre uma API e outra, o mecanismo de acesso às mesmas não muda. Você vai precisar de uma biblioteca para emitir requisições HTTP (requests ou urllib2) e uma biblioteca para fazer a decodificação dos dados retornados (json, simplejson ou alguma biblioteca para manipulação de XML).

Obrigado ao Elias Dorneles pela revisão!

Um blog com Google App Engine

Sumário

Este é um post-tutorial, onde vou descrever o desenvolvimento de um sistema de blogging bem simples usando os recursos nativos do Google App Engine (GAE, para os amigos). Caso não tenha familiaridade com o GAE, sugiro a leitura dos textos anteriores que publiquei aqui no Python Help sobre o assunto:

Com as leituras acima, você terá a base necessária para entender o tutorial aqui apresentado.

O que vamos desenvolver?

O objetivo é implementar um sistema de blogging bem simples, com as seguintes funcionalidades:

  • Tela principal com listagem de todos os posts
  • Tela para adicionar um novo post
  • Tela para visualizar um post
  • Restrição de acesso para a tela de criar post

A tela para adicionar posts deve redirecionar para o post recém criado.

Mãos à obra

A primeira coisa que temos que fazer é criar o projeto, ou seja, definir a estrutura de arquivos que nosso projeto vai ter e configurá-lo no arquivo app.yaml, como já vimos nos posts anteriores. Veja abaixo a estrutura de diretórios que vamos utilizar em nosso projeto:

├── app.yaml
├── main.py
├── models.py
├── static
│   └── main.css
└── templates
    ├── base.html
    ├── detail.html
    ├── form.html
    └── front.html

Crie o arquivo de configurações app.yaml com as seguintes opções:

application: pythonhelp-blog
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /static
  static_dir: static

- url: /.*
  script: main.app

Como você deve lembrar, a diretiva handlers no arquivo de configuração app.yaml configura como nossa aplicação vai tratar as requisições HTTP vindas do cliente. Aqui configuramos nossa aplicação para servir arquivos estáticos a partir do diretório static e para publicar a aplicação criada na variável app dentro do módulo main.

Ah, não se esqueça de alterar o seu application id (valor de application no app.yaml), pois esse identificador deve ser único no GAE. Ou seja, se eu colocar esse sisteminha no ar com o app id "pythonhelp-blog", você não poderá colocá-lo no ar com o mesmo identificador.

O modelo de dados

Agora, vamos modelar o nosso problema. Como iremos representar as informações no nosso banco de dados? De acordo com os requisitos, iremos precisar de um model para representar um post, que tenha vinculado a si alguns atributos, incluindo o autor do texto (que deve ser um usuário cadastrado).

Graças ao GAE, não precisaremos criar uma entidade User, pois ele já fornece um serviço de gerenciamento e autenticação de usuários (usando contas de usuário na google mesmo). Dessa forma, teremos apenas um model de uma entidade que iremos chamar de BlogPost, contendo um atributo do tipo UserProperty, fornecido pelo GAE. Crie um model BlogPost de acordo com o código a seguir e salve no arquivo models.py:

from google.appengine.ext import db

class BlogPost(db.Model):
    subject = db.StringProperty(required=True)
    author = db.UserProperty(required=True,
                             auto_current_user=True)
    content = db.TextProperty(required=True)
    created = db.DateTimeProperty(auto_now_add=True)
    last_updated = db.DateTimeProperty(auto_now=True)

Observe  o campo author. Passamos um atributo nomeado auto_current_user com o valor True. Isso faz com que o campo author seja setado com o usuário atualmente logado quando um objeto do tipo BlogPost for gravado no datastore. Barbadinha, né?

Implementação das ações

Agora vamos definir as URLs que serão utilizadas e para quais classes elas serão mapeadas em nosso projeto.

Defina o mapeamento das URLs para as classes manipuladoras no arquivo main.py:

    app = webapp2.WSGIApplication(
        [("/blog", BlogMainHandler),
         ("/blog/newpost", BlogNewPostHandler),
         ('/blog/post/(\d+)', BlogPostViewHandler),
         ],
        debug=True)

Dessa forma, nosso esquema de URLs será assim:

  • a página principal (que lista todos os posts), será acessível pela URL "/blog";
  • o formulário para criação de um novo post será acessível via "/blog/newpost";
  • um post em específico será acessível pela URL "/blog/post/id_do_post". Por exemplo, "/blog/post/19" irá abrir uma página de visualização do post cujo id seja 19.

Agora vamos à implementação das classes listadas acima. A primeira delas será a mais simples: BlogMainHandler, que será a página inicial do blog, listando todos os posts.

template_dir = os.path.join(os.path.dirname(__file__),
                            'templates')


class BlogMainHandler(webapp2.RequestHandler):
    def get(self):
        self.response.out.write(
            template.render(template_dir +
                            '/front.html',
                            {'posts': BlogPost.all()}))

A segunda classe que iremos implementar é BlogPostViewHandler, responsável pela visualização de um post em específico.

class BlogPostViewHandler(webapp2.RequestHandler):
  def get(self, post_id):
    try:
      post = BlogPost.get_by_id(int(post_id))
      self.response.out.write(
          template.render(template_dir + '/detail.html',
                          {'post': post}))
    except:
      self.response.out.write("Erro: post não encontrado!")

Veja que o método get recebe um parâmetro post_id. Esse parâmetro é passado pelo URL dispatcher, pegando o pedaço da URL que está entre parênteses na definição ('/blog/post/(\d+)') e repassando como valor ao método (\d+ significa um ou mais dígitos e os parênteses ao redor fazem com que esse valor seja passado ao método get). Por exemplo, um acesso a /blog/post/19 irá fornecer ao parâmetro post_id o valor 19.

Você deve ser capaz de entender os códigos listados acima. Caso tenha dificuldades, reveja os posts em que explico como criar um projeto na GAE, como utilizar o Datastore e o mecanismo de templates.

Depois temos a classe responsável pela criação de novos posts: BlogNewPostHandler. Veja o código abaixo:


class BlogNewPostHandler(webapp2.RequestHandler):

  def get(self):
    user = users.get_current_user()
    if user is not None:
      self.response.out.write(
          template.render(template_dir + '/form.html',
                          {'title': 'Escrever post'}))
    else:
      self.redirect(
          users.create_login_url(self.request.uri))

  def post(self):
    author = users.get_current_user()
    if author is not None:
      subject = self.request.get('subject')
      content = self.request.get('content')
      if subject and content:
        b = BlogPost(subject=subject, content=content)
        b.put()
        self.redirect("/blog/post/" + str(b.key().id()))
      else:
        error = 'Ambos os campos são obrigatórios'
        self.response.out.write(
            template.render(template_dir + '/form.html',
                            {'title': 'Escrever post', 
                             'error': error,
                             'subject': subject,
                             'content': content}))
    else:
      self.redirect(
          users.create_login_url(self.request.uri))

Essa classe tem alguns detalhes que valem a pena ser observados. Em primeiro lugar, ela possui dois métodos: get e post. Como já vimos anteriormente, o método get será chamado pelo ambiente subjacente quando houver uma requisição do tipo GET à URL mapeada para a classe BlogNewPostHandler e o método post será chamado quando houver uma requisição do tipo POST (normalmente usada para fazer escrita de dados no servidor). Dessa forma, a classe possui duas funcionalidades:

  1. get: ao acessar a url /blog/newpost, o usuário irá receber como resposta um formulário para criação de um novo post.
  2. post: quando o formulário for submetido, é feito uma requisição POST, passando os dados do formulário para criar um objeto BlogPost no servidor.

Repare como é simples a autenticação do usuário no código usando o serviço provido pelo GAE. Veja que a primeira linha de cada método chama users.get_current_user para obter o usuário logado atualmente. Caso não haja usuário logado (se o objeto retornado for None), então redirecionamos o usuário para uma URL própria para fazer o login. Quando estiver rodando no ambiente externo, é apresentada a tela de autenticação dos serviços da Google mesmo. No ambiente de testes, é apresentada uma tela de login como a mostrada na figura abaixo.

Tela de login no ambiente local (powered by GAE)

Tela de login no ambiente local (powered by GAE)

Os templates

Em um projeto GAE, os templates contém o código HTML que será enviado para o usuário nas requisições. Como visto no post anterior sobre o assunto, é possível a introdução de campos dinâmicos para serem preenchidos de acordo com os parâmetros passados na renderização dos templates.

Como todas as páginas terão um cabeçalho padrão, definimos um template base que os demais irão estender. Veja abaixo o código de cada um dos templates:

templates/base.html:

<!DOCTYPE html>
<html>
  <head>
    <link type="text/css" rel="stylesheet" href="/static/main.css" />
    <title>{{ title }}</title>
  </head>
  <body>
    <div class="main-title"><a href="/blog">stummjr Blog</a></div>
    {% block content %}
    {% endblock %}
  </body>
</html>

Dessa forma, basta aos subtemplates detail.html, form.html e front.html a descrição do bloco content.

templates/detail.html (usado para visualização de um post específico):

{% extends 'base.html' %}

{% block content %}
<div class="post">
  <div class="post-heading">
    <div class="post-title">
      {{ post.subject }}
    </div>
    <div class="post-date">
      {{ post.created}}
    </div>
  </div>
  <pre class="post-content">
    {{ post.content }}
  </pre>
</div>
{% endblock %}

templates/form.html (usado para apresentar a interface de criação de um novo post):

{% extends 'base.html' %}
{% block content %}
<form method="post">
  <label for="subject">Assunto:
    <input type="text" name="subject" value="{{ subject }}">
  </label>
  <label for="content">Conteúdo:
    <textarea name="content" rows="10" cols="50">
      {{ content }}
    </textarea>
  </label>
  <button type="submit">Salvar</button>
  <div class="error">
    {{ error }}
  </div>
</form>
{% endblock %}

templates/front.html (lista todos os posts existentes no banco de dados do blog):

{% extends 'base.html' %}

{% block content %}
{% for post in posts %}
  <div class="post">
    <div class="post-heading">
      <div class="post-title">
        <a href="/blog/post/{{ post.key.id }}">{{ post.subject }}</a>
      </div>
      <div class="post-date">
        {{ post.created }}
      </div>
    </div>
    <pre class="post-content">
      {{ post.content }}
    </pre>
  </div>
{% endfor %}
{% endblock %}

Como podemos ver, o código base.html referencia o arquivo de estilos main.css, que não será listado aqui devido ao seu tamanho. Todos os arquivos do projeto podem ser baixados no link que consta no final do post.

Testando o blog

Tendo as classes e os templates definidos, agora basta testar nosso projeto. Para isso, vamos usar o ambiente de testes fornecido pelo GAE. Veja como instalar e usar esse ambiente no primeiro post sobre o GAE feito no blog.

Próximos passos

Não pare por aqui. Leia a documentação oficial do GAE (em pt-br, inclusive) e incremente o projeto, adicionando comentários, categorias, etc. Bom trabalho!

Download dos arquivos do projeto

Obrigado ao Elias Dorneles, pela baita revisão que fez no texto!

Introdução ao memcache

O problema

Imagine um portal como o globo.com, com suas inúmeras chamadas para matérias que constam já na página principal.

Apelação milenar

Apelação milenar

Cada imagem, título e descrição de reportagem ou comercial que aparecem na página são informações dinâmicas dela. A cada vez que acessamos a página, podemos obter notícias mais recentes, resultando em uma página diferente. Assim, imagina-se que os dados apresentados na página estejam armazenados em um banco de dados e que, a cada acesso de usuário, temos várias consultas sendo realizadas pelo servidor ao banco de dados. Isso seria o mais natural, considerando-se a alta taxa de criação de novas notícias no site. Porém, você já deve saber que o disco é o grande gargalo dos computadores modernos e que acessos a disco são o grande vilão da performance da maioria dos programas de computador. Então, como fazer um sistema web com foco no conteúdo dinâmico sem realizar frequentes acessos ao BD (e consequentemente, ao disco)?

Uma possível solução

Uma solução pode ser fazer o caching de alguns dados na memória. Em um portal de notícias, os usuários que acessarem a página mais ou menos no mesmo horário vão receber o mesmo conteúdo, que são as últimas notícias postadas. Então por que repetir as consultas ao banco de dados para todo e qualquer acesso de usuário ao portal? Seria mais inteligente se o nosso sistema consultasse o banco de dados somente uma vez, armazenasse os resultados da consulta em memória (na nossa cache) e então passasse a responder às requisições subsequentes usando o conteúdo armazenado em memória sem precisar buscar nada no disco até que o conteúdo mude novamente. Assim, as requisições podem ser respondidas com o conteúdo da memória. Quando o conteúdo do BD sofrer alguma alteração, a cache pode ser invalidada e atualizada com o novo conteúdo.

Um exemplo

Vamos seguir com o foco em um portal de notícias, já que existem muitos casos de uso similares. Como já vimos em outro post, o Google App Engine suporta Python; e o mecanismo de datastore que ele oferece é legal pra caramba. Suponhamos que nesse portal existe um modelo de dados que contenha uma entidade Noticia, conforme representado abaixo:

class Noticia(db.Model):
    titulo = db.StringProperty(required=True)
    conteudo = db.TextProperty(required=True)
    url = db.LinkProperty(required=True)
    autor = db.UserProperty(required=True)
    thumbnail = db.LinkProperty()
    data_publicacao = db.DateTimeProperty()

Como todo bom portal de notícias, esse também vai ter aquela página inicial carregadíssima, cheia de notícias e outros elementos. Haja barramento pra aguentar tantos acessos ao disco!

O código a seguir poderia ser a view que gera a sua página principal:

    class PostHandler(webapp2.RequestHandler):

        def get(self):
            ultimas = get_noticias(100, '-data_publicacao')
            categorias = get_categorias()
            comerciais = get_comerciais_ativos()
            for n in ultimas:
                # lista e formata as noticias, categorias, comerciais
                ...

    def get_noticias(limit, order_by):
        return Noticia.all().order(order_by).fetch(limit)

    def get_categorias():
        return Categoria.all()

    def get_comerciais_ativos():
        return Comercial.all().filter('ativa = ', True)

Beleza, funciona. O problema começa quando o site começa a ficar popular e os usuários começam a reclamar de lentidão e de indisponibilidade constante. O que fazer? Bom, a resposta certa é fazer profiling da aplicação, isto é, medir o tempo que estão levando as operações necessárias para carregar a página — tanto no lado cliente quanto no lado servidor — e então você poderá decidir melhor como resolver o problema. Mas quando um site está realizando várias consultas ao banco para cada acesso do usuário, frequentemente uma solução é evitar a realização dessas consultas usando uma cache.

Let’s cache them all!

mecanismo de cache que vou apresentar aqui é o memcache específico para o Google AppEngine, embora existam várias implementações do memcache para as mais variadas plataformas e linguagens. O memcache é um mecanismo que permite o armazenamento de pares chave-valor (assim como um dicionário, ou uma tabela hash) em memória, de forma que o acesso aos valores ocorre de forma bem eficiente.

Agora, vamos modificar o método get_noticias() para fazer caching do conteúdo da página principal. O princípio de funcionamento é bem simples: antes de qualquer consulta ao banco, verifique se os dados que queremos estão presentes na cache. Veja o código:

from google.appengine.api import memcache

def get_noticias(limit, order_by):
    # busca na cache por um valor de chave 'noticias'
    ultimas = memcache.get('noticias')
    if ultimas is None: # valor ainda não está em cache
        # busca no BD
        ultimas = Noticia.all().order(order_by).fetch(limit)
        # e inclui o resultado na cache para os futuros acessos
        if not memcache.add('noticias', ultimas):
            logging.error('Erro ao setar memcache.')

O que foi feito acima para os dados das notícias pode ser feito também para os comerciais e para as categorias. Assim, o primeiro de todos os acessos ao site pode demorar um pouquinho mais, mas os acessos seguintes de todos os usuários vão ser muito rápidos.

Cuidado com a Sincronização

Uma vez que começamos a usar a cache, nós passamos a ter informações redundantes (BD e cache). Dessa forma, basta que alguém insira uma notícia nova no BD para que tenhamos os dados fora de sincronia. Uma alternativa para solucionar esse problema pode ser: logo após salvar uma nova notícia no BD, atualizar os valores da cache. Se isso for algo demorado, poderíamos iniciar uma tarefa em background para fazer isso.

Quando usar cache?

Como já comentei anteriormente, a cache não é a solução para todo e qualquer problema de um sistema web. O problema pode estar no plano de hospedagem, no excesso de arquivos estáticos, em lógica duplicada resultante de um mau hábito de copiar-e-colar, em algoritmos pouco otimizados, etc.

A cache é um mecanismo bem simples de ser implementado, em alguns casos, mas isso não quer dizer que você já deve sair de cara utilizando cache em todo e qualquer projeto a partir de agora. Afinal, premature optimization is the root of all evil. 😛

Para finalizar…

No curso Web Development, ministrado pelo grande Steve Huffman no Udacity.com, aprendi o seguinte:

Um simples acesso de usuário a uma página jamais deveria gerar um acesso ao banco de dados.

Ou seja, se um usuário qualquer quiser apenas visualizar sua página, blog, ou portal, esse acesso não deveria exigir do sistema um acesso ao banco de dados. É um princípio bem simples e que pode evitar que o seu site caia quando se tornar mega-popular. 🙂

Grandes sites como Twitter, Facebook e Reddit usam largamente os mecanismos de cache, de forma que seja possível responder aos tsunamis de requisições que eles recebem a cada segundo.

Leia mais sobre o memcache na Wikipedia.

 

Obrigado ao Elias pela revisão!

Serialização de Objetos em Python

Sumário

Vez por outra precisamos enviar dados via rede, seja através de uma já tradicional conexão HTTP, ou até mesmo através de um socket UDP cruzão, e é bem comum que esses dados estejam representados em nosso programa através de uma instância de uma classe por nós mesmos definida. No entanto, na hora de enviar esse objeto pela rede, é preciso que tenhamos esses dados representados de forma contínua (diferentemente de simples referências a posições de memória) e, muitas vezes, de uma forma que possa ser lida por um sistema diferente do sistema no qual o objeto foi criado. Para atender a esses requisitos, é necessária a serialização de dados, que trata da representação de objetos ou estruturas de dados em um formato que permita que estas sejam armazenado em um disco (ou enviadas pela rede) para posterior recriação do objeto em memória.

Veja mais sobre serialização no artigo da Wikipedia sobre o assunto.

Como serializar?

Em Python, existem diversos mecanismos disponíveis para serialização de dados. A escolha vai depender do tipo de aplicação exigida. Veremos a seguir alguns mecanismos para serialização de objetos:

Pickle

pickle é um módulo que provê a serialização de objetos Python, transformando objetos quaisquer em sequências de bytes. No exemplo a seguir, vamos serializar uma lista:

>>> import pickle
>>> lista = [1, 'hello!', [1, 2, 3]]
>>> s = pickle.dumps(lista)
>>> print s
"(lp0\nI1\naS'hello!'\np1\na(lp2\nI1\naI2\naI3\naa."
>>> type(s)
<type 'str'>

O método dumps() é o responsável por pegar os dados do objeto em questão e gerar uma sequência de bytes capaz de representar tais dados, de forma que estes possam ser transmitidos pela rede, armazenados em um arquivo, e depois, recuperados para o seu formato original (em nosso caso, um objeto list).

>>> print s
"(lp0\nI1\naS'hello!'\np1\na(lp2\nI1\naI2\naI3\naa."
>>> lista_recuperada = pickle.loads(s)
>>> print lista_recuperada
[1, 'hello!', [1, 2, 3]]
>>> type(lista_recuperada)
<type 'list'>

Já o método loads() é responsável por pegar uma sequência de bytes (representada por uma string) e convertê-la de volta para o objeto Python que originalmente representava (veja o exemplo acima).

O dumps() e o loads() serializam e de-serializam os objetos para strings e a partir de strings, respectivamente. Existem também as versões dos mesmos métodos que lidam com dados serializados que estão armazenados em arquivos. São eles os métodos dump() e load() (sem o s no final do nome).

Para serializar um objeto usando a função dump(), é preciso passar a ela o arquivo no qual queremos que o objeto serializado seja gravado:

>>> pickle.dump(lista, open('data.pkl', 'wb'))

(Repare que passamos uma referência ao arquivo já aberto, não somente o nome do arquivo)

Podemos agora verificar o conteúdo do arquivo data.pkl pelo shell do sistema operacional:

user@host$ cat data.pkl
(lp0
I1
aS'hello!'
p1
a(lp2
I1
aI2
aI3
aa.

Para reconstruir as informações contidas no arquivo em um objeto Python, vamos usar o método load():

>>> recuperada = pickle.load(open('data.pkl'))
>>> print recuperada
[1, 'hello!', [1, 2, 3]]

Barbada, não? Ainda existe também uma implementação do mesmo protocolo no módulo cPickle, que, por ser implementado em C, possui um desempenho muito superior ao do Pickle (de acordo com a documentação, pode ser até 1000 vezes mais rápido). Porém, por não se tratar de código Python, existem algumas restrições nele, como não podermos subclasseá-lo (estendê-lo).

Apesar de ser fácil de utilizar, o pickle serializa os dados em um formato próprio (não-popular com outras linguagens). Sendo assim, o pickle será uma boa opção para serializar objetos para envio/gravação somente para outros programas também escritos em Python.

Serializando objetos customizados

É comum criarmos classes novas em nossos projetos e muitas vezes é necessário serializar instâncias dessas classes. O Pickle pode ser usado para isso também. Veja o exemplo abaixo, onde criamos uma classe Objeto, e em seguida serializamos uma instância dela:

>>> class Objeto(object):
....:
....:    def __init__(self):
....:        self.x = 42
....:        self.s = 'hello, world'
....:        self.l = [1, 2, 3]
>>> o = Objeto()
>>> s = pickle.dumps(o)
>>> print s
ccopy_reg
_reconstructor
p0
(c__main__
Objeto
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
S'x'
p6
I42
sS's'
p7
S'hello, world'
p8
sS'l'
p9
(lp10
I1
aI2
aI3
asb.
>>> obj = pickle.loads(s)
>>> print obj
<__main__.Objeto object at 0x1c12790>

Marshal

O módulo marshal tem uma interface bem semelhante ao pickle, com seus métodos load, loads, dump e dumps. Porém, não é recomendado o seu uso para serialização de objetos em aplicações, por não ser garantida a compatibilidade entre versões do interpretador Python (de acordo com a documentação, esse módulo existe para uso interno no interpretador).

Exemplos de uso:

>>> import marshal
>>> print marshal.dumps(lista)
[ishello![iii
>>> print marshal.loads(s)
[1, 'hello!', [1, 2, 3]]

As informações são serializadas para um formato binário. Não vamos nos alongar muito nesse módulo, visto que ele não deve ser usado em aplicações do dia-a-dia.

Struct

struct é um módulo que faz o meio de campo entre objetos Python e estruturas em C. Agora, ao invés dos métodos dump e load, temos pack e unpack.

>>> import struct
>>> p = struct.pack('i5s', 42, 'hello')
>>> p
'*\x00\x00\x00hello'

O seu uso é mais complicado do que o pickle ou o marshall. Vamos rever a chamada à função pack na linha 2 do trecho de código acima:

pack('i5s', 42, 'hello')

O primeiro argumento passado para a função pack deve ser o formato que irá definir como será a estrutura C que irá armazenar os dados dos nossos objetos Python. No exemplo acima, informamos através da string 'i5s' que a estrutura possui 2 campos:

  • um int (representato por i);
  • uma string (char * em C) com 5 posições ('5s');

Para recriar os objetos Python através do dado serializado em forma de struct, vamos usar a função unpack:

>>> num, s = struct.unpack('i5s', p)
>>> print num
42
>>> print s
'hello'
>>> struct.unpack('i5s', p)
(42, 'hello')

Perceba que a função unpack retorna uma tupla contendo os dados que estavam empacotados dentro da estrutura.

Esse formato também não é o melhor para transporte de dados, pois é dependente da arquitetura do computador. Assim sendo, um pacote empacotado em uma máquina poderia ter problemas para ser desempacotado em uma máquina de arquitetura diferente. Além disso, ele só é capaz de empacotar dados dos tipos mais simples, como os tipos numéricos, strings e booleanos.

JSON

O JSON talvez seja hoje o formato para dados intercambiáveis mais utilizado. Esse formato de dados é muito usado em serviços web, e também para o transporte de dados usando outros protocolos. Como ele já foi visto em outros posts (aqui e aqui), não vamos nos aprofundar muito na sua utilização. Vamos ver somente um exemplo simples:

>>> import json
>>> lista = [1, 'hello!', [1, 2, 3]]
>>> s = json.dumps(lista)
>>> s
'[1, "hello!", [1, 2, 3]]'
>>> print type(s)
<type 'str'>
>>> l = json.loads(s)
>>> l
'[1, u'hello!', [1, 2, 3]]'
>>> print type(l)
<type 'list'>

Como já foi visto nos posts anteriormente referidos, JSON pode ser usado para trafegar estuturas de dados mais complexas, em um formato parecido com o de dicionários Python. Assim, esse post aqui mostra praticamente nada da capacidade desse formato (se quiser saber mais, veja os posts anteriores).

Caso você não conheça o JSON, sugiro fortemente que procure documentação sobre ele, pois é um formato muito bom para tráfego de dados entre ambientes heterogêneos.

Shelve

O shelve é um módulo que provê um tipo de dados com uma interface similar a de um dicionário (chamado de shelf), e que agrega a funcionalidade de persistir esse dicionário em um arquivo para uso posterior. Ou seja, o shelve nos provê dicionários persistentes.

Vamos ver um exemplo:

>>> import shelve
>>> user = shelve.open('data.txt')
>>> user['nickname'] = 'stummjr'
>>> user['city'] = 'Blumenau'
>>> user['twitter'] = 'stummjr'
>>> print user
{'city': 'Blumenau', 'twitter': 'stummjr', 'nickname': 'stummjr'}
>>> user.close()

Perceba que a chamada a shelve.open() abre um shelf (se ainda não existir, ele é criado). Depois, podemos manipular o objeto retornado por esta chamada como se fosse um dicionário. Para persistir os dados no arquivo data.txt, é necessário fechar o shelf em questão (user.close()).

Em outro momento, poderíamos recuperar os dados da seguinte forma:

>>> import shelve
>>> user = shelve.open('data.txt')
>>> print user
{'city': 'Blumenau', 'twitter': 'stummjr', 'nickname': 'stummjr'}
>>> user['blog'] = 'pythonhelp.wordpress.com'
>>> user.close()

Legal, né? O shelve nos dá uma forma bem prática de persistir dados. O exemplo acima mostra um caso bem simplificado, mas poderíamos usar um shelf para armazenar dados de várias pessoas, por exemplo:

>>> users = shelve.open('users.dat')
>>> users['stummjr'] = {'nickname': 'stummjr', 'blog': 'pythonhelp.wordpress.com', 'city': 'Blumenau'}
>>> users['eliasdorneles'] = {'nickname': 'eliasdorneles', 'blog': 'eljunior.wordpress.com', 'city': 'Floripa'}
>>> print users
{
    'eliasdorneles': {
        'blog': 'eljunior.wordpress.com',
        'city': 'Floripa',
        'nickname': 'eliasdorneles'
    },
    'stummjr': {
        'blog': 'pythonhelp.wordpress.com',
        'city': 'Blumenau',
        'nickname': 'stummjr'
    }
}

>>> users.close()
>>> users = shelve.open('users.dat')
>>> print users['stummjr']['blog']
pythonhelp.wordpress.com

Então, qual devemos usar?

Antes de mais nada, fique atento às restrições que cada abordagem possui. Por exemplo, dentro das opções apresentadas acima, a única que possui implementação em uma ampla variedade de linguagens é o JSON. Por outro lado, o shelve nos provê essa facilidade de manipular dados em dicionários e persistí-los no disco depois. Tudo irá depender do seu objetivo ao serializar os dados.

Interoperabilidade é importante? Então vá no JSON de olhos fechados. Quer uma forma de serializar dados para uma única plataforma e que seja econômica no tamanho dos dados? Talvez struct seja a sua escolha. Enfim, leia a documentação e descubra qual das alternativas acima melhor se encaixa em suas necessidades.

Templates de páginas com Python e Google App Engine

Sumário

def get(self):
    self.response.out.write("<html><head><title>Lista de Comentários</title></head><body>")
    for c in Comentario.objects.all():
        self.response.out.write("""<p>Nome: %s</p><p>Email: %s</p>
            <p>URL: %s</p><p>Comentário: %s</p>""" % 
                (c.nome_usuario, c.email, c.url, c.comentario))
    self.response.out.write("</body></html>")

Diga aí, o que você achou da implementação do método get() no código acima? Esse monte de HTML embutido em strings no código Python fica horrível, não?

Pois então, este post vai mostrar uma forma mais adequada para usar conteúdo em HTML no seu projeto web: os templates (ou, em bom português, modelos).

Templates são HTMLs açucarados

Templates, em um projeto Web com Python + Google App Engine, nada mais são do que arquivos HTML acrescidos de algumas marcações específicas para representar dados dinâmicos.

<html>
    <head><title></title></head>
    <body>
        <p>Nome: {{ nome }}</p>
        <p>Email: {{ email }}</p>
        <p>URL: {{ url }}</p>
        <p>Comentário: {{ comentario }}</p>
    </body>
</html>

Observe que em meio ao HTML, temos 4 campos diferentes: {{ nome }}, {{ url }}, {{ email }} e {{ mensagem }}. Em um template, sempre que houver uma string do tipo {{ nome }}, o mecanismo de templates tentará substituir tal string pelo valor da variável nome, de forma que esse valor apareça no HTML final (depois veremos como passar valores aos templates).

Além de campos para simples substituição pelo valor de variáveis, o mecanismo de template padrão do GAE fornece construções para fazer repetição, seleção, dentre outras coisas. Veja:

<html>
    <head><title></title></head>
    <body>
        {% for m in mensagens %}
            {% if m.publico %}
                <p>Nome: {{ m.nome_usuario }}</p>
                <p>Email: {{ m.email }}</p>
                <p>URL: {{ m.url }}</p>
                <p>Comentário: {{ m.comentario }}</p>
                <hr>
            {% endif %}
        {% endfor %}
    </body>
</html>

Tudo que o template acima precisaria receber é uma variável chamada comentarios, que fosse uma sequência de objetos do tipo Comentario.

Desse modo, separamos a parte de apresentação da parte lógica do nosso projeto. O código fica mais organizado, limpo e reusável. Vamos ver agora como acoplar esse mecanismo de templates ao nosso projetinho web do post anterior.

Juntando tudo

Precisamos, antes de mais nada, salvar o nosso arquivo que contém o template em um arquivo .html. Vamos chamá-lo de modelo.html:

<html>
    <head><title></title></head>
    <body>
        <h1>Comentários</h1>
        {% for m in mensagens %}
            <hr>
            <h2>Comentário de <em>{{ m.nome_usuario }}</em></h2>
            <p><strong>Email:</strong> {{ m.email }}</p>
            <p><strong>URL:</strong> {{ m.url }}</p>
            <p><strong>Comentário:</strong> {{ m.comentario }}</p>
        {% endfor %}
        <hr>
        <form method="POST" action="/">
            Nome: <p><input type="text" name="nome_usuario"></p>
            Email: <p><input type="text" name="email"></p>
            URL: <p><input type="text" name="url"></p>
            Comentario:<p><textarea name="comentario" cols="40" rows="10"></textarea></p>
            <button type="submit">Enviar</button>
        </form>
    </body>
</html>

Veja que nosso template primeiro renderiza os comentários e, no final deles, apresenta o formulário para envio de uma nova mensagem. Vamos agora reescrever o nosso método get() do post anterior. O que antes era:

def get(self):
    self.response.out.write(html)
    self.response.out.write('<ul>')
    for msg in Mensagem.all():
        self.response.out.write('<li>' + unicode(msg.comentario) + '</li>')
    self.response.out.write('</ul>')

Passará a ser:

def get(self):
    path = os.path.join(os.path.dirname(__file__), 'modelo.html')
    valores = {'mensagens': Mensagem.all()}
    self.response.out.write(template.render(path, valores))

Chamamos a função render() para gerar uma string com conteúdo HTML, com base no arquivo de template modelo.html (passado através da variável path) e do conjunto de valores a ser inserido de forma dinâmica no nosso HTML (dicionário valores). Lembra que, dentro do template, nós referenciávamos uma variável mensagens?

...    
{% for m in mensagens %}
    <hr>
    <h2>Comentário de <em>{{ m.nome_usuario }}</em></h2>
    <p><strong>Email:</strong> {{ m.email }}</p>
    <p><strong>URL:</strong> {{ m.url }}</p>
    <p><strong>Comentário:</strong> {{ m.comentario }}</p>
{% endfor %}
...

No exemplo acima, mensagens terá o conteúdo de Mensagem.all(), que é uma sequência contendo todas as mensagens existentes no datastore. Assim, esse trecho do template irá percorrer as mensagens e imprimir os valores dos campos nome_usuario, email, url e comentario nos locais correspondentes do HTML. Tudo isso é feito no lado servidor, ou seja, o navegador irá receber o HTML já pronto com os valores nos locais adequados. Um exemplo de trecho de HTML gerado no servidor poderia ser:

...
    <hr>
    <h2>Comentário de <em>Pedro Pedreira</em></h2>
    <p><strong>Email:</strong> pedro@pedreira.com</p>
    <p><strong>URL:</strong> https://pythonhelp.wordpress.com/</p>
    <p><strong>Comentário:</strong> Um comentário qualquer. Valeu!</p>
...

Onde Pedro Pedreira é o valor de {{ m.nome_usuario }}, e assim por diante, para um registro qualquer.

Caso não tenha visto o post anterior, veja o código do projeto para então aplicar as mudanças para inclusão do template, ou então siga a leitura neste post para ver o projeto completo.

O projeto completo

Nosso projeto está localizado no diretório gaetest/ e possui a seguinte estrutura:

gaetest/
    app.yaml
    handlers.py
    modelo.html

Agora vamos ver o conteúdo dos arquivos:

app.yaml:

application: gaetest
version: 1
runtime: python
api_version: 1

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

handlers.py:

import os
import webapp2
from google.appengine.ext import db
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp.util import run_wsgi_app


class PostHandler(webapp2.RequestHandler):

    def get(self):
        path = os.path.join(os.path.dirname(__file__), 'modelo.html')
        valores = {'mensagens': Mensagem.all()}
        self.response.out.write(template.render(path, valores))

    def post(self):
        msg = Mensagem(
            nome_usuario=self.request.get('nome_usuario'),
            url=self.request.get('url'),
            email=self.request.get('email'),
            comentario=self.request.get('comentario')
        )
        msg.put()
        self.redirect('/')


class Mensagem(db.Model):
    nome_usuario = db.StringProperty(required=True)
    url = db.LinkProperty()
    email = db.EmailProperty()
    comentario = db.TextProperty(required=True)


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

modelo.html:

<html>
    <head><title></title></head>
    <body>
        <h1>Comentários</h1>
        {% for m in mensagens %}
            <hr>
            <h2>Comentário de <em>{{ m.nome_usuario }}</em></h2>
            <p><strong>Email:</strong> {{ m.email }}</p>
            <p><strong>URL:</strong> {{ m.url }}</p>
            <p><strong>Comentário:</strong> {{ m.comentario }}</p>
        {% endfor %}
        <hr>
        <form method="POST" action="/">
            Nome: <p><input type="text" name="nome_usuario"></p>
            URL: <p><input type="text" name="url"></p>
            Email: <p><input type="text" name="email"></p>
            Comentario:<p><textarea name="comentario" cols="40" rows="10"></textarea></p>
            <button type="submit">Enviar</button>
        </form>
    </body>
</html>

Executando o projeto

Salve o conteúdo acima nos arquivos correspondentes dentro da pasta gaetest e então rode o servidor local de testes. Para isso, vá até a pasta onde você descompactou o pacote na instalação do GAE e rode o seguinte comando:

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

Onde /caminho/para/o/projeto/gaetest/ é o caminho para o diretório onde está o projeto que queremos executar. Após isso, acesse o projeto no seu navegador através da URL localhost:8080.

Boas práticas

Boas práticas são muito importantes na hora de desenvolver um projeto de software. À medida que o projeto vai crescendo, a urgência por uma boa organização dos arquivos também cresce. É comum termos projetos com uma estrutura parecida com essa:

project/
    app.yaml
    handlers.py
    models.py
    templates/
        base.html
        form.html
        list.html

Veja que temos agora dois arquivos .py:

  • handlers.py: as classes que irão manipular nossas requisições;
  • models.py: as classes que representam os modelos de dados em nosso projeto (como a classe Mensagem no nosso exemplo);

Além disso, temos uma pasta específica para conter os templates e dentro dela ficam todos os templates que precisarmos.

Você é livre para definir a estrutura que quiser, mas é bom seguir um padrão que seja minimamente organizado.

Outros mecanismos de template

O mecanismo de template que vimos neste post é o que já vem incluso com o pacote do GAE, que por sua vez é o mecanismo do Django. Além desse, existem vários outros mecanismos que podem ser usados, como o jinja2, por exemplo.

Leia mais