Baixar página de servidor com tolerância a falhas simples

Um dia desses precisei fazer um script Python que baixava uma cacetada de páginas HTML de um servidor, que às vezes respondia com um erro para algumas das requisições. As requisições estavam corretas, as páginas existiam, mas por alguma falha no servidor elas não respondiam no momento exato da execução do script.

A solução foi fazer uma função que tenta carregar a URL, e caso não consiga, espera alguns segundos e tenta de novo:

import time
import urllib2

def get_url(url, retry=3, timeout=3):
    try:
        return urllib2.urlopen(url).read()
    except:
        if retry > 0:
            time.sleep(timeout)
            return get_url(url, retry - 1, timeout)
        else:
            raise

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!