2015-11-13

Roteiro

Introdução a web scraping com Scrapy

Conceitos do Scrapy

Hands-on: crawler para versões diferentes dum site de citações

Rodando no Scrapy Cloud

O tutorial é 90% Scrapy e 10% Scrapy Cloud.

Nota: Scrapy Cloud é o serviço PaaS da Scrapinghub, a empresa em que
trabalho e que é responsável pelo desenvolvimento do Scrapy.

Precisa de ajuda?

Pergunte no Stackoverflow em Português usando a tag
scrapy ou pergunte em inglês no
Stackoverflow em inglês ou na lista de
e-mail scrapy-users.

Introdução a web scraping com Scrapy

O que é Scrapy?

Scrapy é um framework para crawlear web sites e extrair dados estruturados que
podem ser usados para uma gama de aplicações úteis (data mining, arquivamento,
etc).

Scraping:

extrair dados do conteúdo da página

Crawling:

seguir links de uma página a outra

Se você já fez extração de dados de páginas Web antes em Python, são grandes as
chances de você ter usado algo como requests + beautifulsoup. Essas tecnologias
ajudam a fazer scraping.

A grande vantagem de usar Scrapy é que tem suporte de primeira classe a
crawling.

Por exemplo, ele permite configurar o tradeoff de politeness vs speed (sem
precisar escrever código pra isso) e já vem com uma configuração útil de
fábrica para crawling habilitada: suporte a cookies, redirecionamento tanto via
HTTP header quanto via tag HTML meta, tenta de novo requisições que falham,
evita requisições duplicadas, etc.

Além disso, o framework é altamente extensível, permite seguir combinando
componentes e crescer um projeto de maneira gerenciável.

Instalando o Scrapy

Recomendamos usar virtualenv, e instalar o Scrapy com:

A dependência chatinha é normalmente o lxml (que precisa de
algumas bibliotecas C instaladas). Caso tenha dificuldade, consulte as
instruções específicas por
plataforma
ou peça ajuda nos canais citados acima.

Para verificar se o Scrapy está instalado corretamente, rode o comando:

A saída que obtenho rodando este comando no meu computador é:

Rodando um spider

Para ter uma noção inicial de como usar o Scrapy, vamos começar rodando um
spider de exemplo.

Crie um arquivo youtube_spider.py com o seguinte conteúdo:

Agora, rode o spider com o comando:

O scrapy vai procurar um spider no arquivo youtube_spider.py e
escrever os dados no arquivo CSV portadosfundos.csv.

Caso tudo deu certo, você verá o log da página sendo baixada, os dados sendo
extraídos, e umas estatísticas resumindo o processo no final, algo como:

Ao final, verifique os resultados abrindo o arquivo CSV no seu editor de
planilhas favorito.

Se você quiser os dados em JSON, basta mudar a extensão do arquivo de saída:

Outro formato interessante que o Scrapy suporta é JSON lines:

Esse formato usa um item JSON em cada linha -- isso é muito útil para arquivos
grandes, porque fica fácil de concatenar dois arquivos ou acrescentar novas
entradas a um arquivo já existente.

Conceitos do Scrapy

Spiders

Conceito central no Scrapy,
spiders são classes que
herdam de
scrapy.Spider,
definindo de alguma maneira as requisições iniciais do crawl e como proceder
para tratar os resultados dessas requisições.

Um exemplo simples de spider é:

Se você rodar o spider acima com o comando scrapy runspider, deverá ver no
log as mensagens:

Como iniciar um crawl a partir de uma lista de URLs é uma tarefa comum,
o Scrapy permite você usar o atribute de classe start_urls em vez de
definir o método start_requests() a cada vez:

Callbacks e próximas requisições

Repare o método parse(), ele recebe um objeto response que representa uma
resposta HTTP, é o que chamamos de um callback. Os métodos callbacks no
Scrapy são
generators
(ou retornam uma lista ou iterável) de objetos que podem ser:

dados extraídos (dicionários Python ou objetos que herdam de scrapy.Item)

requisições a serem feitas a seguir (objetos scrapy.Request)

O motor do Scrapy itera sobre os objetos resultantes dos callbacks e os
encaminha para o pipeline de dados ou para a fila de próximas requisições a
serem feitas.

Exemplo:

Antes de rodar o código acima, experimente ler o código e prever
o que ele vai fazer. Depois, rode e verifique se ele fez mesmo
o que você esperava.

Você deverá ver no log algo como:

Settings

Outro conceito importante do Scrapy são as settings (isto é, configurações).
As settings oferecem uma maneira de configurar componentes do Scrapy, podendo
ser setadas de várias maneiras, tanto via linha de comando, variáveis de ambiente
em um arquivo settings.py no caso de você estar usando um projeto Scrapy ou ainda
diretamente no spider definindo um atributo de classe custom_settings.

Exemplo setando no código do spider um delay de 1.5 segundos entre cada
requisição:

Para setar uma setting diretamente na linha de comando com scrapy runspider,
use opção -s:

Uma setting útil durante o desenvolvimento é a HTTPCACHE_ENABLED, que
habilita uma cache das requisições HTTP -- útil para evitar baixar as
mesmas páginas várias vezes enquanto você refina o código de extração.

Dica: na versão atual do Scrapy, a cache por padrão só funciona caso você
esteja dentro de um projeto, que é onde ele coloca um diretório
.scrapy/httpcache para os arquivos de cache. Caso você queira usar a cache
rodando o spider com scrapy runspider, você pode usar um truque "enganar" o
Scrapy criando um arquivo vazio com o nome scrapy.cfg no diretório atual, e
o Scrapy criará a estrutura de diretórios .scrapy/httpcache no diretório
atual.

Bem, por ora você já deve estar familiarizado com os conceitos importantes do
Scrapy, está na hora de partir para exemplos mais realistas.

Hands-on: crawler para versões diferentes dum site de citações

Vamos agora criar um crawler para um site de frases e citações, feito
para esse tutorial e disponível em: http://spidyquotes.herokuapp.com

Nota: O código-fonte do site está disponível em:
https://github.com/eliasdorneles/spidyquotes

Descrição dos objetivos:

O site contém uma lista de citações com autor e tags, paginadas com 10 citações
por páginas. Queremos obter todas as citações, juntamente com os respectivos
autores e lista de tags.

Existem 4 variações do site, com o mesmo conteúdo mas usando markup HTML diferente.

Versão com markup HTML semântico: http://spidyquotes.herokuapp.com/

Versão com leiaute em tabelas: http://spidyquotes.herokuapp.com/tableful/

Versão com os dados dentro do código Javascript: http://spidyquotes.herokuapp.com/js/

Versão com AJAX e scroll infinito: http://spidyquotes.herokuapp.com/scroll

Para ver as diferenças entre cada versão do site, acione a opção "Exibir
código-fonte" (Ctrl-U) do menu de contexto do seu
browser.

Nota: cuidado com a opção "Inspecionar elemento" do browser para inspecionar
a estrutura do markup. Diferentemente do resultado da opção "Exibir
código-fonte" Usando essa ferramenta, o código que você vê representa as
estruturas que o browser cria para a página, e nem sempre mapeiam diretamente
ao código HTML que veio na requisição HTTP (que é o que você obtém quando usa
o Scrapy), especialmente se a página estiver usando Javascript ou AJAX. Outro
exemplo é o elemento <tbody> que é adicionado automaticamente pelos
browsers em todas as tabelas, mesmo quando não declarado no markup.

Spider para a versão com HTML semântico

Para explorar a página (e a API de scraping do Scrapy), você pode usar
o comando scrapy shell URL:

Esse comando abre um shell Python (ou IPython, se você o
tiver instalado no mesmo virtualenv) com o objeto response, o mesmo que você
obteria num método callback. Recomendo usar o IPython porque fica mais fácil
de explorar as APIs sem precisar ter que abrir a documentação a cada vez.

Exemplo de exploração com o shell:

Com o resultado da exploração inicial acima, podemos começar escrevendo um
spider assim, num arquivo quote_spider.py:

Se você rodar esse spider com:

Você obterá os dados das citações da primeira página no arquivo quotes.csv.
Só está faltando agora seguir o link para a próxima página, o que você também
pode descobrir com mais alguma exploração no shell:

Juntando isso com o spider, ficamos com:

Agora, se você rodar esse spider novamente com:

Perceberá que ainda assim ele vai extrair apenas os items da primeira página, e a segunda página
vai falhar com um código HTTP 429, com a seguinte mensagem no log:



O status HTTP 429 é usado para indicar que o servidor está recebendo muitas
requisições do mesmo cliente num curto período de tempo.

No caso do nosso site, podemos simular o problema no próprio browser se
apertarmos o botão atualizar várias vezes no mesmo segundo:



Neste caso, a mensagem no próprio site já nos diz o problema e a solução: o máximo de
requisições permitido é uma a cada segundo, então podemos resolver o problema setando
a configuração DOWNLOAD_DELAY para 1.5, deixando uma margem decente para podermos
fazer crawling sabendo que estamos respeitando a política.

Como esta é uma necessidade comum para alguns sites, o Scrapy também permite
você configurar este comportamento diretamente no spider, setando o atributo de
classe download_delay:

Usando extruct para microdata

Se você é um leitor perspicaz, deve ter notado que o markup HTML tem umas
marcações adicionais ao HTML normal, usando atributos itemprop e itemtype.
Trata-se de um mecanismo chamado
Microdata, especificado pela
W3C e feito justamente para facilitar a
tarefa de extração automatizada. Vários sites suportam este tipo de marcação,
alguns exemplos famosos são Yelp, The
Guardian, LeMonde, etc.

Quando um site tem esse tipo de marcação para o conteúdo que você está
interessado, você pode usar o extrator de microdata da biblioteca
extruct.

Instale a biblioteca extruct com:

Veja como fica o código usando a lib:

Usando microdata você reduz sobremaneira os problemas de mudanças de leiaute,
pois o desenvolvedor do site ao colocar o markup microdata se compromete a
mantê-lo atualizado.

Lidando com leiaute de tabelas:

Agora, vamos extrair os mesmos dados mas para um markup faltando bom-gosto:
http://spidyquotes.herokuapp.com/tableful/

Para lidar com esse tipo de coisa, a dica é: aprenda XPath, vale a pena!

Comece aqui: http://www.slideshare.net/scrapinghub/xpath-for-web-scraping

O domínio de XPath diferencia os gurus dos gafanhotos. -- Elias Dorneles, 2014

Como o markup HTML dessa página não uma estrutura boa, em vez de fazer scraping
baseado nas classes CSS ou ids dos elementos, com XPath podemos fazer baseando-se
na estrutura e nos padrões presentes no conteúdo.

Por exemplo, se você abrir o shell para a página
http://spidyquotes.herokuapp.com/tableful, usando a expressão a seguir
retorna os os nós tr (linhas da tabela) que contém os textos das citações,
usando uma condição para pegar apenas linhas que estão imediatamente antes de
linhas cujo texto comece com "Tags: ":

Para extrair os dados, precisamos de alguma exploração:

Note como não tem marcação separando o autor do conteúdo, apenas uma string
"Author:". Então podemos usar o método .re() da classe seletor, que nos
permite usar uma expressão regular:

O código final do spider fica:

Note como o uso de XPath permitiu vincularmos elementos de acordo com o conteúdo
tanto no caso das tags quanto no caso do link para a próxima página.

Lidando com dados dentro do código

Olhando o código-fonte da versão do site: http://spidyquotes.herokuapp.com/js/
vemos que os dados que queremos estão todos num bloco de código Javascript,
dentro de um array estático. E agora?

A dica aqui é usar a lib js2xml para
converter o código Javascript em XML e então usar XPath ou CSS em cima do XML
resultante para extrair os dados que a gente quer.

Instale a biblioteca js2xml com:

Exemplo no shell:

O código final fica:

Fica um pouco obscuro pela transformação de código Javascript em XML, mas a
extração fica mais confiável do que hacks baseados em expressões regulares.

Lidando com AJAX

Agora, vamos para a versão AJAX com scroll infinito: http://spidyquotes.herokuapp.com/scroll/

Se você observar o código-fonte, verá que os dados não estão lá. No fonte só
tem um código Javascript que busca os dados via AJAX, você pode ver isso
olhando a aba Network das ferramentas do browser (no meu caso Chrome, mas
no Firefox é similar).

Nesse caso, precisamos replicar essas requisições com o Scrapy, e tratar
os resultados de acordo com a resposta.

Explorando no shell, vemos que o conteúdo é JSON:

Portanto, podemos simplesmente usar o módulo JSON da biblioteca padrão e ser feliz:

>>> import json
>>> data = json.loads(response.body)
>>> data.keys()
[u'has_next', u'quotes', u'tag', u'page', u'top_ten_tags']
>>> data['has_next']
True
>>> data['quotes'][0]
{u'author': {u'goodreads_link': u'/author/show/12898.Stephen_Chbosky',
u'name': u'Stephen Chbosky'},
u'tags': [u'inspirational', u'love'],
u'text': u'\u201cWe accept the love we think we deserve.\u201d'<span c

Show more