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?

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!

Os métodos mágicos de Python

Obs.: Códigos testados com a versão 2.7 do Python.

Quem está chegando em Python normalmente fica um pouco confuso ao ler o código de uma classe e perceber um monte de métodos com underscores (__) no nome. Para entender o que são esses métodos, vamos ver um exemplo.

Uma classe para números binários

Suponha que, por algum motivo, você receba a tarefa de implementar uma classe para representação de números em base binária.

class Binario(object):

    def __init__(self, valor_dec):
        self.valor_dec = valor_dec
        self.valor_bin = bin(self.valor_dec)  #bin() é uma função builtin

b = Binario(5)
print b

Se executarmos o código acima, teremos em b um objeto do tipo Binario, que representa o valor 5 em base-2. 5 em base binária é 101. Porém, a execução da linha print b mostrou a seguinte saída na tela:

<__main__.Binario object at 0x28286d0>


Isso porque o print executa a função str() no objeto recebido. Essa função, por sua vez, procura por um método chamado __str__() no objeto a ser impresso. Como não definimos um método com esse nome em nossa classe, o interpretador continua sua busca pelo método na classe que está acima de Binario na hierarquia de classes, que é object. Lá ele encontra o método __str__, que então retorna o texto <__main__.Binario object at 0x28286d0>, contendo o endereço do objeto na memória.

O método __str__() deve retornar uma representação em forma de string do valor do objeto. Para personalizar essa mensagem, ou seja, para fazer com que o print em objetos do tipo Binario mostre uma sequência de 0s e 1s representando o número binário em questão, vamos adicionar um método __str__() à nossa classe:

class Binario(object):

    def __init__(self, valor_dec):
        self.valor_dec = valor_dec
        self.valor_bin = bin(self.valor_dec)

    def __str__(self):
        return "%s" % (self.valor_bin)

b = Binario(5)
print b

Agora, o resultado da execução do código acima é o seguinte:


0b101

Que é o formato retornado pela função bin() quando chamada. O prefixo 0b é adicionado para indicar que se trata de um número binário. Podemos facilmente nos livrar desse prefixo para representar o número binário na tela usando operadores de slicing:

def __str__(self):
    return "%s" % (self.valor_bin[2:])

Beleza! Agora nosso número binário pode ser impresso corretamente na tela! 🙂

Sem perceber e sem chamá-los em lugar algum, já utilizamos dois métodos mágicos de Python:

  • __init__: método chamado para inicialização do objeto, logo após a sua construção;
  • __str__: método chamado pela função str() para obter o valor do objeto em forma de string;

Chamamos eles de métodos mágicos porque eles resolvem o nosso problema sem sequer termos que chamá-los. Quem os chama são códigos de outras classes/programas, que esperam que nossos objetos forneçam tais métodos.

E se agora quisermos comparar objetos do tipo Binario em operações relacionais como >, <, >=, <=, != e ==? Se tentarmos comparar duas instâncias de Binario usando algum desses operadores, podemos ter respostas inesperadas, visto que eles não irão fazer o que esperamos. O esperado é que a > b retorne True se o valor de a for maior do que o valor de b. Porém, onde definimos qual valor será usado para comparação dos objetos? Como não fizemos isso, o interpretador irá usar o id de ambos os objetos para a comparação.

Para definir como os objetos de nossa classe serão comparados, podemos implementar o método mágico __cmp__. Na documentação oficial, vemos instruções sobre como implementar esse método para que nossos objetos possam ser comparados e usados em operações relacionais:


object.__cmp__(self, other)
Deve retornar um inteiro negativo se self < other, zero se self == other, ou um número positivo se self > other.

Vamos então implementar o nosso método __cmp__. Podemos, para isso, usar o valor em decimal, que armazenamos internamente na variável self.valor_dec:

def __cmp__(self, other):
    if self.valor_dec > other.valor_dec:
        return 1
    elif self.valor_dec < other.valor_dec:
        return -1
    else:
        return 0

Que poderia também ser escrito como:

def __cmp__(self, other):
    return self.valor_dec - other.valor_dec

Tendo adicionado o código acima à classe Binario, agora podemos utilizar nossos objetos em operações relacionais:

b = Binario(1)
c = Binario(2)
if b < c:
    print "OK"

Mais uma vez, nosso método é executado sem que o chamemos explicitamente. Além dos métodos que vimos aqui, existem vários outros métodos mágicos que podemos implementar em nossos objetos para que o comportamento deles se pareça mais com o comportamento de objetos nativos da linguagem. Vou listar alguns deles a seguir:

  • __add__(self, other): para adicionarmos a possibilidade de aplicação do operador + aos nossos objetos. Para os outros operadores, também existem métodos mágicos (subtração(-): __sub__; multiplicação(*): __mul__, divisão(/): __div__, módulo(%): __mod__, potência(**): __pow__);
  • __call__(self): faz com que o objeto seja chamável (executável), assim como uma função é;
  • __len__: retorna o comprimento do objeto (se for um container);
  • __getitem__(self, key): para containers, retorna o elemento correspondente à chave key;

São muitos os métodos. Se você quiser conhecê-los melhor, sugiro dar uma olhada nesse texto (em inglês): http://www.rafekettler.com/magicmethods.html.

Os métodos mágicos (magic methods), também chamados de métodos dunderscore (double-underscore) ou de métodos especiais, são muito úteis pois permitem que objetos de nossas classes possuam uma interface de acesso semelhante aos objetos nativos da linguagem. A função sorted(), por exemplo, ordena os elementos de um iterável de acordo com o valor dos objetos que a compõe. Se definirmos nosso método de comparação, a função sorted() irá usá-lo para fazer a ordenação dos elementos da lista. Assim, é possível que códigos de terceiros lidem com nosso código sem sequer conhecê-lo. Veja mais sobre esse conceito em: Polimorfismo.

Entendendo “tudo é objeto em Python”

Nessa última semana, li um excelente post sobre o modelo de execução do Python escrito pelo Jeff Knupp e tive vontade de escrever sobre um assunto que é tratado por ele no post: muita gente fala que em Python tudo é um objeto, mas nem todos entendem o que isso realmente significa. Vou tentar ilustrar aqui com um exemplo que acho bem interessante, que é a declaração de classes.

A palavra reservada class

Antes de ler o livro Learning Python, eu tinha uma ideia um pouco distorcida sobre classes em Python. Para mim, a palavra class tinha um quê de mágica envolvida. Com a leitura, aprendi que class é um construtor de objetos, e não somente uma declaração existente no código. Isso porque class é executada pelo interpretador quando encontrada no código, e o resultado dessa execução é um novo objeto existente no escopo do módulo. A diferença deste para um construtor qualquer, é que este constrói objetos do tipo type, permitindo assim que criemos objetos desse tipo recém criado. Vamos ver um exemplo rápido no interpretador:

>>> class Teste(object): pass
>>> print type(Teste)
<type 'type'>

Talvez você esteja pensando: “grande coisa, é a mesma coisa que em outras linguagens orientadas a objetos que já usei…”. Mas se acalme aí, pois o melhor ainda está por vir. Tendo criado o objeto Teste com a declaração class, agora podemos acessá-lo como qualquer outro objeto. Podemos acrescentar atributos à classe:

>>> Teste.x = 0
>>> print Teste.x
0
>>> t1 = Teste()
>>> print t1.x
0

Perceba que acrescentamos o atributo x ao objeto Teste, e que isso faz com que instâncias dessa classe passem a possuir esse atributo. E aí, você fazia isso em Java? Isso permite que, por exemplo, adicionemos métodos a uma classe já existente:

>>> def funcao(self): print 'valor de x:', self.x
>>> Teste.f = funcao
>>> t2 = Teste()
>>> t2.f()
valor de x: 0

Como a palavra def também cria objetos (do tipo function), podemos manipular funções como se fossem outros objetos quaisquer. Preste atenção à linha 2 do trecho acima (Teste.f = funcao) e perceba que não estamos fazendo uma chamada à função funcao. O que fizemos foi criar um atributo f em Teste, e então fizemos com que esse atributo referencie o objeto funcao, que incidentalmente é um objeto “executável”, um objeto function. Assim, todas as instâncias de Teste passam a ter também uma função f em seu escopo.

