Este tutorial foi apresentado na Python Brasil 2015 em São José dos Campos.
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 é:
2015–11–14 19:58:56 [scrapy] INFO: Scrapy 1.0.3 started (bot: scrapybot)
2015–11–14 19:58:56 [scrapy] INFO: Optional features available: ssl, http11
2015–11–14 19:58:56 [scrapy] INFO: Overridden settings: {}
Scrapy 1.0.3
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:
import scrapy
def first(sel, xpath):
return sel.xpath(xpath).extract_first()
class YoutubeChannelLister(scrapy.Spider):
name = ‘channel-lister’
youtube_channel = ‘portadosfundos’
start_urls = [‘https://www.youtube.com/user/%s/videos’ % youtube_channel]
def parse(self, response):
for sel in response.css(“ul#channels-browse-content-grid > li”):
yield {
‘link’: response.urljoin(first(sel, ‘.//h3/a/@href’)),
‘title’: first(sel, ‘.//h3/a/text()’),
‘views’: first(sel, “.//ul/li[1]/text()”),
}
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:
2015–11–14 20:14:21 [scrapy] DEBUG: Crawled (200) <GET https://www.youtube.com/user/portadosfundos/videos> (referer: None)
2015–11–14 20:14:22 [scrapy] DEBUG: Scraped from <200 https://www.youtube.com/user/portadosfundos/videos>
{‘views’: u‘323,218 views’, ‘link’: u‘https://www.youtube.com/watch?v=qSqPkRi-UiE’, ‘title’: u‘GARxc7ONS’}
2015–11–14 20:14:22 [scrapy] DEBUG: Scraped from <200 https://www.youtube.com/user/portadosfundos/videos>
{‘views’: u‘1,295,054 views’, ‘link’: u‘https://www.youtube.com/watch?v=yXc8KCxyEyQ’, ‘title’: u‘SUCESSO’}
2015–11–14 20:14:22 [scrapy] DEBUG: Scraped from <200 https://www.youtube.com/user/portadosfundos/videos>
{‘views’: u‘1,324,448 views’, ‘link’: u‘https://www.youtube.com/watch?v=k9CbDcOT1e8’, ‘title’: u‘BIBLIOTECA’}
…
{‘downloader/request_bytes’: 239,
‘downloader/request_count’: 1,
‘downloader/request_method_count/GET’: 1,
‘downloader/response_bytes’: 27176,
‘downloader/response_count’: 1,
‘downloader/response_status_count/200’: 1,
‘item_scraped_count’: 30,
…
2015–11–14 20:14:22 [scrapy] INFO: Spider closed (finished)
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 é:
import scrapy
class SpiderSimples(scrapy.Spider):
name = ‘meuspider’
def start_requests(self):
return [scrapy.Request(‘http://example.com’)]
def parse(self, response):
self.log(‘Visitei o site: %s‘ % response.url)
Se você rodar o spider acima com o comando scrapy runspider, deverá ver no
log as mensagens:
2015–11–14 21:11:13 [meuspider] DEBUG: Visitei o site: http://example.com
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:
import scrapy
class SpiderSimples(scrapy.Spider):
name = ‘meuspider’
start_urls = [‘http://example.com’]
def parse(self, response):
self.log(‘Visitei o site: %s‘ % response.url)
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:
import scrapy
class SpiderSimples(scrapy.Spider):
name = ‘meuspider’
start_urls = [‘http://example.com’]
def parse(self, response):
self.log(‘Visitei o site: %s‘ % response.url)
yield {‘url’: response.url, ‘tamanho’: len(response.body)}
proxima_url = ‘http://www.google.com.br’
self.log(‘Agora vou para: %s‘ % proxima_url)
yield scrapy.Request(proxima_url, self.handle_google)
def handle_google(self, response):
self.log(‘Visitei o google via URL: %s‘ % response.url)
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:
2015–11–14 21:32:53 [meuspider] DEBUG: Visitei o site: http://example.com
2015–11–14 21:32:53 [scrapy] DEBUG: Scraped from <200 http://example.com>
{‘url’: ‘http://example.com’, ‘tamanho’: 1270}
2015–11–14 21:32:53 [meuspider] DEBUG: Agora vou para: http://www.google.com.br
2015–11–14 21:32:53 [scrapy] DEBUG: Crawled (200) <GET http://www.google.com.br> (referer: http://example.com)
2015–11–14 21:32:54 [meuspider] DEBUG: Visitei o google via URL: http://www.google.com.br
2015–11–14 21:32:54 [scrapy] INFO: Closing spider (finished)
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:
name = ‘meuspider’custom_settings = {
‘DOWNLOAD_DELAY’: 1.5,
}
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:
>>> # vamos pegar o primeiro dele, e ver como extrair o texto:
>>> quote = response.css(‘.quote’)[0]
>>> quote
<Selector xpath=u”descendant-or-self::*[@class and contains(concat(‘ ‘, normalize-space(@class), ‘ ‘), ‘ quote ‘)]” data=u'<div class=“quote” itemscope itemtype=“h’>
>>> print quote.extract()
<div class=”quote” itemscope itemtype=“http://schema.org/CreativeWork”>
<span itemprop=“text”>“We accept the love we think we deserve.”</span>
<small itemprop=“author”>Stephen Chbosky</small>
<div class=“tags”>
Tags:
<meta itemprop=“keywords” content=“inspirational,love”><a href=“/tag/inspirational/page/1/”>inspirational</a>
<a href=“/tag/love/page/1/”>love</a>
</div>
</div>
>>> print quote.css(‘span’).extract_first()
<span itemprop=“text”>“We accept the love we think we deserve.”</span>
>>> print quote.css(‘span::text’).extract_first() # texto
“We accept the love we think we deserve.”
>>> quote.css(‘small::text’).extract_first() # autor
u’Stephen Chbosky’
>>>
>>> # para a lista de tags, usamos .extract() em vez de .extract_first()
>>> quote.css(‘.tags a::text’).extract()
[u’inspirational’, u’love’]
>>>
Com o resultado da exploração inicial acima, podemos começar escrevendo um
spider assim, num arquivo quote_spider.py:
import scrapy
class QuotesSpider(scrapy.Spider):
name = ‘quotes’
start_urls = [
‘http://spidyquotes.herokuapp.com/’
]
def parse(self, response):
for quote in response.css(‘.quote’):
yield {
‘texto’: quote.css(‘span::text’).extract_first(),
‘autor’: quote.css(‘small::text’).extract_first(),
‘tags’: quote.css(‘.tags a::text’).extract(),
}
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:
[<Selector xpath=u”descendant-or-self::li[@class and contains(concat(‘ ‘, normalize-space(@class), ‘ ‘), ‘ next ‘)]” data=u'<li class=”next”>n <a hre’>]
>>> response.css(‘li.next a’)
[<Selector xpath=u”descendant-or-self::li[@class and contains(concat(‘ ‘, normalize-space(@class), ‘ ‘), ‘ next ‘)]/descendant-or-self::*/a” data=u'<a href=”/page/2/”>Next <span aria-hidde’>]
>>> response.css(‘li.next a::attr(“href”)’).extract_first()
u’/page/2/’
>>> # o link é relativo, temos que joinear com a URL da resposta:
>>> response.urljoin(response.css(‘li.next a::attr(“href”)’).extract_first())
u’http://spidyquotes.herokuapp.com/page/2/’
Juntando isso com o spider, ficamos com:
import scrapy
class QuotesSpider(scrapy.Spider):
name = ‘quotes’
start_urls = [
‘http://spidyquotes.herokuapp.com/’
]
def parse(self, response):
for quote in response.css(‘.quote’):
yield {
‘texto’: quote.css(‘span::text’).extract_first(),
‘autor’: quote.css(‘small::text’).extract_first(),
‘tags’: quote.css(‘.tags a::text’).extract(),
}
link_next = response.css(‘li.next a::attr(“href”)’).extract_first()
if link_next:
yield scrapy.Request(response.urljoin(link_next))
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:
2015–11–15 00:06:15 [scrapy] DEBUG: Ignoring response <429 http://spidyquotes.herokuapp.com/page/2/>: HTTP status code is not handled or not allowed
2015–11–15 00:06:15 [scrapy] INFO: Closing spider (finished)
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:
import scrapy
class QuotesSpider(scrapy.Spider):
name = ‘quotes’
start_urls = [
‘http://spidyquotes.herokuapp.com/’
]
download_delay = 1.5
def parse(self, response):
for quote in response.css(‘.quote’):
yield {
‘texto’: quote.css(‘span::text’).extract_first(),
‘autor’: quote.css(‘small::text’).extract_first(),
‘tags’: quote.css(‘.tags a::text’).extract(),
}
link_next = response.css(‘li.next a::attr(“href”)’).extract_first()
if link_next:
yield scrapy.Request(response.urljoin(link_next))
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:
from extruct.w3cmicrodata import LxmlMicrodataExtractorclass QuotesSpider(scrapy.Spider):
name = “quotes-microdata”
start_urls = [‘http://spidyquotes.herokuapp.com/’]
download_delay = 1.5
def parse(self, response):
extractor = LxmlMicrodataExtractor()
items = extractor.extract(response.body_as_unicode(), response.url)[‘items’]
for it in items:
yield it[‘properties’]
link_next = response.css(‘li.next a::attr(“href”)’).extract_first()
if link_next:
yield scrapy.Request(response.urljoin(link_next))
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: “:
[<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>,
<Selector xpath=’//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’ data=u'<tr style=”border-bottom: 0px; “>n ‘>]
Para extrair os dados, precisamos de alguma exploração:
>>> print quote.extract()
<tr style=“border-bottom: 0px; “>
<td style=“padding-top: 2em;”>“We accept the love we think we deserve.” Author: Stephen Chbosky</td>
</tr>
>>> quote.xpath(‘string(.)’).extract_first()
u’n u201cWe accept the love we think we deserve.u201d Author: Stephen Chboskyn ‘
>>> quote.xpath(‘normalize-space(.)’).extract_first()
u’u201cWe accept the love we think we deserve.u201d Author: Stephen Chbosky’
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:
>>> text
u’u201cWe accept the love we think we deserve.u201d’
>>> author
u’Stephen Chbosky’
O código final do spider fica:
import scrapy
class QuotesSpider(scrapy.Spider):
name = ‘quotes-tableful’
start_urls = [‘http://spidyquotes.herokuapp.com/tableful’]
download_delay = 1.5
def parse(self, response):
quotes_xpath = ‘//tr[./following-sibling::tr[1]/td[starts-with(., “Tags:”)]]’
for quote in response.xpath(quotes_xpath):
texto, autor = quote.xpath(‘normalize-space(.)’).re(‘(.+) Author: (.+)’)
tags = quote.xpath(‘./following-sibling::tr[1]//a/text()’).extract()
yield dict(texto=texto, autor=autor, tags=tags)
link_next = response.xpath(‘//a[contains(., “Next”)]/@href’).extract_first()
if link_next:
yield scrapy.Request(response.urljoin(link_next))
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:
scrapy shell http://spidyquotes.herokuapp.com/js/
>>> import js2xml
>>> script = response.xpath(‘//script[contains(., “var data =”)]/text()’).extract_first()
>>> sel = scrapy.Selector(_root=js2xml.parse(script))
>>> sel.xpath(‘//var[@name=”data”]/array/object’)
[<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>,
<Selector xpath=‘//var[@name=”data”]/array/object’ data=u‘<object><property name=”author”><object>’>]
>>> quote = sel.xpath(‘//var[@name=”data”]/array/object’)[0]
>>> quote.xpath(‘string(./property[@name=”text”])’).extract_first()
u‘u201cWe accept the love we think we deserve.u201d‘
>>> quote.xpath(‘string(./property[@name=”author”]//property[@name=”name”])’).extract_first()
u‘Stephen Chbosky’
>>> quote.xpath(‘./property[@name=”tags”]//string/text()’).extract()
[u‘inspirational’, u‘love’]
O código final fica:
import js2xmlclass QuotesSpider(scrapy.Spider):
name = ‘quotes-js’
start_urls = [‘http://spidyquotes.herokuapp.com/js/’]
download_delay = 1.5
def parse(self, response):
script = response.xpath(‘//script[contains(., “var data =”)]/text()’).extract_first()
sel = scrapy.Selector(_root=js2xml.parse(script))
for quote in sel.xpath(‘//var[@name=”data”]/array/object’):
yield {
‘texto’: quote.xpath(‘string(./property[@name=”text”])’).extract_first(),
‘autor’: quote.xpath(
‘string(./property[@name=”author”]//property[@name=”name”])’
).extract_first(),
‘tags’: quote.xpath(‘./property[@name=”tags”]//string/text()’).extract(),
}
link_next = response.css(‘li.next a::attr(“href”)’).extract_first()
if link_next:
yield scrapy.Request(response.urljoin(link_next))
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:
scrapy shell http://spidyquotes.herokuapp.com/api/quotes?page=1
>>> response.headers
{‘Content-Type’: ‘application/json’,
‘Date’: ‘Sun, 15 Nov 2015 22:18:29 GMT’,
‘Server’: ‘gunicorn/19.3.0’,
‘Via’: ‘1.1 vegur’}
Portanto, podemos simplesmente usar o módulo JSON da biblioteca padrão e ser feliz:
>>> 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‘}
>>> data[‘page’]
1
Código final do spider fica:
import jsonclass QuotesSpider(scrapy.Spider):
name = ‘quotes-scroll’
quotes_base_url = ‘http://spidyquotes.herokuapp.com/api/quotes?page=%s‘
start_urls = [quotes_base_url % 1]
download_delay = 1.5
def parse(self, response):
data = json.loads(response.body)
for d in data.get(‘quotes’, []):
yield {
‘texto’: d[‘text’],
‘autor’: d[‘author’][‘name’],
‘tags’: d[‘tags’],
}
if data[‘has_next’]:
next_page = data[‘page’] + 1
yield scrapy.Request(self.quotes_base_url % next_page)
Ao lidar com requisições desse tipo, uma ferramenta útil que pode ser o
minreq, instale com: pip install
minreq.
O minreq tenta encontrar a requisição mínima necessária para replicar
uma requisição do browser, e pode opcionalmente mostrar como montar
um objeto scrapy.Request equivalente.
Rode o minreq com:
Ele fica esperando você colar uma requisição no formato cURL. Para isto,
encontre a requisição AJAX que você quer replicar na aba Network do browser, e
use o recurso “Copy as cURL”:
Cole no prompt do minreq, e espere ele fazer a mágica. =)
Nota: O minreq está em estágio pre-alpha, você provavelmente vai
encontrar bugs — por favor reporte no GitHub.
Rodando no Scrapy Cloud
O Scrapy Cloud é a plataforma PaaS para
rodar crawlers na nuvem, o que permite evitar uma série de preocupações com
infraestrutura.
Funciona como um “Heroku para crawlers”, você faz deploy do seu projeto Scrapy
e configura jobs para rodar spiders periodicamente. Você pode também
configurar scripts Python para rodar periodicamente, os quais podem gerenciar o
escalonamento dos spiders.
Comece criando uma conta free forever em: http://try.scrapinghub.com/free
Criação do projeto
Até aqui nossos exemplos foram simplesmente rodando spiders com scrapy runspider.
Para fazer o deploy, chegou a hora de criar um projeto Scrapy propriamente dito.
Para criar um projeto, basta rodar o comando scrapy startproject passando o nome do projeto:
Feito isso, entre no diretório do projeto com cd quotes_crawler e copie os
arquivos com spiders para dentro do diretório quotes_crawler/spiders.
Certifique-se de usar um nome único para cada spider.
A partir desse momento, você deve ser capaz de rodar cada spider em separado usando o comando:
Nota: Dependendo do caso, é legal começar com um projeto desde o começo,
para já fazer tudo de maneira estruturada. Pessoalmente, eu gosto de começar
com spiders em arquivos soltos, quando estou apenas testando a viabilidade de
um crawler. Crio um projeto apenas quando vou colaborar no código com outras
pessoas ou fazer deploy no Cloud, nessa hora já é interessante que fique tudo
estruturado e fácil de crescer dentro de um projeto.
Configuração no Scrapy Cloud
Antes do deploy, você precisa criar um projeto no Scrapy Cloud. Na tela
inicial, clique no botão adicionar uma organização:
Dê um nome para a organização e confirme:
Em seguida, adicione um serviço do para hospedar o seu serviço, clicando no
botão “+ Service” que aparece dentro da organização criada:
Preencha os dados do seu projeto e confirme:
Depois disso, clique no nome do serviço na página inicial para acessar o local
onde seu projeto estará disponível:
Note o número identificador do seu projeto: você usará esse identificador na
hora fazer o deploy.
Instalando e configurando shub
A maneira mais fácil de fazer deploy no Scrapy Cloud é usando a ferramenta
shub, cliente da linha de comando
para o Scrapy Cloud e demais serviços da Scrapinghub.
Instale-a com:
Faça login com o shub, usando o comando:
Informe sua API key conforme for solicitado (descubra aqui sua API
key).
Dica: Ao fazer login, o shub criará no arquivo ~/.netrc uma entrada
configurada para usar sua API key. Esse arquivo também é usado pelo curl,
o que é útil para quando você deseje fazer requisições HTTP para as APIs na
linha de comando.
Preparando o projeto
Antes de fazer deploy do projeto, precisamos fazer deploy das dependências no
Scrapy Cloud.
Crie um arquivo requirements-deploy.txt com o seguinte conteúdo:
js2xml
slimit
ply
Rode o comando:
Substitua PROJECT_ID pelo id do seu projeto (neste caso, 27199).
Deploy das dependências
Agora faça deploy do projeto com o comando:
Novamente, substituindo PROJECT_ID pelo id do seu projeto (neste caso, 27199)
Se tudo deu certo, você verá algo como
Packing version 1447628479
Deploying to Scrapy Cloud project “27199”
{“status”: “ok”, “project”: 27199, “version”: “1447628479”, “spiders”: 5}
Run your spiders at: https://dash.scrapinghub.com/p/27199/
Agora você pode ir para a URL indicada (neste caso, https://dash.scrapinghub.com/p/27199/)
e agendar jobs dos spiders usando o botão “Schedule”.
Nota: opcionalmente, você pode configurar o identificador do projeto no
arquivo scrapy.cfg, para não precisar ter que lembrar a cada vez.
Para configurar um spider para rodar periodicamente, utilize a aba “Periodic
Jobs”, no menu à esquerda.
The End
Era isso, se você chegou até aqui, parabéns e obrigado pela atenção! 🙂
Você pode conferir o código do projeto final em: https://github.com/eliasdorneles/quotes_crawler
Para obter 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.
Obrigado Valdir pela ajuda com a montagem desse tutorial, tanto no desenvolvimento
do app spidyquotes quanto na escrita do material. You rock, dude!
Este artigo foi importado automaticamente por fazer parte do Planetário Dev. Quer fazer parte deste HUB de conteúdos? Faça parte do Planetário e veja as vantagens.
Não tem site ou blog? Seja um autor do site e ainda pode ser remunerado.
Leia também: