Web scraping com python—Introdução ao Scrapy ao Scrapy Aprenda a utilizar o scrapy para extrair informações de páginas da wikipédia. Aqui no Klipbox utilizamos muito uma ferramenta chamada Scrapy 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. 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 https://en.wikipedia.org/wiki/Wikipedia:Database_download páginas deles hahaha. O código utilizado aqui se encontra em https://github.com/hcoura/wikipedia_scrapper 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: 1. Web scraping com python—Extraindo Dados de um Ecommerce 2. Web scraping com python—Introdução ao Scrapy 3. Web scraping com python—Selenium e Javascript 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
Se você não possui o pipenv instalado, veja aqui mais instruções: https://medium.com/@henriquecoura_87435/webscraping-com-python-extraindo-dados-de-umecommerce-89c16b622f69#5783 Novas funções do XPATH
Nesse projeto vamos utilizar algumas funções mais avançadas do XPATH, caso precise refrescar a memória veja a minha introdução. Novamente, vamos trabalhar com um html hipotético:
Span
href="#"
class="link">Link
href="#"
class="link">Link
3
2 4 class="p_link"> 1
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 Retorna no html acima uma lista com os seguintes elementos:
Link 3
class="link">Link href="#">Link
1 2
Se quiséssemos as urls desses links: //div[@class=”wrapper”]/descendant::a/@href Que retornaria [‘#’, ‘#’, ‘#’,] 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 'Texto 'Texto ]
div div
Wrapper', 1', 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. Mas podemos filtrar o wildcard com operadores booleanos, a expressão //div[@class=”wrapper”]/descendant::*[not(self::a)]/text(), selecionaria os textos a seguir: [ 'Texto 'Texto 'Span 'Texto 'Link ]
div div
Wrapper', 1', 1', 2', 3'
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 'Span 'Link 'Link ]
1', 1', 3', 4'
Expressões regulares(re:test(@attr, ‘regex’))
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 'Span 'Link 'Link 'Paragraph ]
1', 1', 3', 4', 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. Uma expressão complexa
Juntando tudo isso, podemos escrever uma expressão bem complexa, como essa: //body/descendant-or-self::*[(self::a or self::p) and re:test(@class, “.*link.*”)]/text()
Traduzindo para o português, retorne o texto de todos os filhos “p” ou “a” do body, inclusive o body que possuam uma classe que contenha a palavra link. No caso: [ 'Link 'Link 'Link 'Paragraph ]
1', 3', 4', 1'
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 executa m 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. Iniciamos um projeto no scrapy com o seguinte comando: scrapy startproject wikipedia
E criamos nosso primeiro spider: scrapy genspider wiki pt.wikipedia.org
Esse é o spider gerado:
name—É o nome do spider(como iniciamos ele na linha de comando); 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;
start_urls—Lista de urls iniciais;
parse—Callback padrão para parsear a resposta.
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
No logo você verá uma linha assim: {‘url’: ‘https://pt.wikipedia.org/wiki/Clube_Atl%C3%A9tico_Mineiro'}
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; 3. Em tags a, em que a url não começa com #cite. O que gera a seguinte expressão: ‘//div[@class=”mw-parser-output”]/p[1]/descendant-or-self::*[self::a[not(starts-with(@href , “#cite”))] or self::b or self::p]/text()’ Quebrando em partes: 1. //div[@class=”mw-parser-output”]/p[1]—Primeiro parágrafo; 2. descendant-or-self::*—o node encontrado ou seus filhos que respeitem as regras entre colchetes;
3. self::a[not(starts-with(@href , “#cite”))]—elementos “a” que não começam com #cite; 4. or self::b or self::p—ou uma node b, ou uma node p. Atualizando nosso spider: Imagem principal
Imagem principal no inspetor 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: {‘url’: ‘https://pt.wikipedia.org/wiki/Clube_Atl%C3%A9tico_Mineiro', ‘title’: ‘Clube Atlético Mineiro’, ‘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.’, ‘image_url’: ‘//upload.wikimedia.org/wikipedia/commons/thumb/5/5f/Atletico_mineiro_galo.png/120px- Atletico_mineiro_galo.png’}
Aparentemente tudo está correto, exceto a image_url que está sem scheme, mas isso vamos corrigir ao falar de item loaders. 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. Com isso atualizamos nosso spider para o seguinte: Se rodarmos o spider, veremos o mesmo resultado que anteriormente: scrapy crawl wiki
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: 1. No padrão(default_input_processor, default_output_processor), pegaremos o primeiro valor(TakeFirst); 2. Na entrada da url da imagem(image_url_in) adicionaremos o schema(“http:”); 3. E na entrada do parágrafo, juntamos(Join(‘’)) a array de textos, não precisando mais dar o join no spider. Atualizando nosso spider para utilizar o ItemLoader: E ao rodar o spider: scrapy crawl wiki
Temos o resultado esperado: {‘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’ }
Note que agora a image_url agora está correta 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. Primeiro adicionamos um campo no nosso item chamado image: class url title paragraph image_url image = scrapy.Field()
Article(scrapy.Item): scrapy.Field() scrapy.Field() scrapy.Field() scrapy.Field()
= = = =
E editamos o arquivo pipelines.py (gerado automaticamente pelo comando startproject) para baixarmos as imagens: 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: # -*BOT_NAME = 'wikipedia' SPIDER_MODULES NEWSPIDER_MODULE ROBOTSTXT_OBEY = True ITEM_PIPELINES 'wikipedia.pipelines.WikipediaPipeline': }
coding:
utf-8
=
-*['wikipedia.spiders'] 'wikipedia.spiders'
= =
{ 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. Encontrando artigos relacionados
Ao investigar bastante o código fonte da página de um artigo da wikipedia, é possível chegar as seguintes conclusões em relação aos links de artigos relacionados: 1. As urls são relativas; 2. As urls começam com /wiki/ e depois vem o título do artigo; 3. Todos os links estão contidos em um div com id=bodyContent; 4. Não existe ponto e vírgula ‘/wiki/Ficheiro:Atletico_mineiro_galo.png’.
como
nesse
exemplo
negativo:
A seguinte expressão xpath extrai todos os links seguindo as regras acima: ‘//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 , “http://exslt.org/regular-expressions"})
“^/wiki/[^:]*$”)]/@href’,
namespaces={“re”:
Com isso atualizamos o nosso spider: É importante aqui, notar algumas coisas: 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! E se eu te ajudei de alguma forma não se esqueça de clicar no abaixo.
Python
Scrapy
Scraping
Brasil
Dados