Feito isso, peço que releia os trechos de código acima e depois, diga se a seguinte linha de código irá funcionar ou não:

>>> t1.f()

O que você acha? A resposta é: a linha acima é executada com sucesso e a saída na tela de sua execução é mostrada abaixo:


valor de x: 0

Algo estranho? Perceba que t1 foi instanciado antes de adicionarmos o método f à classe Teste. Como ele incorporou esse método? Para entender isso, é preciso entender o modelo de resolução de nomes e hierarquia de objetos em Python.

Hierarquia de classes

Quando criamos uma classe, como fizemos com o objeto Teste, estamos criando um objeto que descende de object, que é um objeto do tipo type e que possui vários atributos e métodos que são herdados pelas classes descendentes. Ao criarmos uma instância de Teste, como t1 e t2, estamos criando objetos que descendem de Teste, e por transitividade, descendem também de object. Veja a imagem abaixo:

mro

Essa é a hierarquia existente em tempo de execução em Python. Sempre que tentamos acessar um atributo de um objeto, o interpretador faz o seguinte: verifica se o objeto em questão possui tal atributo; caso não possua, verifica se o objeto que está imediatamente acima na hierarquia possui tal atributo, e faz isso sucessivamente até encontrar ou chegar a object. Se chegar em object e nem ele possuir o atributo procurado, ocorre um AtributeError. Isso é feito para cada acesso a atributos, em tempo de execução. Isso explica o porquê de t1.f() ter funcionado corretamente, mesmo t1 tendo sido criado antes de termos adicionado f a Teste. O que ocorre é bem simples: ao tentar acessar t1.f(), o interpretador busca por f em t1 e não encontra. Assim, busca por f no objeto imediatamente acima de t1, que é Teste, o encontra lá e executa o código do objeto.

Interessante, não? 🙂

Leia mais:

Variáveis, valores e referências

Já vi muita gente tendo problemas em código Python simplesmente por não entender direito como Python lida com os conceitos de variáveis. Considere o exemplo abaixo:

>>> x = [1, 2, 3]
>>> y = x
>>> x.append(4)
>>> print y
************

Qual o resultado esperado pela execução da última linha (print y)? Se você ficou na dúvida, mesmo que por um curto período de tempo, leia este post até o fim, que você irá entender melhor.

Nome

Grave o seguinte:

Em Python, uma variável é apenas um NOME que REFERENCIA a um OBJETO.

Veja o exemplo abaixo:

>>> x = 42

O código acima é muitas vezes lido como “atribui o valor 42 à variável x”. Mas, o que Python faz é o seguinte: cria um objeto do tipo int que possui 42 como valor, cria o nome x e faz com que o nome x referencie o objeto (do tipo int) 42. Assim, toda vez que o nome x for usado em seu código, ele será automaticamente substituído pelo valor do objeto que este nome referencia (42). A imagem abaixo ilustra melhor a relação entre x e 42.

var1

Continuando o exemplo anterior, o que acontece se fizermos o seguinte?

>>> x = x + 1

É simples, o nome x passa a fazer referência a um novo objeto do tipo int, cujo valor é 43.

A imagem a seguir dá uma idéia melhor sobre o que acontece.

var2

Objetos do tipo int são imutáveis. x = x + 1 cria um novo objeto do tipo int (cujo valor é determinado pela soma de x com 1) e faz com que x passe a referenciar esse novo objeto. Se você observar a imagem acima, verá que não há mais seta alguma apontando para o valor 42, isto é, não há mais nenhum nome fazendo referência àquele objeto. Normalmente, um objeto que não possui nome algum o referenciando vira candidato a coleta de lixo, que é um mecanismo que elimina da memória objetos que não são mais necessários. Mas, o interpretador Python não realiza esse processo em objetos do tipo int e do tipo str (quando pequenos). Ao invés disso, ele mantém esses objetos em uma espécie de cache, para não ter que recriá-los em um futuro próximo e a todo momento em que forem necessários. Se quiser confirmar isso:

>>> 42 is 42  # ambos são o mesmo objeto
True
>>> 'ola' is 'ola'
True
>>> [] is []  # o mesmo já não vale para listas
False

Como comentei anteriormente, toda vez que um nome de variável aparece em uma expressão, esse nome é substituído pelo valor do objeto ao qual ele faz referência. Sabendo disso, considere a expressão abaixo:

>>> y = x

O interpretador cria um novo nome y e faz com que ele referencie o objeto referenciado por x, como mostra a figura abaixo:

var3

Agora, o que acontece se fizermos o seguinte?

>>> x = 10

É criado um objeto int com valor 10, e x então passa a referenciar a esse novo objeto.

var4

Tá, e daí?

E daí que entendendo isso tudo, você achará mais natural alguns comportamentos em Python. Por exemplo, teste o seguinte código e tente entender o que acontece:

>>> x = [1, 2, 3]
>>> y = x
>>> x.append(4)
>>> print x
[1, 2, 3, 4]
>>> print y
[1, 2, 3, 4]

Como mostra a imagem abaixo, y = x faz com que y passe a referenciar o mesmo objeto que x referencia.

var5

Assim, x.append(4) tem efeito sobre o objeto referenciado agora pelas duas variáveis (x e y).

Isso ocorre porque listas, em Python, são objetos mutáveis. O método append() modifica a lista de modo in-place, isto é, as modificações são feitas no próprio objeto, sem a necessidade de criação de uma nova lista, como ocorreria com objetos imutáveis, como strings ou ints, por exemplo.

Mutável vs Imutável

Vamos ver agora um exemplo da diferença entre um objeto mutável (lista) e um objeto imutável (string). Temos dois objetos, l e s:

>>> l = [1, 2, 3]
>>> s = 'abc'

Queremos adicionar um novo elemento ao fim de cada um deles. Com a lista podemos usar o método append():

>>> l.append(4)

Que adiciona o valor 4 ao final de l, modificando-a. Com a string, não temos esse método disponível, então vamos usar o operador de concatenação:

>>> s = s + 'd'

O lado direito da expressão acima cria uma nova string com o conteúdo de s acrescido do caractere 'd' e faz com que o nome s passe a referenciar essa nova string. Ou seja, ao invés de modificar, foi criado um novo objeto. O antigo valor de s ('abc') passa então a ficar sem referência alguma a ele.

Quem é quem?

Para confirmar se duas variáveis referenciam o mesmo objeto, podemos usar o operador de identidade is que verifica se duas variáveis possuem como valor o mesmo objeto.

>>> x = [1, 2, 3]
>>> y = x
>>> x is y
True

Outra forma de verificar se duas variáveis se referem ao mesmo objeto é usando o comando id(), que retorna o identificador do objeto, que nada mais é do que um número inteiro que cada objeto possui para ser unicamente identificado:

>>> print id(x)
30008456
>>> print id(y)
30008456

Tanto x quanto y se referem ao objeto com o identificador 30008456, isto é, ao mesmo objeto ([1, 2, 3]).

Agora, se o que você deseja é fazer uma cópia do objeto lista, de modo que uma modificação na cópia não interfira no objeto original, existem algumas formas de fazer isso: usando o operador de fatiamento (slicing) ou o método copy(), disponível no módulo copy.

>>> x = [1, 2, 3]
>>> y = x[:] # cria uma nova lista com todo conteúdo de x e atribui a y
>>> x is y
False
>>> import copy
>>> y = copy.copy(x)
>>> x is y
False

Perceba que o método copy.copy() faz apenas o que chamamos de cópia rasa da lista, pois se a lista em questão possuir outras listas aninhadas, estas não serão copiadas, sendo somente suas referências copiadas. Para cópias profundas, use copy.deepcopy().

Passagem de parâmetros

Outra confusão muito comum é a passagem de parâmetros para funções. Veja o exemplo abaixo e tente descobrir o resultado da execução:

def func(x, y):
    x = x + 1
    y.append(4)

x = 1
y = [1, 2, 3]
func(x, y)
print 'x:', x
print 'y:', y

(Pare por um momento se for necessário antes de seguir a leitura e analise o código acima para descobrir o resultado.)

O resultado pode ser visto abaixo:

x: 1
y: [1, 2, 3, 4]

