Desenvolvendo com Python e Google App Engine

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

Sumário do post:

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

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

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

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

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

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

O Google App Engine

gaelogo

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

http.5.1

Painel de controle do GAE

Bom, chega de propaganda e vamos ao que interessa!

Instalando e configurando o GAE

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

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

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

Desenvolvendo nosso app

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

frasedodia/
  frasedodia.py
  app.yaml

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

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

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

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

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

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

Manipulador de requisições

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

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

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

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

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

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

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

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

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

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

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

class FraseDoDia(webapp2.RequestHandler):

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

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

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

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

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

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

Executando o ambiente de testes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Aprenda mais

Programando para a Web em Python

Sumário

Este é o quarto post da série sobre Python + Web. Nós já vimos como fazer requisições HTTP a partir de um programa Python, como fazer extração de dados de conteúdo HTML obtido via HTTP e como lidar com dados em formato JSON retornados por um serviço Web. Porém, até agora trabalhamos somente no lado cliente, obtendo dados via HTTP para então fazer algo com eles. A partir de agora vamos para o lado do servidor, onde iremos escrever o provedor do serviço.

Neste post, veremos como construir um servicinho web bem simples, que retorne para o cliente o dia da semana relativo a determinada data, codificado em formato JSON.

Web em Python

Python é uma linguagem muito versátil, podendo ser usada para vários fins. Um dos nichos em que Python mais tem sido usada ultimamente é no desenvolvimento web. E quando se trata de web, temos várias ferramentas disponíveis para usar com Python. Este post irá apresentar a forma mais simples de todas: o CGI (Common Gateway Interface).

CGI

O funcionamento do CGI é bem simples: ao receber uma requisição, o servidor web roda um programa executável para gerar o conteúdo a ser retornado para o cliente (é esse programinha executável que vamos implementar). Assim, um programinha bem simples que poderia funcionar juntamente com um servidor CGI é:

#!/usr/bin/python

# cabecalho que informa o browser para renderizar como HTML
print "Content-Type: text/html\n\n"
# o conteudo em si
print "<strong>Hello, world!</strong>"

Se você quiser brincar um pouquinho com CGI, siga os seguintes passos:

  1. Crie uma pasta chamada src e dentro dela outra pasta chamada cgi-bin;
  2. Salve o arquivo acima como hello.py dentro da pasta cgi-bin recém criada;
  3. Dê permissão de execução ao arquivo hello.py: chmod +x hello.py;
  4. Pelo shell de comandos do seu sistema operacional, vá até a pasta src e execute o servidorzinho CGI que Python traz consigo: python -m CGIHTTPServer 8000
  5. Abra um navegador e acesse a URL: http://localhost:8000/cgi-bin/hello.py
  6. Como resultado, você deverá ver a mensagem “Hello, world!” em negrito no seu navegador.

Mas, para que faça sentido, é preciso que nosso aplicativo seja capaz de obter eventuais informações que o usuário passar a ele. Usando o módulo cgi, podemos manipular parâmetros fornecidos pelo usuário, dentre outras possibilidades. Veja o exemplo abaixo:

#!/usr/bin/python
import cgi

# obtem referencia as variaveis passadas pelo cliente
fields = cgi.FieldStorage()

# cabecalho que informa o browser para renderizar como HTML
print "Content-Type: text/html\n\n"

nome = fields.getvalue('nome')
print "<strong>Hello, %s!</strong>" % (nome)

Salve o arquivo acima no mesmo diretório cgi-bin com o nome de hellouser.py, dê permissão de execução para o mesmo (chmod +x hellouser.py) e então acesse no seu browser: http://localhost:8000/hellouser.py?nome=Seu Nome Aqui.

Como você deve saber, /hellouser.py?nome=Seu Nome Aqui significa que estamos passando para o “programa” /hellouser.py a variável nome com o valor Seu Nome Aqui, tudo pela URL, através do método HTTP GET.

Preparando a implementação do serviço

Antes de sairmos implementando detalhes do serviço web que nos comprometemos a implementar, vamos criar uma funçãozinha que será parte essencial do nosso projeto. Essa função irá receber 3 valores: dia, mês e ano, e irá retornar o dia da semana em forma de string. Por exemplo:

dia_da_semana(27, 3, 2013) --retorna--> "Quarta-feira"
dia_da_semana(24, 3, 2013) --retorna--> "Domingo"

Para implementá-la, vamos usar o método weekday() dos objetos date, disponível no módulo datetime. A documentação dessa função diz o seguinte (tradução própria):

Retorna o dia da semana como um inteiro, onde Segunda-feira é 0 e Domingo é 6.

Assim, tudo o que teremos que fazer é construir uma instância de date com os dados fornecidos como parâmetro para nossa função, e invocar o método weekday() nesse objeto. Exemplo:

>>> from datetime import date
>>> d = date(2013, 3, 27)
>>> print d.weekday()
2

2 significa que a data em questão, 27/03/2013 cai numa Quarta-feira, visto que Segunda-feira é 0 e que Domingo é 1. Perceba que para construir um objeto do tipo date, nós passamos as informações sobre dia, mês e ano em ordem inversa à que estamos acostumados no Brasil. A assinatura do método construtor de objetos date é:

date(ano, mês, dia)

Perfeito, agora só falta definirmos uma forma de obtermos os nomes dos dias em língua portuguesa. Vamos usar uma abordagem bem simples, mas que resolve nosso problema. Vamos usar uma lista contendo as strings representando os dias da semana. (claro que se fôssemos dar suporte a usuários de vários países, a história seria diferente)

from datetime import date

dias = [
    'Segunda-feira',
    'Terça-feira',
    'Quarta-feira',
    'Quinta-feira',
    'Sexta-feira',
    'Sábado',
    'Domingo'
]

def dia_da_semana(dia, mes, ano):
    try:
        return dias[date(ano, mes, dia).weekday()]
    except ValueError:
        return 'Erro: data mal-formatada.'

