Web Scraping Com Python - Introdução Ao Scrapy
Web Scraping Com Python - Introdução Ao Scrapy
ao Scrapy
Aqui no Klipbox utilizamos muito uma ferramenta chamada Scrapy, que é um framework para web
crawling. Ele cuida de muitas coisinhas chatas do scraping, facilita outras, além de ser bastante
completo e open source.
E, dessa vez vou ensinar como utilizá-lo para extrair algumas informações da Wikipedia, além de
avançar em alguns conceitos introduzidos no meu primeiro artigo, Web scraping com python —
Extraindo informações de um ecommerce. Caso não tenha lido ainda e, seja iniciante no assunto,
recomendo muito a leitura antes desse texto.
Após meu primeiro artigo, algumas pessoas vieram me questionar sobre a ética do scraping, quero
deixar claro que o conteúdo aqui tem o objetivo apenas de ensinar, o que fazer com o conhecimento
vai de cada um, desde que esteja dentro da legalidade.
No entanto, nem sempre o scraping é a melhor forma de adquirir a informação. Muitos sites possuem
apis publicas, você pode por exemplo baixar toda Wikipedia como instruido aqui:
https://en.wikipedia.org/wiki/Wikipedia:Database_download, não precisa bombardear todas as
páginas deles hahaha.
As instruções de linha de comando apresentadas aqui são para um ambiente UNIX, lembre-se de
ajustar de acordo com seu sistema operacional.
Esse artigo faz parte de uma série de artigos sobre scraping em python:
Descrição do projeto
O objetivo desse projeto é extrair título, imagem e primeiro parágrafo de todas os artigos relacionados
à um artigo inicial da Wikipedia. Novamente vamos extrair essa informação para um csv, mas queremos
baixar as imagens localmente para uso futuro.
Preparando o ambiente
Caso queira acompanhar o tutorial enquanto escreve seu próprio código, você pode criar um ambiente,
desde que tenha o pipenv instalado, assim:
pipenv install jupyter notebook scrapy lxml requests
E ative o ambiente:
# Ativa o ambiente criado
pipenv shell
<head></head>
<body>
<div class="wrapper">
Texto Wrapper
<div>
Texto div 1
<a href="#" class="link">Link 1</a>
<a href="#">Link 2</a>
<span class="link_span">Span 1</span>
</div>
<div>
Texto div 2
<p>
<a href="#" class="link">Link 3</a>
</p>
</div>
</div>
<div>
<span>Span 2</span>
<a href="#" class="link">Link 4</a>
</div>
<p class="p_link">
Paragraph 1
</p>
</body>
</html>
Descendant::
A função descendant seleciona todos os elementos filhos que atendam os pré-requisitos após os dois
pontos, por exemplo:
//div[@class=”wrapper”]/descendant::a
//div[@class=”wrapper”]/descendant::a/@href
Descendant-or-self::
Funciona exatamente como descendant, mas adiciona o próprio elemento. Como exemplo, a
expressão:
//div[@class=”wrapper”]/descendant-or-self::div/text()
Retorna
[
'Texto Wrapper',
'Texto div 1',
'Texto div 2',
]
Ou seja, os textos de todos os divs filhos do div de classe wrapper incluindo ele mesmo.
Wildcard e booleanos(*)
Eu não cheguei a comentar sobre o wildcard na introdução ao XPATH, mas basicamente o asterisco
representa qualquer node(elemento).
A expressão //div[@class=”wrapper”]/descendant-or-self::* selecionaria TODOS os elementos filhos
de wrapper inclusive ele mesmo por exemplo.
Starts-with(@attr, ‘str’)
A função starts-with seleciona apenas os elementos cujo atributo(primeiro parâmetro) começa pela
string(segundo parâmetro) fornecida. Um exemplo nesse html seria selecionar o texto de tudo que
começa com link:
//*[starts-with(@class, “link”)]/text()
Retorna:
[
'Link 1',
'Span 1',
'Link 3',
'Link 4'
]
Com uma estrutura bem parecida à starts-with, podemos testar por expressões regulares com re:test.
Um exemplo similar ao acima, seria selecionar texto de elementos cuja classe contenha o texto link:
//*[re:test(@class, “.*link.*”)]/text()
Retorna:
[
'Link 1',
'Span 1',
'Link 3',
'Link 4',
'Paragraph 1'
]
Na verdade, existe uma função contains que é mais adequada ao exemplo acima, e funciona
exatamente como starts-with, mas como não utilizarei ela no tutorial, exemplifiquei com o regex aqui.
Juntando tudo isso, podemos escrever uma expressão bem complexa, como essa:
Utilizando o Scrapy
Como dito anteriormente, o scrapy é um framework completo para web scraping, nesse primeiro artigo
sobre o scrapy introduzirei os spiders, as “aranhas” que de fato executam o scraping; os
Items/ItemLoaders, que são as classes que o scrapy utiliza para lidar com os objetos extraídos; os
ItemPipelines, classes para execução de funções após a extração do Item.
allowed_domains — A lista de domínios permitidos. Caso não seja definida não haverá restrição
de domínios. Útil quando se está procurando novos links para extração mas não quer sair do
domínio atual por exemplo;
No momento esse spider não faz absolutamente nada, vamos mudar a página inicial e extrair a url de
resposta.
Para testar o spider, basta rodá-lo na linha de comando com o comando crawl:
scrapy crawl wiki
Se você quiser extrair para um arquivo, como um csv por exemplo, basta utilizar o parametro -o do
comando scrapy:
scrapy crawl wiki -o wiki.csv
Extraindo as informações
Esse spider ainda não extrai as informações que nós precisamos, vamos cuidar disso agora. O processo
de exploração para encontrar as expressões utilizadas aqui você encontrar no jupyter notebook no
meu repositório. Recomendo que você rode localmente e interaja com ele, descobrindo expressões
melhores e erros que eu possa ter cometido.
Título
Título no inspetor
O título, como pode-se ver na imagem do inspetor do chrome, está em um h1 com id firstHeading:
O objeto TextResponse, que é o objeto que recebemos no método parse, já possui um método xpath,
não sendo necessário utilizar o lxml aqui. Além disso possui algumas funções úteis como
extract_first()(extrai o primeiro match do xpath) e extract()(extrai todos os matches) que serializam e
retornam os elementos encontrados como uma lista, no caso de extract(), de strings unicode.
Primeiro parágrafo
Parágrafo no inspetor
Ao analisar a imagem do inspetor, percebe-se que o texto de interesse só está presente nos seguintes
elementos:
1. Na própria tag p;
2. Em tags b;
Quebrando em partes:
1. //div[@class=”mw-parser-output”]/p[1] — Primeiro parágrafo;
Imagem principal
Ao analisar o inspetor, percebe-se que a imagem principal é a primeira imagem na tabela dentro da
div de classe mw-parser-output. Como isso o spider fica assim:
Na verdade, essa expressão pega a primeira imagem na tabela de resumo do artigo. Geralmente
essa é a imagem que eu considero principal no artigo, porém em alguns casos essa imagem
não está presente e essa expressão pegaria qualquer outra imagem nessa tabela,
provavelmente não representando o que gostaríamos, mas para esse tutorial é bom o suficiente.
Pronto, vamos testar nosso spider agora:
scrapy crawl wiki
# O item extraído:
Items
Segundo a documentação do scrapy, os Items fornecem uma api parecida com dicionários python
porém com mais “estrutura”. Além disso, e o que importa para gente aqui, os Items do scrapy podem
ser usados com ItemLoaders para “limpar” nossos dados, em um arquivo mais adequado e para usar
ItemPipelines para execução de funções após a extração.
Normalmente, os Items são declarados no arquivo items.py criado automaticamente pelo comando
startproject. e não passam de uma classe python que determina quais são os campos desse item.
O nosso arquivo items.py ficaria assim:
O objeto Field nada mais é do que o um alias para a classe dict do python, ou seja, um bom e velho
dicionário python.
ItemLoader
Os ItemLoaders, quando são utilizados, são os mecanismos com os quais os Items são populados.
São muito úteis para quando o campo extraído pode estar em mais de um lugar, por exemplo utilizando
duas expressões xpath diferentes, e também, como usaremos aqui, para formatar dados.
Podemos criar ItemLoaders específicos para nossos Items, estendendo a classe ItemLoader e
sobrescrevendo(overriding) os métodos correspondentes. Geralmente declaro essas classes no arquivo
items.py. No nosso caso ficaria assim:
Aqui, definimos que:
3. E na entrada do parágrafo, juntamos(Join(‘’)) a array de textos, não precisando mais dar o join
no spider.
E ao rodar o spider:
scrapy crawl wiki
ItemPipelines
Na extração de informações, só falta uma coisa: baixar as imagens localmente para, caso quisermos
usá-las, não fazermos hotlinking. Para isso vamos utilizar os ItemPipelines.
ItemPipelines nada mais são que classes que definem o método process_item, nos quais serão
passados os Items. Aqui podemos adicionar campos aos Items, descartar duplicados, validar dados,
limpar html, etc.
Se rodarmos nosso spider nesse momento nada acontecerá por que esse pipeline não está configurado
para ser utilizado. Para isso precisamos editar o arquivo settings.py adicionando a configuração dos
ItemPipelines, o novo settings.py fica assim:
# -*- coding: utf-8 -*-
BOT_NAME = 'wikipedia'
SPIDER_MODULES = ['wikipedia.spiders']
NEWSPIDER_MODULE = 'wikipedia.spiders'
ROBOTSTXT_OBEY = True
ITEM_PIPELINES = {
'wikipedia.pipelines.WikipediaPipeline': 300,
}
O número 300 aqui dita a ordem em que os pipelines irão rodar caso existam mais de um,
quanto menor o número primeiro o item vai passar nele.
As configurações em settings.py são globais, para todos os spiders, caso necessário, você pode
defini-las na variável custom_settings do spider específico. Essa variável aceita um dict com as
configurações customizadas.
Agora, podemos rodar nosso scrapy que a imagem será baixada para a pasta images, e o novo item
terá o campo image:
scrapy crawl wiki
# O item extraído:
{‘image’: ‘120px-Atletico_mineiro_galo.png’,
‘image_url’: ‘http://upload.wikimedia.org/wikipedia/commons/thumb/5/5f/Atletico_mineiro_galo.png/120px-
Atletico_mineiro_galo.png',
‘paragraph’: ‘O Clube Atlético Mineiro (conhecido apenas por Atlético e cujo ‘
‘acrônimo é CAM) é um clube brasileiro de futebol sediado na ‘
‘cidade de Belo Horizonte, Minas Gerais. Fundado em 25 de março ‘
‘de 1908 por um grupo de estudantes, tem como suas cores ‘
‘tradicionais o preto e o branco. Contudo, o clube teve como ‘
‘primeiro nome . Seu símbolo e alcunha mais popular é o Galo, ‘
‘mascote oficial no final da década de 1930. O Atlético é um dos ‘
‘clubes mais populares do Brasil.’,
‘title’: ‘Clube Atlético Mineiro’,
‘url’: ‘https://pt.wikipedia.org/wiki/Clube_Atl%C3%A9tico_Mineiro’}
Só nos resta agora, encontrar os links de artigos relacionados e extrair as informações deles.
‘//div[@id=”bodyContent”]/descendant::a[re:test(@href, “^/wiki/[^:]*$”)]/@href’
É importante notar, que caso você queira usar a função re:test com a função xpath do lxml você
precisa definir o namespace da função, como no exemplo a seguir. No scrapy não é necessário pois já
é fornecido internamente.
html.xpath(‘//div[@id=”bodyContent”]/descendant::a[re:test(@href, “^/wiki/[^:]*$”)]/@href’, namespaces={“re”:
“http://exslt.org/regular-expressions"})
1. Para a função parse gerar mais de um item, ela deve, no lugar de retornar um item, gerar um
iterador de Requests e/ou Items ou dicts. Por isso utilizo o yield, gerando um iterador de
Requests para as urls encontradas, dessa vez, passando como callback a função parse_article
e o item gerado por parse_article utilizando a resposta inicial de parse;
2. Apenas extraio informações dos 5 primeiros artigos pois, para o propósito aqui, não há
necessidade de extrair os mais de mil artigos relacionados à página do Atlético-mg;
3. Uso de list(set(articles)) para ter uma lista única após tornar as urls absolutas.
Tudo pronto, podemos rodar nosso crawler e extrair tudo para um csv e baixar as imagens
relacionadas.
scrapy crawl wiki -o items.csv
Conclusão
Mesmo para um exemplo de crawler mais simples como esse, pode-se ver como o scrapy pode nos
fornecer a estrutura para manter nosso código simples, claro e de fácil manutenção. Utilizo-o muito no
meu dia a dia no Klipbox e acredito que pode te ajudar bastante em seus projetos de scraping.
Pretendo escrever artigos mais curtos de agora em diante com uma frequência maior, o próximo será
uma introdução à como extrair informações de páginas com javascript. Como já disse antes, esse é
um projeto novo e estou completamente aberto a sugestões ideias e feedbacks, só comentar abaixo!
Python
Scrapy
Scraping
Brasil
Dados