A função func recebeu dois argumentos. De forma leiga, poderíamos dizer que ambos sofreram alterações dentro da função, mas somente na lista a alteração persistiu fora do escopo da função. Isso tem uma explicação bem clara: para qualquer objeto passado para uma função, é feita uma cópia da referência do objeto para o escopo local da função. Assim, se o objeto for mutável, uma operação como append(), por exemplo, vai afetar o objeto referenciado pela variável e assim a alteração persiste fora do escopo da função. Com um objeto imutável, não é possível alterar o objeto em si. O que se faz é alterar a referência (x = x + 1, por exemplo). Como a referência é apenas uma cópia, qualquer alteração feita sobre ela não irá ter efeito na referência do escopo de fora. Por isso, x permanece referenciando o mesmo objeto (1) no escopo global após a função ter terminado.

Resumindo: para entender o comportamento da passagem de parâmetros em Python, basta entender a diferença entre objetos mutáveis e imutáveis, e também lembrar que toda variável em Python nada mais é do que uma referência para algum objeto da memória.

Além disso, é conveniente deixar de lado um pouco aquela noção de que x = x + 1 (ou operações parecidas) altera(m) o objeto. Lembre sempre que o lado direito da expressão (x + 1) cria um novo objeto e que o lado esquerdo da expressão (x =) faz com que o nome x passe a referenciar o novo objeto. Mais nada. 🙂

Pronto!

O conceito de variáveis e objetos em Python é bem simples e consistente. Fique atento ao seu código, principalmente quando você precisa realizar cópias de objetos, o que em Python não é feito usando o operador =. Ah, não se esqueça outras linguagens implementam esses conceitos de formas diferentes. Veja: Other languages have “variables”… Python has “names” .

Simplificando as coisas com namedtuples

Antes de conhecer e até mesmo nos primeiros projetos que desenvolvi usando Python, era bem comum que eu escrevesse classes que não continham método algum (a não ser os famigerados getters/setters e o construtor). Elas eram normalmente usadas para representar objetos que tinham um papel mais passivo, como por exemplo, uma classe para representar as mensagens que o cliente passava para o servidor:

class Message(object):
    headers = []
    content = None
    from_addr = None
    to_addr = None

Ou então:

class Message(object):
    def __init__(self, content, from_addr, to_addr):
        self.headers = []
        self.content = content
        self.from_addr = from_addr
        self.to_addr = to_addr

    def add_header(self, info):
        self.headers.append(info)

Além disso, eu definia os setters/getters para cada atributo (muitas vezes sem pensar se iria precisar deles ou não, afinal, a maioria das IDEs já geravam tudo pra mim).

Mas aos poucos veio a sensação de que eu estava subutilizando Python criando classes somente para representação de objetos simples assim. Afinal, além de ser uma linguagem com tipagem dinâmica, Python ainda oferece várias estruturas para representação dos dados. Minha primeira idéia foi utilizar tuplas para representar os objetos a serem trocados entre os elementos. Por exemplo, poderia representar uma mensagem através de uma tupla de 4 elementos:

message = ([], content, from_addr, to_addr)

Assim, para imprimir o conteúdo da mensagem, eu teria que fazer:

print message[1]

Assim, seria sempre necessário saber em qual posição da tupla determinado campo está armazenado. Um atentado à legibilidade e manutenção do código! Depois, pensei em usar dicionários:

message = {'headers': [], 'content': content, 'from_addr': from_addr, 'to_addr': to_addr}

Feito! O problema da legibilidade tinha acabado. Agora eu poderia acessar o campo content usando o nome do campo como chave:

print message['content']

Mas, ainda não me parecia uma solução muito adequada. Foi então que veio a luz: descobri a namedtuple, que faz parte do pacote collections. O próprio Guido Van Rossum, criador da linguagem Python, recomendou recentemente o uso de namedtuples para representação de objetos. Além de evitar a sobrecarga sintática que a definição de uma classe dá ao código, ela também oferece melhor desempenho.

Usando as namedtuples

A definição de estruturas para representação de objetos com namedtuples é bem simples. Veja o exemplo abaixo:

>>> import collections
>>> Message = collections.namedtuple('Message', 'headers content from_addr to_addr')

A segunda linha acima mostra a criação de uma namedtuple que chamamos de Message, e que possui 4 campos: headers, content, from_addr e to_addr. O primeiro argumento que passamos para a factory namedtuple() é o nome para o novo tipo (em nosso caso, Message) e o segundo argumento é uma string contendo os nomes dos campos da estrutura, separados por espaços em branco ('headers content from_addr to_addr'). A chamada à collections.namedtuple() retorna uma nova subclasse de tuple, podendo assim ser usado como se fosse um novo tipo.

Tendo a estrutura definida, podemos então criar objetos do tipo Message da seguinte forma:

>>> m = Message([], "Hello, server!", "me", "some.address.com")

A partir daí, o acesso aos campos da mensagem é feito exatamente como faríamos com instâncias de classes que definimos:

>>> print m.content
Hello, server!
>>> m.headers.append("alguma informação")
>>> print m.headers
["alguma informação"]
>>> m.to_addr = "localhost"
>>> print m.to_addr
localhost

Barbadinha, né? A partir de agora, quando for criar uma nova classe para representação de algo em seu projeto, verifique se não é o caso de definir o novo tipo usando namedtuples ao invés de uma nova classe. Se for possível, use a namedtuple. Mas, nem sempre uma namedtuple será suficiente para representar determinados objetos. Nesses casos, as classes são a solução mais adequada.

Desempacotamento de tupla

Tuple unpacking, ou em bom português, desempacotamento de tupla. Tá aí algo que usamos pra caramba, mas sobre o qual pouco ouvimos falar. Desempacotar uma tupla pode ser explicado como o ato de atribuir os elementos dela individualmente a outros objetos. Veja um exemplo de desempacotamento de tupla:

>>> t = (1, 2, 3, 4)
>>> a, b, c, d = t  # "desempacotando" uma tupla
>>> print b * c
6

No exemplo acima, o primeiro elemento da tupla t é atribuído para o nome a. O segundo elemento de t é atribuído para b, e assim por diante. É óbvio que, para desempacotar uma tupla, é necessário que tenhamos no lado esquerdo da expressão a quantidade necessária de variáveis para receber os valores. Veja uma situação que gera um erro por tentarmos desempacotar uma tupla de 4 elementos para 3 variáveis:

>>> t = (1, 2, 3, 4)
>>> a, b, c = t
Traceback (most recent call last):
  File "", line 1, in
ValueError: too many values to unpack

"ValueError: too many values to unpack", que significa “valores demais para desempacotar".

Mas isso é útil para quê?

Já viu uma função retornando mais de um valor em um único return em Python?

def f(x):
    return x, x*2, x*3

valor, dobro, triplo = f(2)

Isso é algo bastante comum em código Python. O código da segunda linha (return x, x*2, x*3) na realidade retorna apenas um valor, que é uma tupla. A expressão x, x*2, x*3 cria uma tupla de 3 elementos, apesar de não estar envolta em parênteses. Assim sendo, a chamada de função f(2) retorna uma tupla de 3 valores, que são então desempacotados para as variáveis valor, dobro e triplo, respectivamente.

Percorrendo listas de tuplas

Quando temos uma lista contendo tuplas, podemos percorrê-la assim como qualquer outra lista. Veja:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for tupla in lista:
...     print tupla
...
(1, 2, 3)
(2, 4, 6)
(4, 8, 12)
(8, 16, 24)

Dentro do for, poderíamos desempacotar os valores de cada tupla, se precisássemos usá-los individualmente:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for tupla in lista:
...     a, b, c = tupla
...     print a * b * c
...
6
48
384
3072

Mas, poderíamos fazer melhor, desempacotando os elementos dentro da expressão do for:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for (a, b, c) in lista:
...     print a * b * c
...
6
48
384
3072

Poderíamos também omitir os parênteses na hora de desempacotar as tuplas:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for a, b, c in lista:
...     print a * b * c
...
6
48
384
3072

Mas nem tudo interessa

Considere que nas tuplas da lista lista acima, somente nos interessa o primeiro elemento de cada tupla. Para não precisar criar dois novos nomes (b e c) que nunca serão usados, podemos utilizar um idioma que é um tanto comum em código python: usar o caractere de undescore _ como o nome da variável que vai receber o primeiro e último valores das tuplas:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for a, _, _ in lista:
...     print a * 2
...
2
4
8
16