Simples, huh? Utilizamos o inteiro retornado pelo método weekday() como índice para nossa lista com os nomes dos dias da semana. Além disso, envolvemos nosso código com try-except pois o usuário pode muito bem fornecer uma data inválida, como 31/02/2013 ou 100/123/2013. O construtor date() levanta a exceção ValueError quando recebe parâmetros que não representam uma data válida.

Feito isso, agora podemos implementar nosso serviço. (leia atentamente o código acima, caso não tenha entendido algo)

Implementando o serviço web

Já vimos como implementar um aplicativozinho web beeeeeeeem simples usando CGI, também já vimos como obter o dia da semana em que determinada data cai, e num post anterior vimos como codificar e decodificar dados em formato JSON. Agora, basta juntarmos as peças para implementar o serviço.

Só para relembrar, estamos implementando um serviço web que retorna o dia da semana relativo à determinada data fornecida pelo usuário. O usuário irá fornecer a data no formato dd/mm/aaaa. Por exemplo, o usuário pode solicitar a data fazendo uma requisição ao serviço da seguinte forma: http://um.host.qualquer.com/diasemana.py?data=27/3/2013.

Segue o código do nosso serviço:

#!/usr/bin/python
# -*- encoding:utf-8 -*-
import cgi
import json
from datetime import date

dias = [
    'Segunda-feira',
    'Terça-feira',
    'Quarta-feira',
    'Quinta-feira',
    'Sexta-feira',
    'Sábado',
    'Domingo'
]

def quebra_data(data):
    if (data.count('/') == 2 and
            all((x.isdigit() for x in data.split('/')))):
        return [int(x) for x in data.split('/')]
    else:
        raise ValueError

def dia_da_semana(data):
    try:
        dia, mes, ano = quebra_data(data)
        return dias[date(ano, mes, dia).weekday()]
    except ValueError:
        return 'Erro: data mal-formatada.'

print "Content-Type: text/json\n\n"
fields = cgi.FieldStorage()
data = fields.getvalue('data')
dia_str = dia_da_semana(data) if data is not None else 'Erro'
print json.dumps({'data': data, 'diasemana': dia_str})

Como a data fornecida pelo usuário é recebida no programa através do cgi.FieldStorage() sob a forma de string, é preciso que separemos essa data em dia, mês e ano. Para isso, criamos a função quebra_data() que antes de mais nada verifica se a data fornecida pelo usuário está no formato adequado (dia/mês/ano), e caso não esteja, a função levanta uma exceção ValueError para ser capturada pela função dia_da_semana(). Se a data estiver no formato correto, é retornada uma lista com três valores inteiros: dia, mês e ano. O método split(), usado em nossa função quebra_data separa a string em questão em todas as ocorrências do caractere separador fornecido como entrada ('/') e retorna uma lista contendo as substrings.

Ao final de nosso programinha, geramos um dicionário com as informações a serem codificadas em JSON e então imprimimos a informação JSONificada.

Momento “Lição de Vida”

Como você pode perceber lendo o código acima, boa parte da lógica do programa envolve a verificação dos dados fornecidos pelo usuário. Em se tratando de serviços ou aplicativos web, nunca podemos ser relapsos com os dados fornecidos pelo usuário, afinal o sistema está exposto pela web, podendo ser acessado de qualquer lugar do mundo, por pessoas com os mais variados níveis de conhecimento e com as mais variadas intenções. Às vezes, a falta de validação de um simples dado fornecido pelo usuário pode ser o suficiente para que o usuário destrua o nosso serviço ou, o que muitas vezes é pior, obtenha acesso à informações que deveriam ser sigilosas.

Portanto, nunca deixe de verificar todo e qualquer dado que um usuário forneça para o seu sistema. Às vezes ficamos com preguiça, com pena de poluir o código com “todas aquelas validações”, ou até achando que “os nossos usuários nunca tentariam sacanear o sistema”, mas não caia na bobagem de seguir esses pensamentos. Existem várias histórias de sistemas que foram destruídos e de pessoas que perderam seus empregos por deixar de filtrar os dados fornecidos pelo usuário.

Python + CGI está na crista da onda, então?

Não. Programação web com CGI nada mais é do que escrevermos programas que serão executados pelo servidor web quando houver uma requisição para eles. Essa simplicidade toda tem o seu custo: a cada requisição recebida pelo servidor web, ele inicia um novo processo que irá executar o código do recurso solicitado. Assim, recursos que de outra forma poderiam ser compartilhados entre instâncias são perdidos ao fim de cada processo. Conexões com o banco de dados, por exemplo, são reestabelecidas a CADA requisição feita para o serviço quando usamos CGI. Ou seja, sistemas web baseados em CGI dificilmente irão escalar quando necessário for.

Além disso, programar somente com CGI não é muito prático, pois temos que nos preocupar com diversos elementos que podem ser abstraídos e passados para uma outra camada. E outra coisa, código Python imprimindo HTML é muito anos 90! 😉

Para facilitar a nossa vida e resolver alguns dos problemas acima descritos, existem os frameworks para desenvolvimento web, que serão assunto para um próximo post.

Então CGI é inútil?

Não, longe disso. Com CGI, podemos executar programas remotamente e, de lambuja, ver o resultado da execução desse programa em nosso navegador. Se ele gerar HTML na saída, melhor ainda! 🙂

MAS, é claro que não vamos deixar exposto na web um programa que apaga todos os arquivos do disco rígido do servidor em que está rodando. 😛 Só que nada impede que disponibilizemos um scriptzinho inocente via CGI para que outra pessoa possa o acessá-lo remotamente, sem a necessidade de possuir acesso SSH àquela máquina. O legal é que tudo que é preciso para o acesso é um web browser, e, como você sabe, hoje em dia até as TVs possuem web browsers, sem falar nos telefones celulares.

Acessando conteúdo via APIs Web baseadas em JSON

Quem acompanhou os dois posts anteriores (aqui e aqui) sabe que neles nós realizamos buscas por conteúdo dentro de arquivos HTML. Quem conhece código HTML, sabe que ele não é um formato muito amigável para extração de conteúdo, principalmente quando mal-usado pelos desenvolvedores. Apesar disso, conseguimos fazer o webscraper funcionar, graças ao fato de o reddit.com apresentar um HTML com informações bem classificadas, fáceis de serem extraídas. Mas, mesmo assim, baixar um HTML para depois extrair informações dele não é a melhor solução existente.

