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": "joao@joao.com"
        },
        {
            "tipo": "profissional",
            "endereco": "joao.smith@algumaempresa.com"
        }
    ]
}

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": "joao@joao.com"
            },
            {
                "tipo": "profissional",
                "endereco": "joao.smith@algumaempresa.com"
            }
        ]
    }
'''

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?

Anúncios

12 comentários sobre “Acessando conteúdo via APIs Web baseadas em JSON

  1. Pingback: Programando para a Web em Python | Python Help
  2. Pingback: Dicas para lidar com JSON | Python Help
  3. Pingback: Serialização de Objetos em Python | Python Help
  4. Pingback: Acessando APIs REST com Python | Python Help

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s