Lembre-se: usamos isso somente em casos em que os elementos não serão necessários no escopo do for.

O _ é utilizado comumente como um nome para não me interessa. Por exemplo*, em uma tupla contendo nome, sobrenome e apelido, em um momento em que estamos interessados apenas no nome e no sobrenome, poderíamos usar o _ para indicar que o apelido não é importante neste momento:

dados_pessoais = ('Joao', 'Silva', 'joaozinho')
...
nome, sobrenome, _ = dados_pessoais

*Exemplo adaptado dessa resposta no StackOverflow.com

Como o autor da resposta no link acima comenta, é preciso tomar cuidado com o uso do _, pois ele também é utilizado em outros contextos e isso pode gerar confusão e mau-funcionamento. Ele é bastante usado com a biblioteca de internacionalização gettext como um atalho para funções usadas com muita frequência.

Então é isso. O desempacotamento de tuplas é um dos recursos que eu considero mais legais em Python, pois elimina bastante código desnecessário. Até a próxima! 🙂

Deu erro! E agora, o que eu faço?

Traceback (most recent call last):
  File "novo.py", line 12, in <module>
    hello()
  File "novo.py", line 10, in hello
    monta_lista(10)
  File "novo.py", line 5, in monta_lista
    return x[max]
IndexError: list index out of range

Se você sente calafrios só de ver a mensagem de erro acima, este post é para você. Você já ficou p*** da vida com o interpretador porque aquele programa recém escrito, perfeitinho, começou a apresentar mensagens de erro indecifráveis? Vá com calma, na maioria esmagadora dos casos, a culpa não é do interpretador, da linguagem, do sistema operacional ou do hardware. A culpa é quase sempre sua! (lembre-se sempre disso). Frases como “esse compilador está errado!” são muito comuns nos momentos de frustração, mesmo para programadores experientes. Mas caia na real, o compilador/interpretador só está fazendo o trabalho dele, o errado da história, em geral, é você*.

No momento de desespero, quando você não está entendendo o que está acontecendo para que o seu programa tenha problemas, é preciso que você conheça os mecanismos adequados para simplificar a tarefa de descobrir os problemas em seu código. Neste post nós vamos ver como podemos proceder para facilitar a descoberta de erros no nosso código. Vamos começar pelo basicão: como encontrar e identificar o erro em nosso código.

*É claro que não é impossível que você realmente tenha encontrado algum defeito no compilador ou no sistema operacional, mas esgote todas as possibilidades antes de partir para tal hipótese.

Entendendo melhor as mensagens de erro do Python

O traceback é a informação que o interpretador Python fornece para nos ajudar a identificar o problema em nosso código. Ele contém algumas informações úteis:

  • a sequência de chamadas de funções até o ponto onde ocorreu o problema;
  • o número da linha onde o erro foi gerado;
  • o tipo de erro que ocorreu, bem como uma pequena mensagem informativa sobre o ocorrido.

Às vezes o traceback parece meio enigmático, mas você verá que é bem fácil de entendê-lo. Vejamos um exemplo de um traceback bem simples:

Traceback (most recent call last):
  File "erro1.py", line 4, in <module>
    print (z+y)/x
ZeroDivisionError: integer division or modulo by zero

Mesmo sem olhar o código que o gerou, já podemos extrair algumas informações úteis:

  1. o erro foi gerado na linha 4 do arquivo erro1.py;
  2. o erro que ocorreu é chamado de ZeroDivisionError, indicando que ocorreu uma tentativa de dividir um valor por zero;
  3. o código da linha que gerou o problema é print (z+y)/x

Olhando agora para o código-fonte do programa que gerou o erro acima, podemos identificar logo o erro:

1  x = 0
2  y = 10
3  z = 3
4  print (z+y)/x

Tá na cara, estamos dividindo um valor por x, sendo que x é zero. Divisão por zero é uma indefinição matemática e nem o computador sabe lidar com isso.

Outro exemplo

Agora você está escrevendo um programinha que vai manipular uma lista com valores aleatórios.

#! -*- encoding:utf-8 -*-
from random import shuffle
TAM = 5
lista = range(TAM)
shuffle(lista)
print 'Primeiro:', lista[0]
print 'Último:', lista[TAM]

Ao executar, se depara com o seguinte saída, incluindo um traceback:

Primeiro: 3
Último:
Traceback (most recent call last):
  File "/home/user/src/erro2.py", line 7, in <module>
    print 'Último:', lista[TAM]
IndexError: list index out of range

Perceba que as duas primeiras linhas acima fazem parte da saída gerada pelo seu programinha. Vamos agora encontrar e identificar o erro:

  • De acordo com o traceback, o erro foi gerado na linha 7 do nosso arquivo. Essa é a última linha (print 'Último:', lista[TAM]).
  • Outra informação muito importante na saída apresentada acima é o tipo de erro que ocorreu: IndexError: list index out of range (adaptado em pt-br: Erro de índice: índice fora da faixa da lista). O IndexError significa que estamos tentando acessar alguma posição da lista que vai além do tamanho dela. Por exemplo, em uma lista de dez elementos, você não pode acessar o décimo-primeiro elemento, pois ele não existe. Se você tentar, vai receber um IndexError na cabeça.

Agora você já tem alguma informação sobre o que pode estar acontecendo: é na última linha e é algum problema relacionado a acesso a posições inexistentes da lista.

Vamos, antes de mais nada lembrar rapidinho como se dá a indexação de listas (e outras sequências) em Python. Veja a lista abaixo, e seus respectivos índices:

lis
+-----+-----+-----+-----+-----+
|  2  |  9  |  6  |  8  |  7  |
+-----+-----+-----+-----+-----+
   0     1     2     3     4

Você já sabe que o índice para acesso ao primeiro elemento da lista lis é 0. Para imprimir o primeiro elemento, você faria print lis[0]. Sabendo que o primeiro índice é 0, fica óbvio que o último não é o tamanho da lista, mas sim o tamanho da lista – 1. Ou seja, em uma lista de 5 elementos (como lis), para imprimir o último elemento, você deve utilizar print lis[4] ao invés de print lis[5]. Agora reveja o traceback e o código-fonte do programa e descubra o erro. 😛

Mais um exemplo

Agora você vai fazer um programa para montar e imprimir URLs de acordo com os parâmetros de protocol, host e path. Veja abaixo o exemplo:

def URL(protocol, host, path):
    return protocol + "://" + host + "/" + path 

def print_urls():
    print URL('http', 'www.example.com', 'path/to/file.html')
    print URL('ftp', 'www.example.com', 'path/to/file.html')
    print URL('http', 'www.example.com', 2)

print_urls()

Porém, pra variar, ao executar, lá vem o maldito traceback (apesar de odiá-lo, acredite, seria muito pior sem ele).

http://www.example.com/path/to/file.html
ftp://www.example.com/path/to/file.html
Traceback (most recent call last):
File "/home/user/src/erro3.py", line 11, in <module>
  print_urls()
File "/home/user/src/erro3.py", line 8, in print_urls
  print URL('http', 'www.example.com', 2)
File "/home/user/src/erro3.py", line 2, in URL
  return protocol + "://" + host + "/" + path 
TypeError: cannot concatenate 'str' and 'int' objects