Alguns serviços na Web fornecem um mecanismo para acesso às informações de forma mais direta, disponibilizando seu conteúdo através de estruturas em formato JSON. Os caras do reddit, que de bobos não tem nada, disponibilizam várias informações através de arquivos JSON. Veja um exemplo em: http://www.reddit.com/r/programming/.json. Se você se assustou com o conteúdo que o browser lhe mostrou ao acessar esse endereço, fique tranquilo, pois já vamos ver do que ele se trata.

JSON – JavaScript Object Notation

JSON (que é lido Jason, como em Sexta-feira 13) é um padrão que estabelece um formato para troca de dados entre programas, sendo usado principalmente na web. Ele tem sido muito usado na web como alternativa ao formato XML, que até então era o padrão de facto para representação de dados a serem trocados.

O interessante do JSON é que nossa aplicação escrita em Python pode enviar e receber dados usando esse formato com aplicações escritas em outras linguagens de uma forma razoavelmente maleável. Cada linguagem fornece uma maneira de transformar dados no formato JSON em objetos nativos da linguagem e vice-versa, de forma que se você descobrir que precisará enviar mais informações do que havia pensado inicialmente, basta adicioná-las no JSON enviado que as demais aplicações já poderão usá-las. Isso permite que você enxergue duas aplicações de linguagens de programação e plataformas diferentes como se fossem duas funções Python que recebem um dicionário como argumento. Além disso, o JSON é um formato que pode ser facilmente compreendido por humanos, sendo também utilizado como formato de arquivos de configuração de alguns programas.

Veja um exemplo de dados em formato JSON (adaptado de http://en.wikipedia.org/wiki/JSON):


{
    "primeiroNome": "Joao",
    "ultimoNome": "Smith",
    "idade": 25,
    "endereco": {
        "rua": "Rua Assis Brasil, 1000",
        "cidade": "Blumenau",
        "estado": "SC"
    },
    "telefones": [
        "5555-5555",
        "9999-9999"
    ],
    "emails": [
        {
            "tipo": "pessoal",
            "endereco": "[email protected]"
        },
        {
            "tipo": "profissional",
            "endereco": "[email protected]"
        }
    ]
}

Se quiser ver um exemplo grande de JSON, veja aqui.

Como você deve ter percebido, o conteúdo JSON acima tem o formato BEM similar ao formato adotado para representação de dicionários em Python. Assim como nos dicionários, em JSON um objeto pode ter seus atributos representados sob a forma:

chave:valor

O acesso aos atributos pode ser realizado através das chaves de cada um deles. Para entender melhor, vamos abrir um shell Python e testar o módulo json.

import json
data = '''
    {
        "primeiroNome": "Joao",
        "ultimoNome": "Smith",
        "idade": 25,
        "endereco": {
            "rua": "Rua Assis Brasil, 1000",
            "cidade": "Blumenau",
            "estado": "SC"
        },
        "telefones": [
            "5555-5555",
            "9999-9999"
        ],
        "emails": [
            {
                "tipo": "pessoal",
                "endereco": "[email protected]"
            },
            {
                "tipo": "profissional",
                "endereco": "[email protected]"
            }
        ]
    }
'''

Os dados em formato JSON nada mais são do que strings formatadas de acordo com as convenções definidas na especificação. Sendo strings, não temos uma forma simples de acessar os valores armazenados através das chaves. Por exemplo, o seguinte código falha porque data é uma string:

    >>> data['primeiroNome']
    ...
    TypeError: string indices must be integers, not str

Tendo os dados JSONificados (codificados em formato JSON) em uma string, podemos decodificá-los para que, ao invés de uma só string, tenhamos os dados em objetos Python:

json_data = json.loads(data)

Como resultado da decodificação, obtivemos um dicionário:

>>> type(json_data)
<type 'dict'>
>>> print json_data.keys()
[u'telefones', u'ultimoNome', u'idade', u'primeiroNome', u'endereco', u'emails']

Agora, se quisermos acessar o primeiro nome do usuário, fazemos:

>>> print json_data['primeiroNome']
Joao
>>> print json_data['telefones']
[u'5555-5555', u'9999-9999']

O valor correspondente à chave 'telefones' é uma lista, e assim sendo, o acesso aos seus elementos é feito através de um valor do tipo int como índice:

>>> print json_data['telefones'][0]
5555-5555
>>> print json_data['telefones'][1]
9999-9999


A lista é a estrutura para a qual os Arrays representados em JSON são traduzidas em Python.

Vamos agora acessar o nome da rua onde mora o cidadão representado acima:

>>> print json_data['endereco']['rua']
Rua Assis Brasil, 1000


Uma vez tendo sido decodificado o conteúdo JSON, o acesso aos elementos que o compõem é bem fácil, pois o processo de decodificação gera um dicionário. Em resumo, quando obtivermos um conteúdo em formato JSON em uma string, podemos usar a função json.loads() para decodificá-lo, transformando-o em um dicionário Python. O contrário também pode ser feito. Podemos transformar um objeto Python em string JSON, como veremos abaixo:

>>> dict_data = {'nome': 'joao da silva', 'idade': 20, 'telefones': ['99999999', '55555555']}
>>> json_str = json.dumps(dict_data)
>>> print json_str
{"idade": 20, "telefones": ["99999999", "55555555"], "nome": "joao da silva"}
>>> type(json_str)
<type 'str'>

A tabela abaixo mostra um resumo das duas funções vistas:

Função Funcionalidade
json.dumps() Transforma um objeto em string JSON.
json.loads() Transforma uma string JSON em objeto.

Acessando a API Web do reddit

Como comentei anteriormente, o pessoal do reddit disponibiliza uma série de informações em formato JSON, que podem ser acessados através de simples requisições HTTP. Chamamos esse conjunto de recursos que o reddit oferece a outros desenvolvedores de API Web, pois é definida uma interface para funções que retornam as informações desejadas em formato JSON. Dê uma olhada na documentação da API web do reddit em: www.reddit.com/dev/api. Vamos analisar rapidinho uma das funções fornecidas nessa API:


GET /new
    after   fullname of a thing
    before  fullname of a thing
    count   
    limit   the maximum number of items desired (default: 25, maximum: 100)
    show    
    target

A documentação acima indica que podemos obter os links mais recentes publicados no reddit através de um GET HTTP no recurso /new, podendo passar os parâmetros listados logo abaixo. Se você clicar no link api.reddit.com/new, o seu browser irá realizar uma requisição GET para o serviço fornecido pela API em api.reddit.com/new e como resultado irá receber um conteúdo JSON que será mostrado a você. Podemos também passar parâmetros para o serviço, como no link api.reddit.com/new?limit=2 onde passamos o parâmetro limit com valor 2, fazendo com que o serviço nos retorne somente os dois links mais recentes.

Você deve estar achando estranho ficarmos acessando conteúdo JSON no browser, afinal não é tão fácil assim ler e entender do que se tratam os valores que vemos na tela, não é? Fique tranquilo, pois essas APIs não foram feitas para que nós, como usuários do browser as acessemos. Elas foram criadas para que a gente escreva programas que acessem tais recursos e então interpretem o JSON retornado.

Sabendo que a API nos fornece acesso aos links mais controversos do momento através do recurso api.reddit.com/controversial, vamos implementar um programinha que busque a lista com os 5 links mais controversos do reddit no momento. O programa deve gerar a seguinte saída:


x/y - Descrição do link - URL do link
x/y - Descrição do link - URL do link
x/y - Descrição do link - URL do link
x/y - Descrição do link - URL do link
x/y - Descrição do link - URL do link

Onde x representa a quantidade de upvotes (votos positivos) e y a quantidade de downvotes (votos negativos) recebidos pelo post.

Talk is cheap, show me the code

sabemos como acessar um recurso na web e como decodificar o JSON recebido como resposta:

import json
import requests
r = requests.get('http://api.reddit.com/controversial?limit=5')
if r.status_code == 200:
    reddit_data = json.loads(r.content)

Até aí tudo tranquilo. A última linha do código acima cria um dicionário contendo o conteúdo JSON convertido para objetos Python. Mas e o que tem dentro desse dicionário?


{
    'kind': 'Listing',
    'data': {
        'modhash': '',
        'children': [{
                'kind': 't3',
                'data': {
                    'domain': 'i.imgur.com',
                    'banned_by': None,
                    'media_embed': {},
                    'subreddit': 'WTF',
                    'selftext_html': None,
                    'selftext': '',
                    'likes': None,
                    'link_flair_text': None,
                    'id': '1ajwg4',
                    'clicked': False,
                    'title': 'This was the disabled toilet at an airport in Myanmar. I was questioned by security for 25 minutes after taking it.',
                    'media': None,
                    'score': 1,
                    'approved_by': None,
                    'over_18': False,
                    'hidden': False,
                    'thumbnail': '',
                    'subreddit_id': 't5_2qh61',
                    'edited': False,
                    'link_flair_css_class': None,
                    'author_flair_css_class': None,
                    'downs': 25,
                    'saved': False,
                    'is_self': False,
                    'permalink': '/r/WTF/comments/1ajwg4/this_was_the_disabled_toilet_at_an_airport_in/',
                    'name': 't3_1ajwg4',
                    'created': 1363673738.0,
                    'url': 'http://i.imgur.com/gRqqYTq.jpg',
                    'author_flair_text': None,
                    'author': 'mfizzled',
                    'created_utc': 1363644938.0,
                    'distinguished': None,
                    'num_comments': 17,
                    'num_reports': None,
                    'ups': 26
                }
            }, 
            // outros elementos foram omitidos para simplificar
        ],
        'after': 't3_1ajoim',
        'before': None
    }
}

Alguns elementos foram ocultados para simplificar. Se quiser ver o JSON completo, clique aqui.

Para obter a lista contendo todos os posts, vamos acessar a chave 'data' no dicionário reddit_data que obtivemos ao decodificar o JSON recebido. O valor relacionado à chave 'data' possui dentro de si um item cuja chave é 'children', que contém a lista de posts. Nessa lista, obtida acessando reddit_data['data']['children'], temos 5 elementos, cada um contendo dois pares chave-valor: kind e data, sendo que este último contém os dados do link em si. Assim, podemos rapidamente verificar quais dados existem dentro de um link:

      print reddit_data['data']['children'][1]['data']
      {
          'domain': 'imgur.com',
          'banned_by': None,
          'media_embed': {},
          'subreddit': 'funny',
          'selftext_html': None,
          'selftext': '',
          'likes': None,
          'link_flair_text': None,
          'id': '1akkpt',
          'clicked': False,
          'title': 'Girls love when boys can cook...(fixed)',
          'media': None,
          'score': 6,
          'approved_by': None,
          'over_18': False,
          'hidden': False,
          'thumbnail': '',
          'subreddit_id': 't52qh33',
          'edited': False,
          'link_flair_css_class': None,
          'author_flair_css_class': None,
          'downs': 43,
          'saved': False,
          'is_self': False,
          'permalink': '/r/funny/comments/1akkpt/girls_love_when_boys_can_cookfixed/',
          'name': 't3<em>1akkpt',
          'created': 1363692074.0,
          'url': 'http://imgur.com/JfOg96S',
          'author_flair_text': None,
          'author': 'backwardsgiant21',
          'created_utc': 1363663274.0,
          'distinguished': None,
          'num_comments': 7,
          'num_reports': None,
          'ups': 49
      }
      


Veja que os atributos em que estamos interessados estão dentro de data. Para imprimir a URL do segundo link da lista, poderíamos fazer:

>>> print reddit_data['data']['children'][1]['data']['url']
http://imgur.com/JfOg96S

Sabendo disso, agora ficou fácil. Basta percorrer os 5 elementos retornados quando acessamos reddit_data['data']['children'] e obter os dados que precisamos desse elemento. Segue o código:

import json
import requests
r = requests.get('http://api.reddit.com/controversial?limit=5')
if r.status_code == 200:
    reddit_data = json.loads(r.content)
    for link in reddit_data['data']['children']:
        print "%s/%s - %s - %s" % (link['data']['ups'], link['data']['downs'], link['data']['title'], link['data']['url'])

Para tornar nosso código mais reusável, podemos extrair uma função do código acima:

import json
import requests
def get_controversial(limit):
    result = []
    r = requests.get('http://api.reddit.com/controversial?limit=%s' % (limit))
    if r.status_code == 200:
        reddit_data = json.loads(r.content)
        for link in reddit_data['data']['children']:
            result.append((link['data']['ups'], link['data']['downs'], link['data']['title'], link['data']['url']))
    return result

Se você ficou com dúvida sobre como acessamos os elementos que foram retornados no JSON, veja novamente o arquivo JSON do exemplo para entender o porquê de termos acessados as chaves 'data', 'children', 'data', 'ups', etc.

Caso tenha entendido tudinho, sugiro que tente resolver o desafio a seguir.

Desafio

Cada link submetido por usuários no reddit pode ser votado, tanto de forma positiva (upvotes) quanto de forma negativa (downvotes). Esses votos são usados para criar o score (pontuação) do link da seguinte forma:


score = upvotes - downvotes

Essa informação está disponível no JSON, podendo ser acessado através da chave 'score'.

Sabendo disso, escreva um programa que, usando a API Web do reddit, busque a lista com os 20 links mais recentes e apresente somente a URL do link que obtiver o melhor score. Mas, aqui você não deverá utilizar o score tradicional. Você deverá usar um score calculado da seguinte forma:


score = upvotes - downvote * 2

Ou seja, um voto negativo anula dois votos positivos do link. E aí, vai encarar?

Webscraping em Python

No post anterior, vimos como fazer requisições HTTP utilizando a excelente requests. A maioria dos documentos disponíveis na web estão em formato HTML, o que não é um grande problema quando acessamos via browser, pois ele é quem faz o trabalho sujo de renderizar o documento pra gente. Mas, se quisermos extrair apenas determinados trechos de informações que podem nos ser úteis, precisaremos percorrer o documento HTML em busca dessas informações, o que não é nada fácil, visto que além de pouca gente seguir os padrões para composição de documentos HTML, muitas vezes os criadores dos sites de onde queremos obter as informações não estão nem um pouco preocupados em classificar as informações que ali disponibilizam, deixando os dados soltos em uma barafunda de table, div, p, etc.

Nesse post vamos ver como fazer Web Scraping, que é a extração de dados de páginas web.

Scraping no reddit

Como já mencionei no post anterior, o reddit é um agregador de conteúdo submetido pelos usuários, e tem vários subreddits voltados a assuntos específicos. Por exemplo, o subreddit programming é um dos meus preferidos, pois além de apresentar as notícias mais relevantes, também apresenta artigos técnicos e discussões de altíssima qualidade. Veja a imagem abaixo, que contém a página inicial desse subreddit:

http2.1

O objetivo desse post é mostrar como fazer um extrator dos principais links submetidos pelos usuários ao reddit programming, usando uma técnica diferente daquela mostrada no final do post anterior.

O resultado esperado é um programinha em linha de comando que gere a seguinte saída:

Descrição do link - URL do link
Descrição do link - URL do link
Descrição do link - URL do link
Descrição do link - URL do link
Descrição do link - URL do link

A primeira coisa que temos que fazer quando queremos desenvolver um extrator de informações da web é entender a estrutura dos documentos HTML de onde vamos retirar a informação. Vamos dar uma olhada no código-fonte do reddit:

http2.2

Na imagem acima, estou usando o developer tools do Chrome para facilitar a visualização das informações do que precisamos no HTML (ctrl+alt+c abre essa ferramenta). Veja que ele já faz o highlighting das informações tanto no código-fonte quanto no documento renderizado. Assim é moleza descobrir o que precisamos extrair do HTML, não? Podemos ver na imagem acima que cada link submetido pelos usuários fica dentro de um elemento p com um atributo class com valor title, como vemos no trecho abaixo (retirado do reddit):

...
    <a class="title " href="http://www.ocamlpro.com/blog/2013/03/14/opam-1.0.0.html">
        OPAM, a new package manager for OCaml is officially released!
    </a>
    <span class="domain">
        (<a href="http://www.reddit.com/domain/ocamlpro.com/">
            ocamlpro.com
        </a>)
    </span>
...

Dentro do elemento p, temos outro elemento, a, que contém um atributo com a URL em si (href) e que tem como conteúdo o título do link, que também é de nosso interesse. A imagem abaixo mostra em mais detalhes uma parte do conteúdo da página que nos interessa.

http2.3

Perceba que precisamos extrair da página várias instâncias de informação como essa mostrada acima, pois para cada link submetido teremos uma estrutura como a apresentada acima. Assim, teremos que procurar por todas as ocorrências de links no documento HTML e extrair essas informações de lá.

Agora que já sabemos onde está a informação que precisamos em meio à bagunça do HTML, chegou o desafio principal, que é extrair essa informação usando um programinha. Mas, antes de meter a mão na massa, vamos conhecer a ferramenta que vai nos possibilitar fazer a extração dos dados de uma forma mais prática: a biblioteca BeautifulSoup.

BeautifulSoup

No post anterior, no último trecho de código, mostrei uma forma meio tosca de buscar a informação no HTML. Vou repetir aquele código aqui para você lembrar sobre o que estou falando:

import requests

def cria_lista_links(content):
    links = []
    for line in content.split('</p>'):
        index = line.find('class="title ')
        if index != -1:
            href_start = line.find('href="', index) + 6
            href_end = line.find('"', href_start)
            links.append(line[href_start:href_end])
    return links

r = requests.get("http://www.reddit.com/r/programming")
print '\n'.join(cria_lista_links(r.content))

É tosca porque não é muito precisa, visto que nossa busca se baseia somente em uma informação textual dentro de um conjunto de linhas com o HTML. Veja que fazemos a busca usando o caractere de " (aspas duplas) ou a string href= como parâmetro. Se o autor da página mudar de aspas duplas para aspas simples, ou de href= para href =, nossa busca já irá parar de funcionar.

A forma mais confiável de fazermos isso é obtida através do parsing (análise sintática) do conteúdo, de forma que ele nos devolva elementos HTML com seus atributos em uma estrutura bem fácil de manipular. Mas, fazer um bom parser para HTML daria bastante trabalho, ainda mais um parser que funcione bem com todas as variações que são usadas em documentos HTML pela web. A boa notícia é que esse parser já existe e está disponível pra gente através da biblioteca BeautifulSoup, que vamos utilizar daqui pra frente.

Analisando código HTML com BeautifulSoup

Considere o HTML apresentado abaixo. Observe com atenção a sua estrutura.

    <html>
        <head>
            <title>Uma página qualquer</title>
        </head>
        <body>
            <div id="content">
                <div class="content-title">
                    Aqui vai um conteúdo 
                    <a class="link" href="http://www.google.com/">qualquer</a>, 
                    um texto sem fundamento algum, escrito sem a mínima 
                    intenção de despertar qualquer interesse por parte
                    do <a class="link-old"> leitor</a>.
                </div>
                <div class="content-author">
                    Escrito por um autor qualquer, que não tinha algo 
                    melhor para escrever.
                </div>
                <div class="content-data">
                    16/03/2013
                </div>
            </div>
            <div class="footer">
                Nenhum direito reservado.
            </div>
        </body>
    </html>

Para entender melhor o HTML acima, vamos ver a árvore de representação dele: http2.4 Agora vamos iniciar um exemplo de análise do HTML acima usando o BeautifulSoup.

import BeautifulSoup

html_data = '''
<html>
    <head><title>Uma página qualquer</title></head>
    <body>
        <div id="content">
            <div class="content-title">
                Aqui vai um conteúdo 
                <a class="link" href="http://www.google.com/">qualquer</a>, 
                um texto sem fundamento algum, escrito sem a mínima 
                intenção de despertar qualquer interesse por parte
                do <a class="link-old"> leitor</a>.
            </div>
            <div class="content-author">
                Escrito por um autor qualquer, que não tinha 
                algo melhor para escrever.
            </div>
            <div class="content-data">
                16/03/2013
            </div>
        </div>
        <div class="footer">
            Nenhum direito reservado.
        </div>
    </body>
</html>'''

soup = BeautifulSoup.BeautifulSoup(html_data)

*Por praticidade, o conteúdo HTML foi inserido em uma string enorme chamada html_data.

Já podemos extrair informações da página, que foi “parseada” na última linha do trecho acima. Perceba que os atributos existentes em cada elemento possuem os mesmos nomes dos elementos no código HTML (.title por exemplo, se refere ao elemento title da página).

print soup.title
#<title>Uma página qualquer</title>

print soup.title.text
# Uma página qualquer

O atributo text de qualquer elemento sempre retorna o texto que está contido em tal elemento. Podemos também encadear o acesso a elementos. Abaixo, estamos acessando o primeiro div do body do HTML analisado.

print soup.body.div
# <div id="content"><div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div><div class="content-author">Escrito por um autor qualquer, que não tinha algo melhor para escrever.</div><div class="content-data">16/03/2013</div></div>

Perceba que no trecho de código acima foi impresso somente um dos dois divs que compõem o body (somente o de id content). Olhando para a árvore e para o valor impresso, vemos que foi impressa toda a subárvore do primeiro div do body. Como temos mais de um div dentro do body, quando tentamos acessar o atributo div, nos é retornado somente o primeiro dos divs. Para obter todos os divs do body, podemos fazer assim:

print soup.body.contents
# [u'\n', <div id="content"><div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div><div class="content-author">Escrito por um autor qualquer, que não tinha algo melhor para escrever.</div><div class="content-data">16/03/2013</div></div>, <div class="footer">Nenhum direito reservado.</div>, u'\n']

O atributo contents de um elemento qualquer, retorna os elementos que são seus filhos na árvore do HTML. Ou então, podemos percorrer os “filhos” de um elemento, usando o generator childGenerator():

for x in soup.body.childGenerator():
    print x, '\n'

Como resultado, teremos:

<div id="content"><div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div><div class="content-author">Escrito por um autor qualquer, que não tinha algo melhor para escrever.</div><div class="content-data">16/03/2013</div></div 

<div class="footer">Nenhum direito reservado.</div>

Além de acessar os elementos filhos de uma tag, podemos também acessar o elemento pai através do atributo parent. Aí vai um exemplo:

a = soup.body.div.a
print a.parent
# <div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div>

Como você pode ver, o elemento pai de a é o próprio div que o antecede.

Buscando por elementos específicos

Se quisermos buscar todos os elementos a de uma página HTML, podemos usar o método findAll():

print soup.findAll('a')
# [<a class="link" href="http://www.google.com/">qualquer</a>, <a class="link-old"> leitor</a>]

O método retorna uma lista contendo todas as ocorrências de elementos do tipo procurado. Também podemos obter elementos através de seus atributos id. Em nosso HTML, temos um div com id="content". Podemos obtê-lo com:

print soup.find(id="content")
# <div id="content"><div class="content-title">Aqui vai um conteúdo <a class="link" href="http://www.google.com/">qualquer</a>, um texto sem fundamento algum, escrito sem a mínima intenção de despertar qualquer interesse por parte do <a class="link-old"> leitor</a>.</div><div class="content-author">Escrito por um autor qualquer, que não tinha algo melhor para escrever.</div><div class="content-data">16/03/2013</div></div>

Ou então, buscar por elementos que estejam classificados em uma classe específica:

print soup.find(attrs={'class':'footer'})
# <div class="footer">Nenhum direito reservado.</div>

Enfim, dá pra fazer muita coisa usando a BeautifulSoup. Se quiser conhecê-la melhor, veja a documentação: http://www.crummy.com/software/BeautifulSoup/bs4/doc/.

Implementando o scraper do reddit

Como vimos anteriormente, um link no reddit possui a seguinte estrutura:

...

    <a class="title " href="http://www.ocamlpro.com/blog/2013/03/14/opam-1.0.0.html">
        OPAM, a new package manager for OCaml is officially released!
    </a>
    <span class="domain">
        (<a href="http://www.reddit.com/domain/ocamlpro.com/">
            ocamlpro.com
        </a>)
    </span>

...

Primeiro, precisamos obter o HTML direto do site do reddit:

import requests
import BeautifulSoup

r = requests.get('http://www.reddit.com/r/programming/')
soup = BeautifulSoup.BeautifulSoup(r.content)

Depois, vamos obter cada elemento a que possua o atributo class com valor igual a "title ". Podemos fazer isso usando o método findAll(), percorrendo o resultado e extraindo os atributos que nos interessam (href e text):

for a in soup.findAll('a', attrs={'class': 'title '}):
    print "%s - %s" % (a.text, a.get('href'))

Podemos melhorar o código acima, criando uma função que obtenha a lista de links de um subreddit qualquer:

import requests
import BeautifulSoup

def get_links_from(subreddit):
    links = []
    r = requests.get('http://www.reddit.com/r/%s/' % (subreddit))
    if r.status_code != 200:
        return None

    soup = BeautifulSoup.BeautifulSoup(r.content)

    for a in soup.findAll('a', attrs={'class': 'title '}):
        links.append((a.text, a.get('href')))
    return links

A função acima retorna uma lista contendo tuplas de pares (título do link, URL do link). Assim, podemos percorrer essa lista, imprimindo os valores:

for link_name, link_url in get_links_from('programming'):
    print "%s - %s" % (link_name, link_url)

for link_name, link_url in get_links_from('python'):
    print "%s - %s" % (link_name, link_url)

Está pronto! Barbada, hein? Que tal agora você começar a estudar aquele site de onde você quer extrair informações e implementar um webscraper?

Boa sorte! 🙂

Leitura adicional

Acessando recursos na web com Python

Uma das coisas mais divertidas de se fazer com um computador é programá-lo para que busque informações na web para que não precisemos ficar clicando em um monte de páginas até chegar à informação que desejamos.

Poderíamos escrever um programa para buscar e mostrar os próximos horários de ônibus de determinada linha de uma forma mais amigável do que o portal da empresa de transporte urbano (busaoemfloripa). Ou então, poderíamos agregar várias informações de estações de monitoramento de chuvas e nível de rios para criar um sistema de apoio à população em épocas de cheia dos rios (enchentes.org). E que tal um programinha que busque na web o horário em que o seu programa de TV favorito será transmitido e que emita um alerta no seu desktop quando estiver chegando a hora? São várias as possibilidades de pequenos aplicativos que podem vir a tornar a nossa vida mais prática. Mas, para implementá-los, precisamos saber fazer algumas coisas:

  1. Acessar recursos que estão na web através de nossos programas.
  2. Filtrar os dados que são retornados nesses recursos para que possamos utilizá-los em nosso programa.

Nesse post, veremos como fazer para acessar recursos da web via HTTP. Mais tarde, em um post futuro, veremos como filtrar os dados retornados para usarmos em nossos programas.

Acessando a web com a API Requests

Antes de mais nada, para seguir este post, você precisará instalar o módulo requests.

Instalando o módulo requests

O módulo pode ser instalado via PIP:

$ pip install requests

Outras formas de instalação podem ser vistas em: http://docs.python-requests.org/en/latest/user/install/

Acessando um recurso simples

Usar o módulo requests é muito simples. Vamos começar acessando um recurso simples na web, como a lista de notícias do reddit programming:

>>> import requests
>>> response = requests.get('http://www.reddit.com/r/programming/')
>>> print response.status_code  # 200 significa requisição OK
200
>>> print len(response.content)
87773
>>> print response.content[:100]  # imprimindo só os 100 primeiros chars
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" ><head><title>prog

A função requests.get() obtém um recurso qualquer através de uma URL passada como argumento. O caso acima é o mais simples possível, pois sequer requer autenticação para fazer a requisição. A chamada a essa função retorna um objeto do tipo Response, que dentre outras coisas contém o status da requisição, que é um código numérico indicando o que aconteceu com a requisição. Esse status pode ser checado antes de acessarmos o conteúdo da requisição, para verificar se esta ocorreu com sucesso ou não.

O conteúdo da resposta enviada pelo servidor é armazenado no atributo content da resposta e pode ser acessado como qualquer outro atributo (response.content no código acima). Como você pode ver, o conteúdo retornado foi um tanto quanto grande (uma string de comprimento 87773), e por isso eu trunquei a impressão do conteúdo no código acima.

Possíveis problemas

Vamos tentar acessar um recurso inválido, como: http://www.google.com/umrecursoinvalido. Se tentarmos acessá-lo via browser, teremos a seguinte resposta:

http1

Ou seja, realizamos uma requisição e o status dessa requisição é 404, que ocorre quando conseguimos nos conectar ao servidor, mas este não encontra o recurso solicitado. Veja o que acontece em nosso código:

>>> response = requests.get('http://www.google.com/umrecursoinvalido')
>>> print response.status_code
404

Outro erro comum é fazermos uma requisição para uma URL inexistente (que não possui mapeamento nos DNS), como por exemplo: http://umaurlquenaoapontapranada.com/. Nesse caso, é disparada uma exceção de erro de conexão, visto que não é possível estabelecer uma conexão com o servidor, sendo impossível assim ter um código de status:

>>> response = requests.get('http://umaurlquenaoapontapranada.com')
Traceback (most recent call last):
  File "", line 1, in
  File "/usr/lib/python2.7/dist-packages/requests/api.py", line 52, in get
    return request('get', url, **kwargs)
  File "/usr/lib/python2.7/dist-packages/requests/api.py", line 40, in request
    return s.request(method=method, url=url, **kwargs)
  File "/usr/lib/python2.7/dist-packages/requests/sessions.py", line 229, in request
    r.send(prefetch=prefetch)
  File "/usr/lib/python2.7/dist-packages/requests/models.py", line 605, in send
    raise ConnectionError(e)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='umaurlquenaoapontapranada.com', port=80): Max retries exceeded with url: /