E agora, o que diabos está acontecendo? Primeiramente, você deve tentar entender a saída do programa. Veja que as duas primeiras linhas indicam que as duas primeiras tentativas de impressão de URLs ocorreram com sucesso. Então, mesmo sem olhar o traceback, você já imagina que algo ocorreu de errado na terceira URL (que deveria mostrar http://www.example.com/2). Analise o traceback, seguindo o procedimento que vimos anteriormente (encontrar onde pode estar o erro e então identificá-lo, para depois corrigí-lo).

A antepenúltima linha do traceback diz que o erro está na linha 2, na função URL (File "/home/user/src/erro3.py", line 2, in URL). A linha 2 é return protocol + "://" + host + "/" + path. Mas peraí, se você olhar o traceback de cima para baixo, verá que ele aponta várias linhas como fontes do erro: 11, 8 e 2. Observe que ele mostra o rastro (trace) de chamadas de funções que levaram ao código com defeito. Como o próprio traceback nos mostra, ocorreu a seguinte chamada de funções que levou ao erro apresentado acima:

print_urls()  -->  print URL('http', 'www.example.com', 2)  -->  return protocol + "://" + host + "/" + path 
    11                                 8                                                2

Talvez você não esteja conseguindo descobrir o erro. Então, vamos analisar mais uma informação do traceback: o tipo do erro. De acordo com o que foi apresentado na última linha do erro, temos um erro do tipo TypeError (erro de tipagem), com a mensagem de erro: cannot concatenate 'str' and 'int' objects (não é possível concatenar objetos dos tipos ‘str’ e ‘int’). Ou seja, estamos em algum lugar passando um número inteiro para ser concatenado às strings que são utilizadas para compor a URL. Agora ficou moleza, pois na linha 8 estamos passando o argumento 2 (um valor do tipo int) para o parâmetro path da função URL() (que é usado nessa função para ser concatenado a valores do tipo str). Para corrigir é fácil, basta substituir o 2 do tipo inteiro pelo '2' do tipo string. A linha 8 ficaria assim:

print URL('http', 'www.example.com', '2')

Mas às vezes os erros não estão tão explícitos assim, e somente analisar o traceback não é o suficiente para descobrirmos o problema de nosso código. Então teremos que partir para outras abordagens, como veremos a seguir.

(Se você quiser conhecer os principais erros e exceções que podem ocorrer em Python, consulte a documentação oficial.)

Muito além do traceback

Muitas vezes é preciso que analisemos a execução do programa em questão. Essa análise envolve descobrirmos os valores que as variáveis estão assumindo e quais caminhos no código o programa está percorrendo durante sua execução. A forma mais comum que existe para fazermos isso e que para muitos é a solução definitiva para o problema, é encher o código de prints, mostrando assim no console os valores das variáveis, para que possamos ver se o programa está se comportando da forma desejada. Existem outras formas também, usando ferramentas específicas, que permitem que o programa seja pausado, executado linha por linha e inspecionado. Tanto o uso dos prints quanto o uso de ferramentas específicas é chamado de depuração de código (debugging, eliminação de bugs).

O print

Quem nunca encheu o código de prints para tentar entender o que está se passando no programa atire a primeira pedra. Acaba que essa é a solução mais natural para o cara que está iniciando. A regra é simples: imprima o valor das variáveis que lhe interessam, principalmente em pontos como entradas de expressões condicionais (if-elif-else); antes, durante e depois da execução de um laço de repetição (for, while); antes e depois da chamada de uma função que modifica valores de nossas variáveis; e em toda situação que ficamos na dúvida sobre o que está acontecendo com nossas variáveis.

Veja um exemplo do uso de prints para depurar um programa:

x = 0
x = faca_algo_com(x)
print 'pre-while: %s' % (x, )
while x > 0:
    x = faca_algo_com(x)
    if x % 2 == 0:
        x += 1
    print 'while: %s' % (x, )
print 'pos-while: %s' % (x, )

No exemplo acima, usamos 3 prints para termos melhor entendimento do que está acontecendo durante a execução do programa. Em programas que já lançam muitos valores na saída padrão, pode ser útil lançar as mensagens de depuração na saída padrão de erros do shell, para que possamos, se preciso for, analisá-las em separado do resto do programa:

import sys
x = 0
x = faca_algo_com(x)
sys.stderr.write('pre-while: %s\n' % (x, ))
while x > 0:
    x = faca_algo_com(x)
    if x % 2 == 0:
        x += 1
    sys.stderr.write('while: %s\n' % (x, ))
sys.stderr.write('pos-while: %s\n' % (x, ))

Mas, tenha cuidado, pois às vezes o excesso de informações vai atrapalhar mais do que ajudar.

Os loggers

Uma alternativa interessante aos prints é o uso de loggers. O log de um programa é, em geral, um objeto no qual registramos informações, para uma possível análise posterior. Em geral, utilizamos o log para registro da ocorrência de eventos durante a execução de um programa. A ideia principal é basicamente a mesma do que usar os prints, mas na prática existem várias vantagens no uso de um logger.

O recurso mais interessante dos loggers é a possibilidade de “categorizar” as informações que vamos jogando na tela, de acordo com o seu grau de importância. Por exemplo, o mecanismo de logging padrão do Python possui 5 níveis: DEBUG, INFO, WARNING (nível padrão), ERROR e CRITICAL. Veja o exemplo:


import logging

logging.debug('depurando ...')
logging.info('informando ...')
logging.warning('alertando ...')
logging.error('assustando ...')
logging.critical('apavorando ...')

Se o programa acima for executado, a saída será:

WARNING:root:alertando ...
ERROR:root:assustando ...
CRITICAL:root:apavorando ...

Perceba que as duas primeiras linhas do logging não foram refletidas na saída do programa. Isso porque o nível padrão do logger é o warning, o que significa que somente mensagens de warning e de categorias mais importantes (no caso, error e critical) serão impressas.

Usando o logger, podemos incluir várias mensagens de depuração no código, e ativar seu aparecimento na saída do programa somente quando tivermos interesse.

Veja mais em: http://docs.python.org/2/library/logging

O debugger

Depuradores são ferramentas feitas para auxiliar o desenvolvedor a descobrir falhas em seu código. Eles permitem que executemos um programa passo-a-passo, que a cada passo inspecionemos as variáveis, que verifiquemos em que seção do código está a execução do programa, além de outros recursos. Para mim, o melhor dos recursos é poder inspecionar os valores aos quais as variáveis referenciam durante a execução passo-a-passo do programa.

O depurador que vamos ver em seguida é o pdb, que é o depurador padrão distribuído com Python. Basta que executemos o depurador, informando a ele qual o programa que queremos depurar. Por exemplo, para depurar um programa chamado prog.py, invocamos o pdb
da seguinte forma (em um shell do sistema operacional):

$ python -m pdb prog.py

Teste você mesmo, baixando o arquivo prog.py. Veja na imagem a seguir um exemplo de execução de um programa usando o pdb:

pdb

Ao iniciar a execução, o depurador para e aguarda por comandos do usuário, que podem ser: execute a atual e pule para a próxima linha, entre dentro da função, mostre o valor de uma variável/expressão, mostre o código fonte, etc.

Alguns comandos úteis do pdb:

  • list: mostra um trecho do código em execução, com destaque para a linha atual;
  • next: comando que indica ao pdb que ele deve seguir para a próxima linha de execução, sem adentrar em funções quando houverem chamadas;
  • step: indica ao pdb para que ele siga em frente, entrando na função se a linha atual for uma chamada de função;
  • pp <nome_de_variável>: mostra na tela o valor referenciado pela variável nome_de_variável.

A seguir, vou apresentar um processo de depuração, para que você entenda melhor do que se trata e como podemos fazê-lo. Vamos testar um programa que faz a multiplicação de dois valores através de sucessivas somas:

def multiplica(x, y):
  result = 0
  while y > 0:
      result = result + x
      y = y - 1
  return result

z = 10
k = 2
r = multiplica(z, k)
print r

Veja o vídeo abaixo, onde demonstro uma pequena sessão de depuração do programa acima:

Como você pode perceber, o mais interessante do uso do depurador é a análise detalhada que é possível que façamos enquanto o programa está em execução. No vídeo, inspecionamos os valores de variáveis (com o comando pp), verificamos o código em execução do programa (o list mostra a linha em execução atualmente, bem como algumas linhas acima e abaixo), passamos de linha em linha (usando o comando next) e até mesmo entramos em funções, para verificar o que é feito dentro delas (com o comando step). Normalmente, é disso que se trata uma sessão de depuração, uma análise do programa em execução, inspeção de valores, verificação de caminhos percorridos pela execução no código do programa. Assim, o programador pode obter boas pistas sobre o problema que está enfrentando.

A seguir, uma tabela contendo os principais comandos do pdb e suas funcionalidades:

Comando Atalho Uso
help h Mostra a ajuda do comando.
break x b Insere um breakpoint na linha x, um ponto de parada, até o qual o programa será executado sem interatividade. A partir dali, a execução fica sob o controle do usuário do depurador.
step s Prossegue a execução até a próxima linha de código, inclusive entrando em funções.
next n Parecido com step, mas executa chamadas a funções como comandos comuns, sem entrar nelas.
pp exp pp Imprime o valor da expressão exp, podendo exp ser uma simples variável ou uma expressão complexa.
quit q Fecha o depurador

Mais comandos em: http://docs.python.org/2/library/pdb.html#debugger-commands

Veja mais sobre o pdb em (em inglês):

Breakpoints

Para que você não precise excutar o programa todo, linha por linha, next por next, desde o começo até o ponto que lhe interesse, você pode usar um breakpoint. Um breakpoint é um ponto de parada, de forma que o programa é executado normalmente (sem interatividade) até o momento em que encontrar o breakpoint. A partir dali, o depurador passa a aguardar pelos comandos do usuário. Breakpoints são muito úteis, pois em geral estamos interessados na depuração de somente um trecho do código, onde desconfiamos que esteja ocorrendo o erro.

winpdb

Se você prefere um ambiente gráfico, vários IDEs fornecem a integração com depurador na sua interface. Basta escolher um IDE e usar a depuração sempre que for preciso.

Mas, se você não quiser usar um IDE só por isso, você pode utilizar o winpdb, que é um depurador gráfico para Python. Em uma janela só, você vê o código, uma marcação indicando a linha em execução, uma tabela contendo todas as informações que estão no escopo local e global, além de suportar avaliação de expressões, e até mesmo, mudanças nos valores das variáveis do programa em tempo de execução.

No vídeo a seguir eu mostro um exemplo de execução de um programa usando o winpdb:

Enfim…

Se você sofre muito para encontrar os problemas existentes no seu código, aprenda a usar um depurador, seja ele gráfico ou em texto, e se acostume a usá-lo. Usar um depurador acaba sendo muito mais eficiente do que encher o código com prints, pois ele permite que paremos a execução em um determinado ponto, que alteremos valores, e que analisemos com mais calma a execução do programa, tendo sempre uma visão melhor do estado atual deste.

Atenção: os códigos apresentados neste post foram elaborados com o único propósito de mostrar uma situação em que erros podem ocorrer. Assim, de forma alguma eles podem ser considerados exemplos de boas práticas.

Armazenando senhas de forma segura

Dica ao leitor: não deixe de ler a seção “O mecanismo correto”.

Diga aí, você anota a senha do seu email em um post-it e deixa ele colado em seu monitor, visível a qualquer um? Aposto que não, pois seria muito fácil para alguém acessar sua conta e mandar emails engraçadinhos para seus contatos. Da mesma forma que você protege sua senha, os aplicativos que utilizam informações de login e senha para autenticar usuários (como o seu serviço de email) também devem cuidar das senhas armazenadas neles, não devendo nunca guardá-las em texto puro. Vamos ver nesse post como funciona a autenticação, e as melhores práticas para implementar esse serviço na sua aplicação de forma razoavelmente segura.

Autenticação

Você já parou para pensar em como funciona a autenticação em um serviço de email? Primeiro, você digita o seu nome de usuário e senha em um formulário web, como o exemplo abaixo:

Usuário: pythonhelp
  Senha: *******

Esses dados são então enviados para o servidor que está fornecendo o serviço de autenticação do seu email. Lá dentro, o serviço de autenticação irá procurar por um usuário chamado pythonhelp no banco de dados de usuários. Se encontrá-lo, irá realizar uma comparação (mais para frente veremos que não é uma simples comparação de duas strings) para verificar se a senha fornecida no formulário web corresponde à senha do usuário. Em caso positivo, a autenticação ocorre com sucesso e você pode então acessar sua conta de email. Em caso negativo, aquela mensagenzinha chata avisando que você errou seu nome de usuário ou sua senha aparece na tela. (A propósito, você já percebeu que a maioria dos serviços não informa se o que erramos foi o nome de usuário ou se foi a senha? Esse tipo de informação é usualmente interessante para um invasor em potencial.)

Deixando de lado alguns detalhes, a autenticação funciona basicamente da forma descrita acima. Antes de vermos como isso tudo poderia ser implementado, veremos como NÃO deve ser implementado um serviço de autenticação.

Como NÃO implementar autenticação

Vamos desenvolver o serviço de autenticação para a nossa aplicação. Para isso, criamos uma tabela no banco de dados chamada USUARIOS, que contém duas informações sobre cada usuário: seu nome de usuário e sua senha. Como você pode ver abaixo, armazenamos a senha dos usuários em texto puro, ou seja, as senhas estão visíveis a qualquer pessoa que obtiver acesso ao banco de dados.

+--------------+
| USUARIOS     |
+--------------+------------------------+
|    NOME      |        SENHA           |
+--------------+------------------------+
| joaozinho    | teste                  |
+--------------+------------------------+
| pedrinho     | teste123               |
+--------------+------------------------+
| maria        | t35t3                  |
+--------------+------------------------+

Para fazer a autenticação, basta que o usuário forneça a sua senha e que comparemos a senha fornecida com a que está armazenada no BD.

Isso até funciona, mas eu é que não forneceria a minha senha para um sistema meia-boca desses que vai armazená-la em texto puro no banco de dados. Sabe por quê? Porque uma vez que alguém obtenha acesso ao banco de dados do sistema, basta isso para quebrar a privacidade de todos os usuários:

user@host:~/$ sqlite usuarios.db
sqlite> select * from usuarios;
joaozinho|teste
pedrinho|teste123
maria|t35t3

You're doing it wrong!

Que coisa, não? As senhas estão expostas. Falha de segurança gravíssima! Sabendo que a maioria dos usuários usa a mesma senha para os logins em vários sites, dá pra ter uma idéia do estrago né?

Lição número 1: JAMAIS ARMAZENE SENHAS EM TEXTO PURO!

Um jeito melhor

Agora que você já sabe como não fazer, vamos ver uma forma um pouquinho melhor (ainda não a correta) de implementar um mecanismo de autenticação.

Dessa vez nós não vamos armazenar as senhas dos usuários no BD. O que vamos armazenar é uma informação relacionada à senha e gerada a partir dela, o chamado hash da senha.

O que é o Hash?

O hash de um valor é o resultado da aplicação de uma função de hashing a tal valor. Esse resultado é, em geral, muito diferente do valor original. Uma função de hashing H recebe como entrada um valor x e retorna como resultado o hash h correspondente àquele valor:

H(x) -> h

Vamos calcular o hash do valor 'teste123':

H('teste123') -> 'aa1bf4646de67fd9086cf6c79007026c'

Vamos agora calcular o hash do valor 'teste12':

H('teste12') -> '0940004e70ce8d82b440d3c1244dfdee'

Vamos calcular novamente o hash do valor 'teste123':

H('teste123') -> 'aa1bf4646de67fd9086cf6c79007026c'

Agora vamos aplicar a função de hash ao valor 'aa1bf4646de67fd9086cf6c79007026c' (que é o hash de 'teste123'):

H(‘aa1bf4646de67fd9086cf6c79007026c’) -> ‘0728a200630cec4b33e33e20646bc54a’

Observando com atenção, você pode notar algumas coisas sobre as funções de hash:

  1. A função de hash gera um resultado cujo valor é muito diferente do valor original.
  2. Quando alteramos levemente o valor de entrada para a função de hash, o valor retornado por ela muda completamente (veja os exemplos de aplicação nas entradas 'teste123' e 'teste12'). Isso é chamado de efeito avalanche.
  3. Quando aplicamos H novamente à entrada 'teste123', obtivemos o valor idêntico ao obtido pela primeira vez. Ou seja, a função de hash é determinística.
  4. O último exemplo nos mostra que uma função de hash não é reversível, isto é, dado o hash de um valor, não conseguiremos descobrir o valor original através desse hash.

A partir das observações acima, podemos inferir algumas propriedades que as funções de hash possuem:

  1. É improvável (muito improvável mesmo) que você modifique a entrada da função sem modificar o resultado dela.
  2. É impossível gerar o valor de entrada a partir do resultado.
  3. É muito difícil (muito mesmo) encontrar dois valores para os quais a função de hashing produza o mesmo resultado.
  4. Sempre que aplicada ao mesmo valor x, uma mesma função H(x) irá retornar o mesmo resultado h.

* Nos exemplos acima, usei o algoritmo de hashing MD5, embora existam vários outros que poderiam ser igualmente usados, como SHA-1, SHA-256, etc.

Autenticação com hash

Conhecendo as propriedades acima, podemos criar um mecanismo de autenticação mais seguro. O objetivo é armazenar as informações de senha de forma mais protegida, em vez de deixá-la exposta em texto puro.

A primeira idéia pode ser simplesmente armazenar apenas o hash da senha. Parece uma boa idéia, afinal, a propriedade 2 diz que se alguém roubar o valor do hash da sua senha, não conseguirá obter a senha propriamente dita, e a propriedade 4 nos possibilita verificar se a senha está correta comparando o hash do que o usuário digitou com o hash armazenado no banco de dados.

Veja abaixo como ficaria o nosso novo BD de usuários. Para cada usuário, armazenamos o hash da senha que ele forneceu no cadastro.

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+
| NOME      |         HASH DA SENHA            |
+-----------+----------------------------------+
| joaozinho | 698dc19d489c4e4db73e28a713eab07b |
+-----------+----------------------------------+
| pedrinho  | aa1bf4646de67fd9086cf6c79007026c |
+-----------+----------------------------------+
| maria     | c7ac7410983dc7efbb2e5c062c515b7d |
+-----------+----------------------------------+

Quando o usuário quiser se autenticar no sistema, teremos em mãos a senha fornecida por ele na tela de login, mas não podemos compará-la diretamente ao valor armazenado no banco, pois o que está armazenado é o hash da senha. O serviço de autenticação deverá aplicar a função de hash sobre a senha fornecida pelo usuário e comparar o resultado com o que está armazenado no BD. Se o valor obtido for idêntico ao hash armazenado no banco de dados para aquele usuário, a autenticação ocorre com sucesso. Caso contrário, erro de autenticação.

Agora as senhas estarão um pouquinho mais seguras. Isso mesmo, somente um pouco, pois existem meios para descobrir o valor da senha original através do hash dela. A próxima seção vai descrever um pouco isso.

Ataques ao banco de dados de senhas (ou hashes delas)

Uma coisa que acontece com frequência é um sistema sofrer uma invasão e os invasores realizarem uma cópia do seu banco de dados. Considere que os invasores roubaram o seguinte banco de dados:

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+
| NOME      |         HASH DA SENHA            |
+-----------+----------------------------------+
| joaozinho | 698dc19d489c4e4db73e28a713eab07b |  <-- hash de "teste"
+-----------+----------------------------------+
| pedrinho  | aa1bf4646de67fd9086cf6c79007026c |  <-- hash de "teste123"
+-----------+----------------------------------+
| maria     | 2a1bdbdad93b1081007abc4b419d8f0b |  <-- hash de "t35t3"
+-----------+----------------------------------+
|    ...    |               ...                |
+-----------+----------------------------------+

O que eles poderiam fazer com esses dados? De acordo com as propriedades que vimos sobre as funções de hashing, não seria possível extrair o valor original que gerou o hash, então você imaginaria que as senhas estariam seguras dessa maneira.

Teoricamente, sim. Mas agora imagine que o invasor (uma pessoa muuuuuuito paciente), de posse do hash da senha do usuário joaozinho (698dc19d489c4e4db73e28a713eab07b), comece a fazer alguns testes com valores comumente usados como senhas:

>>> import hashlib
>>> print hashlib.md5('123').hexdigest()
202cb962ac59075b964b07152d234b70
>>> print hashlib.md5('abcde').hexdigest()
ab56b4d92b40713acc5af89985d4b786
>>> print hashlib.md5('bla').hexdigest()
128ecf542a35ac5270a87dc740918404
>>> print hashlib.md5('teste').hexdigest()
698dc19d489c4e4db73e28a713eab07b

Opa! O invasor acabou de descobrir a senha do joaozinho (teste), pois o hash obtido para essa string é o mesmo hash que está armazenado para o joaozinho no BD. Se ele continuar testando valores que considera prováveis de serem usados como senha, ele pode, eventualmente, acabar fazendo:

>>> print hashlib.md5('teste123').hexdigest()
aa1bf4646de67fd9086cf6c79007026c

Então ele terá descoberto a senha do usuário pedrinho, pois o hash gerado para teste123 possui o mesmo valor que está armazenado para esse usuário. A esse tipo de ataque, chamamos de Ataque por força bruta.

Imagino que você esteja pensando que o invasor terá muito trabalho para descobrir a senha do usuário maria, testando milhões de possibilidades antes de chegar em t35t3. Pois é. Se ele for testando manualmente, dificilmente irá descobrir a senha. Mas imagine agora que o invasor tenha escrito um programa que gere uma tabela gigantesca contendo possíveis senhas e seus hashes:

+----------------------------------+---------------------+
|              HASH                |       SENHA         |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 698dc19d489c4e4db73e28a713eab07b | teste               |
+----------------------------------+---------------------+
| e959088c6049f1104c84c9bde5560a13 | teste1              |
+----------------------------------+---------------------+
| 38851536d87701d2191990e24a7f8d4e | teste2              |
+----------------------------------+---------------------+
| 56c1056afb34f0d5ad809821d417a52b | t3st3               |
+----------------------------------+---------------------+
| 2a1bdbdad93b1081007abc4b419d8f0b | t35t3               |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 128ecf542a35ac5270a87dc740918404 | bla                 |
+----------------------------------+---------------------+
| 14a310c18e7ee2627b3de4ff82b11e76 | bl4                 |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+

Tendo essa tabela pré-calculada, uma vez que o invasor esteja de posse do hash da senha da maria (2a1bdbdad93b1081007abc4b419d8f0b), basta pesquisar por esse hash na tabela de hashes. Se encontrar um registro que possua tal hash, basta obter a senha que o acompanha.

É evidente que essa tabela deve ser gigantesca, gigantesca mesmo, para conter uma boa quantidade de combinações de valores de senhas. Para “facilitar o trabalho”, invasores do mundo todo colaboram na criação e busca em tabelas desse tipo. Essas tabelas são conhecidas como Rainbow Tables.

Não só isso, os próprios usuários colaboram com os invasores, usando as mesmas senhas em vários lugares, ou usando palavras simples de adivinhar, de forma que bem antes duma tabela dessas cobrir todas as possibilidades de hash, ela já pode ser extremamente útil para os invasores.

Lição número 2: O HASH SOZINHO NÃO FAZ MILAGRE!

Evitando as Rainbow Tables

Para lidar com isso, existe uma técnica chamada de salgar senhas (tradução literal do inglês “salting passwords”), que consiste em adicionar um “temperinho” na senha antes de armazenar. A idéia é gerar uma string contendo alguns valores aleatórios e concatenar essa string à senha do usuário na hora gerar o hash. Assim, ao invés de tomar somente a senha do usuário como entrada, a função de hashing passa a tomar como entrada também a string aleatória (chamada de salt, ou sal). Esse sal também é armazenado no BD para que posteriormente seja possível que o serviço de autenticação realize a verificação.

O que isso traz de segurança para o sistema? Vamos visualizar uma tabela que armazena o nome do usuário, o hash da concatenação entre senha e sal, e o sal. A senha de cada usuário é a mesma senha mostrada lá no início do post.

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+-----------+
| NOME      |       HASH DE (SENHA+SAL)        |    SAL    |
+-----------+----------------------------------+-----------+
| joaozinho | e3e923b2c0d3890270a2cb6d52b13bf6 |   h6ja8   |
+-----------+----------------------------------+-----------+
| pedrinho  | 046b8f04069efab205d9f7bcc099e3d3 |   5uoWB   |
+-----------+----------------------------------+-----------+
| maria     | 960e18e4dc393660fdf3caba8634f38e |   jtm7a   |
+-----------+----------------------------------+-----------+

Veja como foram gerados os hashes armazenados:

>>> print hashlib.md5('teste'+'h6ja8').hexdigest()
e3e923b2c0d3890270a2cb6d52b13bf6
>>> print hashlib.md5('teste123'+'5uoWB').hexdigest()
046b8f04069efab205d9f7bcc099e3d3
>>> print hashlib.md5('t35t3'+'jtm7a').hexdigest()
960e18e4dc393660fdf3caba8634f38e

Está achando estranho o fato de termos armazenado o sal em texto puro no BD? Pois é, se o invasor roubar nossa base, ele terá de lambuja o valor do sal. Mas isso não é um problema, pois o objetivo principal de salgarmos a senha é impossibilitar a utilização de rainbow tables, afinal a idéia principal por trás dessas tabelas é o cálculo prévio dos hashes de vários valores. Ao roubar o sal e o hash de uma senha, o invasor teria que recalcular toda a rainbow table para poder descobrir a senha do usuário. E essa tarefa é muito custosa.

Assim, se cada usuário possuir um sal diferente, a rainbow table terá que ser recalculada para cada usuário, tornando essa atividade praticamente impossível.

Considere que o invasor possui a rainbow table abaixo, bem como o hash (e3e923b2c0d3890270a2cb6d52b13bf6) e o sal (h6ja8) do usuário joaozinho (roubados da base de dados de usuários).

+----------------------------------+---------------------+
|              HASH                |       SENHA         |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 698dc19d489c4e4db73e28a713eab07b | teste               |
+----------------------------------+---------------------+
| e959088c6049f1104c84c9bde5560a13 | teste1              |
+----------------------------------+---------------------+
| 38851536d87701d2191990e24a7f8d4e | teste2              |
+----------------------------------+---------------------+
| 507eb04c9c427e9f961e47a7204fac41 | teste3              |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 128ecf542a35ac5270a87dc740918404 | bla                 |
+----------------------------------+---------------------+
| df5ea29924d39c3be8785734f13169c6 | blabla              |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+

Para obter a senha do usuário, o invasor terá que recalcular toda a tabela. Por exemplo, terá que calcular o hash de “testeh6ja8”, de “teste1h6ja8”, e assim por diante, até encontrar o hash correspondente. Considerando que ela pode ter zilhões de registros, essa é uma tarefa bastante demorada. Imagine recalcular toda a tabela para todas as possíveis combinações de valores no sal.

Perceba que o sal que utilizamos é bastante curto. Quanto mais longo for o sal, mais protegidas contra rainbow tables as senhas estarão, pois o número de combinações possíveis de valores para o sal aumentam exponencialmente.

Dessa forma, o sal derruba a maior força das rainbow tables, que é a busca rápida por elementos (que ocorre graças ao cálculo prévio dos valores). Ao usar valores de sal suficientemente longos, estamos aumentando MUITO (MUITO MESMO) a quantidade de entradas que uma rainbow table deve ter para servir como uma base de hashes pré-calculados (segundo a Wikipedia, hoje em dia são usados salts de até 128 bits). Se, além disso, usarmos um sal diferente para cada usuário, então tornamos impossível o uso de tabelas pré-computadas para a descoberta das senhas dos nossos usuários.

Mas, ainda assim, hashes salgados não são a solução definitiva para armazenamento de senhas. Leia a próxima seção para descobrir o porquê.

O mecanismo correto

Mesmo sendo um mecanismo muito mais seguro do que armazenando a senha em texto puro ou o hash simples, o armazenamento de hashes de senhas salgadas ainda não é a melhor solução. Apesar de amplamente utilizados, os algoritmos de hashing como MD5 e SHA-1 não são recomendados para o armazenamento de senhas, pois são bastante velozes para realizar o hashing de um valor. Um algoritmo rápido torna a geração de ataques via força bruta e rainbow tables mais fácil, pois o tempo necessário para gerar as tabelas acaba sendo pequeno.

Um algoritmo para uso no armazenamento de senhas tem como requisito ser lento. Lento o suficiente para atrapalhar o atacante, mas não o bastante para atrapalhar o usuário.

Uma forma recomendada de armazenar as senhas dos usuários é usando o bcrypt. O BCrypt é um mecanismo criptográfico criado para lidar com senhas. Assim sendo, uma de suas características é ser demorado para geração do hash.

Para ter uma idéia da diferença na velocidade dos dois mecanismos, observe o resultados de uns testes que fiz usando o timeit:

$ python -m timeit -s "import bcrypt; salt = bcrypt.gensalt()" "bcrypt.hashpw('teste', salt)"
10 loops, best of 3: 243 msec per loop

$ python -m timeit -s "import hashlib" "hashlib.md5('teste')"
1000000 loops, best of 3: 0.409 usec per loop

Enquanto o MD5 gera os hashes em uma média de 0.409 microsegundos por hash, o bcrypt leva em média 243 milisegundos por hash.

Instalando o bcrypt

A implementação Python do bcrypt não está disponível com a biblioteca-padrão, portanto é necessário instalá-la através do gerenciador de pacotes pip:

sudo pip install py-bcrypt

Obs.: o módulo é escrito em linguagem C, portanto o código será compilado. Para isso, é necessário possuir instalados (em sistema Ubuntu): build-essential e python-dev.

Alternativamente, num sistema Debian ou Ubuntu você pode instalar o pacote no repositório do apt:

sudo apt-get install python-bcrypt

Usando o bcrypt

Assim como a maioria dos módulos Python, o bcrypt é facinho de usar. Vamos fazer alguns exemplos.

Gerando o hash de uma senha

>>> import bcrypt
>>> print bcrypt.hashpw('teste123', bcrypt.gensalt())
$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu

Execute o código acima no seu interpretador Python local e perceba a diferença no tempo de resposta entre o bcrypt e o MD5.

Verificando a senha do usuário

Para verificar se determinado hash é correto, devemos passá-lo como argumento para a função hashpw, juntamente com a senha:

>>> print bcrypt.hashpw("teste123", "$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu")
$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu

Se ela retornar o mesmo hash que foi passado como argumento, é porque o hash corresponde à senha passada como primeiro argumento.

Podemos então criar uma função de validação de senha de usuário:

def valida_senha(senha_digitada, hash_senha):
    return bcrypt.hashpw(senha_digitada, hash_senha) == hash_senha

Um sistema de autenticação usando BD

Vamos agora implementar um mecanismo para autenticação de usuários, usando sqlite3 e bcrypt.

# -*- encoding:utf-8 -*-
import bcrypt
import sqlite3

def valida_senha(senha_digitada, hash_senha):
    return bcrypt.hashpw(senha_digitada, hash_senha) == hash_senha

def insere_usuario(conexao, usuario, senha):
    hash_senha = bcrypt.hashpw(senha, bcrypt.gensalt())
    conexao.execute('insert into USUARIOS values ("%s", "%s")' % (usuario, hash_senha))
    conexao.commit()

def usuario_autenticado(conexao, usuario, senha):
    cursor = conexao.execute('select SENHA from USUARIOS where NOME = "%s"' % (usuario,))
    dados = cursor.fetchone()
    hash_senha = str(dados[0])
    return valida_senha(senha, hash_senha)

# alguns testes
if __name__=='__main__':
    conexao = sqlite3.connect('arquivo.db')
    insere_usuario(conexao, 'maria', 'teste')
    insere_usuario(conexao, 'joao', 'teste123')
    if usuario_autenticado(conexao, 'joao', 'teste123'):
        print 'joao está autenticado!'
    else:
        print 'Xiiiii...'

 

Antes de terminar …

O post mostrou quão simples é a implementação de um mecanismo seguro para armazenamento das senhas dos usuários em um banco de dados. Agora você não tem mais desculpas para implementar a autenticação usando senhas armazenadas em texto puro.

Se você armazena as senhas em texto puro, é fácil alterar seus algoritmos de autenticação e gerar os hashes das senhas existentes (faça backup antes, claro :)). Vale a pena gastar um tempinho corrigindo isso em seu sistema.

Finalizando, o py-bcrypt é tão simples de ser utilizado que me deixa à vontade para deixar como lição final:

LIÇÃO FINAL: USE O BCRYPT PARA ARMAZENAMENTO DE SENHAS DE USUÁRIOS!

LIÇÃO DEFINITIVA: NÃO SEJA RELAPSO COM AS INFORMAÇÕES DOS SEUS USUÁRIOS!

Leia mais sobre o assunto

Em inglês:

P.S.: obrigado ao eliasdorneles por ter revisado esse post. 🙂