Veja a lista de possíveis códigos de status HTTP em: http://pt.wikipedia.org/wiki/Anexo:Listadecódigosdestatus_HTTP.

Obtendo um recurso com parâmetros

Quando queremos fazer uma requisição a um recurso na web usando um navegador, é comum passarmos alguns parâmetros na própria URL, através de uma requisição HTTP usando o método GET. Por exemplo, para fazer uma busca no Google, devemos passar um parâmetro q para http://www.google.com/search. Para passar o parâmetro via URL basta adicionar um ? ao final e então adicionar os parâmetros e valores ligados por =. Se passarmos mais de um parâmetro, devemos separá-los por &. Veja um exemplo:

http://www.uma.url.qualquer.com/recurso?parametro1=valor1&parametro2=valor2&maiscoisa=continua

Então, para soliciarmos uma busca ao google.com, podemos fazer uma requisição GET à URL:

http://www.google.com/search?q=o+que+quisermos+pesquisar

Podemos passar os parâmetros para a URL através de um dicionário que passamos como entrada à função requests.get(). O dicionário possui o seguinte formato:

{'nome do parametro 1': 'valor do parâmetro 1', 'nome do parâmetro 2': 'valor do parâmetro 2'}

Vamos fazer então uma busca no Google:

>>> response = requests.get("http://www.google.com/search", params={'q': 'busca qualquer coisa'})
>>> print response.status_code == 200
True

A página HTML que nosso browser iria renderizar se fosse ele o cliente da requisição é então armazenada no atributo response.content. Se você der uma observada no conteúdo desse atributo, verá que não parece nada fácil tirar algo útil dali, pois há uma quantidade enorme de ruído em meio à informação. Em outro post veremos como filtrar os dados de forma a “pegar” somente a parte útil da resposta, ignorando o HTML e outros códigos que vem junto com a resposta.

Acesso com autenticação básica

Alguns recursos que queremos acessar exigem uma autenticação HTTP para liberar o acesso. Como por exemplo, tente acessar a seguinte URL: http://httpbin.org/basic-auth/user/passwd. O navegador irá lhe confrontar com a seguinte tela, solicitando as suas credenciais:

http2

Para passar os dados de usuário e senha com a requisição HTTP que estivermos fazendo, basta passsar um parâmetro chamado auth junto com a requisição. Primeiramente, vamos fazer uma requisição sem passar as credenciais:

>>> r = requests.get("http://httpbin.org/basic-auth/user/passwd")
>>> print r.status_code
401

O código 401 indica que a requisição não pode ser completada porque as credenciais de acesso estão incorretas. Agora vamos passar as credencias corretas (nome de usuário é 'user' e a senha é 'passwd'):

>>> r = requests.get("http://httpbin.org/basic-auth/user/passwd", auth=('user', 'passwd'))
>>> print r.status_code
200

Perceba que auth é um parâmetro que recebe uma tupla com os dados do usuário a ser autenticado.

O método POST

O método GET do protocolo HTTP é usado para a obtenção de dados de algum servidor web. Mas, se quisermos que nosso programa envie dados para algum servidor, devemos utilizar o método POST do HTTP.

Fazer requisições POST em um código Python nos permite inúmeras possibilidades, como por exemplo o preenchimento e submissão automática de formulários na web. Vamos testar o post em um serviço de testes, o httpbin:

>>> r = requests.post('http://httpbin.org/post', data={'comentario': 'huaaaaaaa'})
>>> print r.status_code
200
>>> print r.content
{
  "origin": "xxx.xxx.xxx.xxx",
  "files": {},
  "form": {
    "comentario": "huaaaaaaa"
  },
  "url": "http://httpbin.org/post",
  "args": {},
  "headers": {
    "Content-Length": "20",
    "Via": "1.0 PROXY",
    "Accept-Encoding": "identity, deflate, compress, gzip",
    "Connection": "close",
    "Accept": "*/*",
    "User-Agent": "python-requests/0.12.1",
    "Host": "httpbin.org",
    "Cache-Control": "max-age=259200",
    "Content-Type": "application/x-www-form-urlencoded"
  },
  "json": null,
  "data": ""
}

Conseguimos fazer a requisição e recebemos como resposta um recurso que se parece com um dicionário, mas que na verdade é um conteúdo codificado como JSON, que é um padrão para codificação de objetos para troca de mensagens pela rede (o JSON também será visto em um post futuro).

Um exemplo funcional

No início do post, acessamos o reddit.com usando o requests e obtivemos como resposta um monte de código HTML misturado com o conteúdo que nos interessa. Para quem não conhece, o reddit é um agregador de notícias submetidas e votadas pelos seus usuários. Com o slogan “the front page of the internet”, o reddit possui várias categorias e nelas são listadas as principais notícias/artigos do dia. Sabendo disso, vou mostrar um programinha bem simples e tosco que imprime na tela a lista de links da categoria programming do reddit:

import requests

def cria_lista_links(content):
    links = []
    for line in content.split('</p>'):
        index = line.find('class="title ')
        if index != -1:
            href_start = line.find('href="', index) + 6
            href_end = line.find('"', href_start)
            links.append(line[href_start:href_end])
    return links

r = requests.get("http://www.reddit.com/r/programming")
print '\n'.join(cria_lista_links(r.content))

IMPORTANTE: o código acima não é um exemplo de boas práticas!

O código acima busca o conteúdo HTML e percorre esse conteúdo verificando em cada linha se ela possui o texto 'class="title ', o que indica que a linha é um link. Depois, extraímos a URL de cada link e adicionamos à lista de resultado.

Como você pode ver, é bem fácil fazer esse trabalho de filtragem. Basta um pouquinho de paciência, ler o código HTML e descobrir padrões que possam ser buscados para fazer a filtragem. PORÉM, o código acima está longe de ser a melhor solução para o problema. Em um post futuro, veremos a forma correta de “filtrar” e extrair informações úteis de conteúdo HTML, usando um módulo específico para isso. Mas, o exemplo acima foi apresentado justamente para lhe mostrar que você já tem conhecimento suficiente para fazer o seu próprio programinha para filtrar dados das páginas que você gosta de acessar.

Vá em frente e se tiver dúvidas, comente aqui!