Web Scraping Com Python
Web Scraping Com Python
Web Scraping Com Python
Ryan Mitchell
Novatec
Authorized Portuguese translation of the English edition of titled Web Scraping with Python, 2E,
ISBN 9781491985571 © 2018 Ryan Mitchell. This translation is published and sold by permission
of O'Reilly Media, Inc., the owner of all rights to publish and sell the same.
Tradução em português autorizada da edição em inglês da obra Web Scraping with Python, 2E,
ISBN 9781491985571 © 2018 Ryan Mitchell. Esta tradução é publicada e vendida com a permissão
da O'Reilly Media, Inc., detentora de todos os direitos para publicação e venda desta obra.
© Novatec Editora Ltda. [2019].
Todos os direitos reservados e protegidos pela Lei 9.610 de 19/02/1998. É proibida a reprodução
desta obra, mesmo parcial, por qualquer processo, sem prévia autorização, por escrito, do autor e da
Editora.
Editor: Rubens Prates
Tradução: Lúcia A. Kinoshita
Revisão gramatical: Tássia Carvalho
Editoração eletrônica: Carolina Kuwabata
ISBN: 978-85-7522-734-3
Histórico de edições impressas:
Março/2019 Segunda edição
Agosto/2016 Primeira reimpressão
Agosto/2015 Primeira edição (ISBN: 978-85-7522-447-2)
Novatec Editora Ltda.
Rua Luís Antônio dos Santos 110
02460-000 – São Paulo, SP – Brasil
Tel.: +55 11 2959-6529
Email: novatec@novatec.com.br
Site: www.novatec.com.br
Twitter: twitter.com/novateceditora
Facebook: facebook.com/novatec
LinkedIn: linkedin.com/in/novatec
Sumário
Prefácio
Parte I ■ Construindo scrapers
Capítulo 1 ■ Seu primeiro web scraper
Conectando
Introdução ao BeautifulSoup
Instalando o BeautifulSoup
Executando o BeautifulSoup
Conectando-se de forma confiável e tratando exceções
Capítulo 5 ■ Scrapy
Instalando o Scrapy
Escrevendo um scraper simples
Spidering com regras
Criando itens
Apresentando itens
Pipeline de itens
Fazendo log com o Scrapy
Outros recursos
Agradecimentos
Assim como alguns dos melhores produtos surgem de um grande volume
de feedback de usuários, este livro jamais poderia ter existido em nenhum
formato útil se não fosse a ajuda de muitos colaboradores, torcedores e
editores. Agradeço à equipe da O’Reilly e ao seu apoio incrível a esse
assunto, de certo modo, nada convencional, aos meus amigos e familiares
que me ofereceram conselhos e aceitaram fazer leituras extemporâneas e
aos meus colegas de trabalho da HedgeServ, a quem agora, provavelmente,
devo muitas horas de trabalho.
Agradeço, em particular, a Allyson MacDonald, Brian Anderson, Miguel
Grinberg e Eric VanWyk o feedback, a orientação e, ocasionalmente, o
tratamento duro, mas por amor. Muitas seções e exemplos de código
foram escritos como resultado direto de suas sugestões inspiradoras.
Agradeço a Yale Specht a paciência ilimitada nos últimos quatro anos e
duas edições, dando-me a coragem inicial para levar adiante este projeto,
além de oferecer feedback quanto ao estilo durante o processo de escrita.
Sem ele, este livro teria sido escrito em metade do tempo, mas não estaria
nem perto de ser tão útil.
Por fim, agradeço a Jim Waldo, que realmente deu início a tudo isso
muitos anos atrás, quando enviou uma máquina Linux e o livro The Art
and Science of C a uma adolescente jovem e sugestionável.
PARTE I
Construindo scrapers
Conectando
Se você ainda não dedicou muito tempo a redes ou segurança de redes, o
modo de funcionamento da internet pode parecer um pouco misterioso.
Você não vai querer pensar no que a rede faz, exatamente, sempre que
abrir um navegador e acessar http://google.com, e, hoje em dia, isso não é
necessário. Na verdade, eu diria que o fato de as interfaces de computador
terem avançado tanto, a ponto de a maioria das pessoas que usam a
internet não ter a mínima ideia de como ela funciona, é incrível.
No entanto, o web scraping exige abrir mão de parte dessa camada
protetora da interface – não só no nível do navegador (o modo como ele
interpreta todos esses códigos HTML, CSS e JavaScript), mas também,
ocasionalmente, no nível da conexão de rede.
Para ter uma ideia da infraestrutura necessária para que as informações
cheguem até o seu navegador, vamos usar o exemplo a seguir. Alice tem
um servidor web. Bob usa um computador desktop, que está tentando se
conectar com o servidor de Alice. Quando uma máquina quer conversar
com outra, algo como a troca a seguir ocorre:
1. O computador de Bob envia um stream de bits 1s e 0s, representados
como tensões altas e baixas em um fio. Esses bits compõem uma
informação, com um cabeçalho (header) e um corpo (body). O
cabeçalho contém um destino imediato, que é o endereço MAC do
roteador local de Bob, e um destino final, que é o endereço IP de Alice.
O corpo contém a requisição para a aplicação de servidor de Alice.
2. O roteador local de Bob recebe todos esses 1s e 0s e os interpreta como
um pacote, do endereço MAC de Bob, destinado ao endereço IP de
Alice. O roteador carimba o seu próprio endereço IP no pacote como o
endereço IP de “origem” (from) e o envia pela internet.
3. O pacote de Bob passa por vários servidores intermediários, que o
direcionam pelo caminho físico/conectado correto até o servidor de
Alice.
4. O servidor de Alice recebe o pacote em seu endereço IP.
5. O servidor de Alice lê a porta de destino do pacote no cabeçalho,
passando-o para a aplicação apropriada – a aplicação do servidor web.
(A porta de destino do pacote é quase sempre a porta 80 para aplicações
web; podemos pensar nela como o número de um apartamento para
dados de pacote, enquanto o endereço IP seria como o endereço da rua.)
6. A aplicação de servidor web recebe um stream de dados do processador
do servidor. Esses dados dizem algo como:
- Esta é uma requisição GET.
- O arquivo a seguir está sendo requisitado: index.html.
7. O servidor web localiza o arquivo HTML correto, insere esse arquivo
em um novo pacote a ser enviado para Bob e o envia por meio de seu
roteador local, para que seja transportado de volta para o computador
de Bob, pelo mesmo processo.
E voilà! Temos a Internet.
Então, nessa troca, em que ponto o navegador web entra em cena?
Absolutamente, em nenhum lugar. Na verdade, os navegadores são uma
invenção relativamente recente na história da internet, considerando que o
Nexus foi lançado em 1990.
Sim, o navegador web é uma aplicação útil para criar esses pacotes de
informações, dizendo ao seu sistema operacional que os envie e
interpretando os dados recebidos na forma de imagens bonitas, áudios,
vídeos e texto. Entretanto, um navegador web é somente um código, e um
código pode ser dividido, separado em seus componentes básicos,
reescrito, reutilizado, e você pode fazer com que ele aja como você quiser.
Um navegador web pode dizer ao processador que envie dados para a
aplicação que trata a sua interface sem fio (ou com fio), mas você pode
fazer o mesmo em Python usando apenas três linhas de código:
from urllib.request import urlopen
html = urlopen('http://pythonscraping.com/pages/page1.html')
print(html.read())
Para executar esse código, use o notebook iPython do Capítulo 1
(https://github.com/REMitchell/python-
scraping/blob/master/Chapter01_BeginningToScrape.ipynb) que está no
repositório do GitHub, ou salve-o localmente como scrapetest.py e execute-
o em seu terminal com o comando a seguir:
$ python scrapetest.py
Observe que, se você também tiver Python 2.x instalado em seu
computador e estiver executando as duas versões de Python lado a lado,
talvez seja necessário chamar explicitamente o Python 3.x executando o
comando da seguinte maneira:
$ python3 scrapetest.py
Esse comando exibe o código HTML completo de page1, que está no URL
http://pythonscraping.com/pages/page1.html. Para ser mais exato, ele exibe
o arquivo HTML page1.html, que se encontra no diretório <web
root>/pages, no servidor localizado no domínio http://pythonscraping.com.
Por que é importante começar a pensar nesses endereços como “arquivos”
em vez de “páginas”? A maioria das páginas web modernas tem muitos
arquivos de recursos associados a elas. Podem ser arquivos de imagens,
arquivos JavaScript, arquivos CSS ou qualquer outro conteúdo associado à
página sendo requisitada. Quando um navegador web encontra uma tag
como <img src="cuteKitten.jpg">, ele sabe que deve fazer outra requisição ao
servidor a fim de obter os dados do arquivo cuteKitten.jpg e renderizar
totalmente a página para o usuário.
É claro que seu script Python não tem a lógica para voltar e requisitar
vários arquivos (ainda); ele é capaz de ler apenas o único arquivo HTML
que você requisitou diretamente.
from urllib.request import urlopen
significa o que parece que significa: a instrução olha para o módulo request
de Python (que se encontra na biblioteca urllib) e importa somente a
função urlopen.
urllib é uma biblioteca-padrão de Python (ou seja, não é necessário instalar
nada extra para executar esse exemplo); ela contém funções para requisitar
dados da web, tratando cookies e até mesmo modificando metadados
como cabeçalhos e o agente de usuário (user agent). Usaremos bastante a
urllib neste livro, portanto recomendo que você leia a documentação de
Python dessa biblioteca (https://docs.python.org/3/library/urllib.html).
urlopen é usada para abrir um objeto remoto em uma rede e lê-lo. Por ser
uma função razoavelmente genérica (é capaz de ler facilmente arquivos
HTML, arquivos de imagens ou qualquer outro stream de arquivo), ela
será usada com muita frequência neste livro.
Introdução ao BeautifulSoup
Bela Sopa, tão rica e verde,
À espera em uma terrina quente!
Quem não se derreteria por uma iguaria como essa?
Sopa do jantar, bela Sopa!1
A biblioteca BeautifulSoup2 tem o mesmo nome de um poema de Lewis
Carroll que aparece no livro Alice no País das Maravilhas. Na história, esse
poema é declamado por uma personagem chamada Falsa Tartaruga (Mock
Turtle) – por si só, é um trocadilho com o nome do popular prato vitoriano
chamado Sopa Falsa de Tartaruga (Mock Turtle Soup), que não é feito de
tartaruga, mas de vaca).
Assim como o seu homônimo no País das Maravilhas, o BeautifulSoup
tenta dar sentido ao que não faz sentido: ajuda a formatar e a organizar a
web confusa, fazendo correções em um código HTML mal formatado e
apresentando objetos Python que podem ser facilmente percorridos, os
quais representam estruturas XML.
Instalando o BeautifulSoup
Como a biblioteca BeautifulSoup não é uma biblioteca Python default, ela
deve ser instalada. Se você já tem experiência em instalar bibliotecas
Python, use seu instalador favorito e vá direto para a próxima seção,
“Executando o BeautifulSoup”.
Para aqueles que ainda não instalaram bibliotecas Python (ou que precisam
de um lembrete), o método genérico a seguir será usado para instalar várias
bibliotecas ao longo do livro, portanto você poderá consultar esta seção no
futuro.
Usaremos a biblioteca BeautifulSoup 4 (também conhecida como BS4)
neste livro. As instruções completas para instalar o BeautifulSoup 4 podem
ser encontradas em Crummy.com
(https://www.crummy.com/software/BeautifulSoup/bs4/doc/); no entanto, o
método básico para Linux é exibido a seguir:
$ sudo apt-get install python-bs4
Para Macs, execute:
$ sudo easy_install pip
Esse comando instala o gerenciador de pacotes Python pip. Então execute o
comando a seguir para instalar a biblioteca:
$ pip install beautifulsoup4
Novamente, observe que, se você tiver tanto Python 2.x quanto Python 3.x
instalados em seu computador, talvez seja necessário chamar python3
explicitamente:
$ python3 myScript.py
Certifique-se de usar também esse comando ao instalar pacotes; caso
contrário, os pacotes poderão ser instalados para Python 2.x, mas não para
Python 3.x:
$ sudo python3 setup.py install
Se estiver usando pip, você também poderá chamar pip3 para instalar as
versões dos pacotes para Python 3.x:
$ pip3 install beautifulsoup4
Instalar pacotes no Windows é um processo quase idêntico àquele usado
em Mac e Linux. Faça download da versão mais recente do BeautifulSoup
4 a partir da página de download
(http://www.crummy.com/software/BeautifulSoup/#Download), vá para o
diretório no qual você o descompactou e execute:
> python setup.py install
É isso! O BeautifulSoup agora será reconhecido como uma biblioteca
Python em seu computador. Você pode fazer um teste abrindo um terminal
Python e importando a biblioteca:
$ python
> from bs4 import BeautifulSoup
A importação deverá ser concluída sem erros.
Além disso, há um instalador .exe para pip no Windows
(https://pypi.org/project/setuptools/), para que você possa instalar e
gerenciar pacotes facilmente:
> pip install beautifulsoup4
Mantendo as bibliotecas organizadas em ambientes virtuais
Se você pretende trabalhar com vários projetos Python, ou precisa de uma maneira fácil de
empacotar projetos com todas as bibliotecas associadas, ou está preocupado com possíveis
conflitos entre bibliotecas instaladas, é possível instalar um ambiente virtual Python a fim de
manter tudo separado e simples de administrar.
Ao instalar uma biblioteca Python sem um ambiente virtual, ela será instalada globalmente. Em
geral, isso exige que você seja um administrador ou execute como root, e que a biblioteca Python
esteja disponível para todos os usuários de todos os projetos no computador. Felizmente, criar
um ambiente virtual é fácil:
$ virtualenv scrapingEnv
Esse comando cria um novo ambiente chamado scrapingEnv, que deve ser ativado para ser
usado:
$ cd scrapingEnv/
$ source bin/activate
Depois de ativado, o nome do ambiente será exibido no prompt de comandos para que você
lembre que está trabalhando nele no momento. Qualquer biblioteca que você instalar ou
qualquer script que executar estarão somente nesse ambiente virtual.
Trabalhando no ambiente scrapingEnv recém-criado, você poderá instalar e usar o
BeautifulSoup; por exemplo:
(scrapingEnv)ryan$ pip install beautifulsoup4
(scrapingEnv)ryan$ python
> from bs4 import BeautifulSoup
>
O comando deactivate pode ser usado para sair do ambiente; depois disso, não será mais
possível acessar nenhuma biblioteca que tenha sido instalada no ambiente virtual:
(scrapingEnv)ryan$ deactivate
ryan$ python
> from bs4 import BeautifulSoup
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'bs4'
Manter todas as bibliotecas separadas por projeto também facilita compactar a pasta completa
do ambiente e enviá-la para outra pessoa. Desde que essas pessoas tenham a mesma versão de
Python instalada em seus computadores, o código funcionará no ambiente virtual, sem exigir a
instalação de nenhuma biblioteca.
Embora eu não dê explicitamente instruções para que você use um ambiente virtual nos
exemplos deste livro, lembre-se de que pode utilizar um ambiente desse tipo a qualquer
momento, apenas o ativando previamente.
Executando o BeautifulSoup
O objeto mais comum usado na biblioteca BeautifulSoup, como não
poderia deixar de ser, é o objeto BeautifulSoup. Vamos observá-lo em ação,
modificando o exemplo apresentado no início deste capítulo:
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com/pages/page1.html')
bs = BeautifulSoup(html.read(), 'html.parser')
print(bs.h1)
Eis a saída:
<h1>An Interesting Title</h1>
Note que somente a primeira instância da tag h1 encontrada na página é
devolvida. Por convenção, apenas uma tag h1 deve ser usada em uma
página; contudo, as convenções muitas vezes são desrespeitadas na web,
portanto saiba que esse código devolverá somente a primeira instância da
tag, e não necessariamente aquela que você está procurando.
Como nos exemplos anteriores de web scraping, a função urlopen está
sendo importada e a função html.read() é chamada para obter o conteúdo
HTML da página. Além da string de texto, o BeautifulSoup também pode
usar diretamente o objeto de arquivo devolvido por urlopen, sem precisar
chamar .read() antes:
bs = BeautifulSoup(html, 'html.parser')
O conteúdo HTML é então transformado em um objeto BeautifulSoup com a
seguinte estrutura:
• html → <html><head>...</head><body>...</body></html>
– head → <head><title>A Useful Page<title></head>
– title → <title>A Useful Page</title>
– body → <body><h1>An Int...</h1><div>Lorem ip...</div></body>
– h1 → <h1>An Interesting Title</h1>
– div → <div>Lorem Ipsum dolor...</div>
Observe que a tag h1 que você extraiu da página está aninhada a dois níveis
de profundidade na estrutura do objeto BeautifulSoup (html → body → h1). No
entanto, ao buscá-la no objeto, é possível acessar a tag h1 diretamente:
bs.h1
Com efeito, qualquer uma das chamadas de função a seguir produziria o
mesmo resultado:
bs.html.body.h1
bs.body.h1
bs.html.h1
Ao criar um objeto BeautifulSoup, dois argumentos são passados:
bs = BeautifulSoup(html.read(), 'html.parser')
O primeiro é o texto HTML no qual o objeto se baseia, e o segundo
especifica o parser que queremos que o BeautifulSoup use para criar esse
objeto. Na maioria dos casos, o parser escolhido não fará diferença.
html.parser é um parser incluído no Python 3, e não exige nenhuma
instalação extra para ser usado. Exceto quando outro parser for necessário,
usaremos esse parser no livro.
Outro parser conhecido é o lxml (http://lxml.de/parsing.html). Esse parser
pode ser instalado com o pip:
$ pip3 install lxml
O lxml pode ser usado com o BeautifulSoup se a string de parser
especificada for alterada:
bs = BeautifulSoup(html.read(), 'lxml')
O lxml tem algumas vantagens em relação ao html.parser: em geral, ele é
melhor para fazer parse de código html “confuso” ou mal formatado. É um
parser mais tolerante e corrige problemas como tags sem fechamento, tags
indevidamente aninhadas e tags de cabeçalho e de corpo ausentes. De certo
modo, é também um parser mais rápido que o html.parser, embora a
velocidade não seja necessariamente uma vantagem no web scraping,
considerando que a própria velocidade da rede quase sempre será o
principal gargalo.
Uma das desvantagens do lxml é que ele deve ser instalado separadamente
e depende de bibliotecas C de terceiros para funcionar. Isso pode causar
problemas de portabilidade e de facilidade de uso, em comparação com o
html.parser.
Outro parser HTML conhecido é o html5lib. Assim como o lxml, o html5lib
é um parser extremamente tolerante, com mais iniciativa ainda para
corrigir um código HTML com falhas. Também tem dependência externa e
é mais lento quando comparado tanto com o lxml quanto com o
html.parser. Apesar disso, essa pode ser uma boa opção caso você esteja
trabalhando com sites HTML confusos ou escritos manualmente.
Para usá-lo, faça a sua instalação e passe a string html5lib para o objeto
BeautifulSoup:
bs = BeautifulSoup(html.read(), 'html5lib')
Espero que essa pequena amostra do BeautifulSoup tenha dado a você uma
ideia da eficácia e da simplicidade dessa biblioteca. Qualquer informação
pode ser virtualmente extraída de qualquer arquivo HTML (ou XML),
desde que ela tenha uma tag de identificação ao redor ou próxima a ela. O
Capítulo 2 detalha melhor as chamadas de função mais complexas do
BeautifulSoup, além de apresentar as expressões regulares e mostrar como
elas podem ser usadas com o BeautifulSoup para extrair informações dos
sites.
def getTitle(url):
try:
html = urlopen(url)
except HTTPError as e:
return None
try:
bs = BeautifulSoup(html.read(), 'html.parser')
title = bs.body.h1
except AttributeError as e:
return None
return title
title = getTitle('http://www.pythonscraping.com/pages/page1.html')
if title == None:
print('Title could not be found')
else:
print(title)
Nesse exemplo, criamos uma função getTitle que devolve o título da
página, ou um objeto None caso tenha havido algum problema para obtê-lo.
Em getTitle, verificamos se houve um HTTPError, como no exemplo anterior,
e encapsulamos duas das linhas do BeautifulSoup em uma instrução try.
Um AttributeError pode ser lançado por qualquer uma dessas linhas (se o
servidor não existir, html seria um objeto None e html.read() lançaria um
AttributeError). Na verdade, você poderia incluir quantas linhas quiser em
uma instrução try, ou chamar outra função totalmente diferente, que
poderia lançar um AttributeError em qualquer ponto.
Ao escrever scrapers, é importante pensar no padrão geral de seu código a
fim de lidar com as exceções e, ao mesmo tempo, deixá-lo legível. É
provável que você queira também fazer uma intensa reutilização de código.
Ter funções genéricas como getSiteHTML e getTitle (completas, com todo o
tratamento para exceções) facilita fazer uma coleta de dados da web de
forma rápida – e confiável.
Navegando em árvores
A função find_all é responsável por encontrar tags com base em seus nomes
e atributos. Como seria se tivéssemos que achar uma tag com base em sua
localização em um documento? É nesse cenário que uma navegação em
árvore se torna conveniente. No Capítulo 1, vimos como navegar em uma
árvore do BeautifulSoup em uma única direção:
bs.tag.subTag.outraSubTag
Vamos agora ver como navegar para cima, para os lados e na diagonal em
árvores HTML. Usaremos nosso site de compras extremamente
questionável em http://www.pythonscraping.com/pages/page3.html como
uma página de exemplo para coleta de dados, conforme vemos na Figura
2.1.
Figura 2.1 – Imagem da tela em
http://www.pythonscraping.com/pages/page3.html.
O HTML dessa página, mapeado na forma de árvore (com algumas tags
omitidas para sermos mais sucintos), tem o seguinte aspecto:
• HTML
– body
– div.wrapper
– h1
– div.content
– table#giftList
– tr
– th
– th
– th
– th
– tr.gift#gift1
– td
– td
– span.excitingNote
– td
– td
– img
– ... outras linhas da tabela...
– div.footer
Essa mesma estrutura HTML será usada como exemplo nas próximas
seções.
Lidando com filhos e outros descendentes
Em ciência da computação e em alguns ramos da matemática, muitas vezes
ouvimos falar de ações horríveis cometidas com os filhos: movê-los,
armazená-los, removê-los e até mesmo matá-los. Felizmente, esta seção
tem como foco apenas a sua seleção!
Na biblioteca BeautifulSoup, assim como em muitas outras bibliotecas, há
uma distinção entre filhos e descendentes: de modo muito parecido com
uma árvore genealógica humana, os filhos estão sempre exatamente uma
tag abaixo de um pai, enquanto os descendentes podem estar em qualquer
nível da árvore abaixo de um pai. Por exemplo, as tags tr são filhas da tag
table, enquanto tr, th, td, img e span são todas descendentes da tag table (pelo
menos em nossa página de exemplo). Todos os filhos são descendentes,
mas nem todos os descendentes são filhos.
Em geral, as funções do BeautifulSoup sempre lidam com os descendentes
da tag selecionada no momento. Por exemplo, bs.body.h1 seleciona a
primeira tag h1 que é descendente da tag body. As tags localizadas fora do
corpo não serão encontradas.
De modo semelhante, bs.div.find_all('img') encontrará a primeira tag div no
documento, e então obterá uma lista de todas as tags img que são
descendentes dessa tag div.
Se quiser encontrar somente os descendentes que sejam filhos, a tag
.children pode ser usada:
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com/pages/page3.html')
bs = BeautifulSoup(html, 'html.parser')
for child in bs.find('table',{'id':'giftList'}).children:
print(child)
Esse código exibe a lista das linhas de produto da tabela giftList, incluindo
a linha inicial com os rótulos das colunas. Se fôssemos escrevê-lo com a
função descendants() no lugar de children(), aproximadamente duas dúzias
de tags seriam encontradas na tabela e seriam exibidas, incluindo as tags
img, span e as tags td individuais. Sem dúvida, é importante fazer uma
diferenciação entre filhos e descendentes!
Lidando com irmãos
Com a função next_siblings() do BeautifulSoup, é trivial coletar dados de
tabelas, particularmente daquelas com linhas de título.
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com/pages/page3.html')
bs = BeautifulSoup(html, 'html.parser')
– "$15.00" ❹
– td ❷
– <img src="../img/gifts/img1.jpg"> ❶
Expressões regulares
Como diz a velha piada em ciência da computação: “Suponha que você
tenha um problema e decida resolvê-lo usando expressões regulares. Bem,
agora você tem dois problemas”.
Infelizmente, as expressões regulares (em geral, abreviadas como regex)
muitas vezes são ensinadas com tabelas grandes de símbolos aleatórios,
encadeados de modo a parecer um conjunto sem sentido. Isso tende a
afastar as pessoas; mais tarde, elas saem para o mercado de trabalho e
escrevem funções complicadas e desnecessárias para pesquisar e filtrar,
quando apenas precisavam de uma expressão regular com uma só linha!
Felizmente para você, não é tão difícil começar a trabalhar com expressões
regulares, e é fácil aprender a usá-las analisando e fazendo experimentos
com alguns exemplos simples.
As expressões regulares recebem esse nome porque são usadas para
identificar strings regulares; elas podem seguramente afirmar o seguinte:
“Sim, esta string que você me deu segue as regras e será devolvida” ou
“Esta string não segue as regras e será descartada”. Esse recurso pode ser
excepcionalmente conveniente para analisar documentos grandes em busca
de strings que sejam números de telefone ou endereços de email, de forma
rápida.
Observe que usei a expressão strings regulares. O que é uma string regular?
É qualquer string que seja gerada por uma série de regras lineares3, como
estas:
1. Escreva a letra a no mínimo uma vez.
2. Concatene aí a letra b exatamente cinco vezes.
3. Concatene aí a letra c qualquer número par de vezes.
4. Escreva a letra d ou a letra e no final.
Strings que seguem essas regras são aaaabbbbbccccd, aabbbbbcce e assim
por diante (há um número infinito de variações).
As expressões regulares são simplesmente uma forma concisa de expressar
esses conjuntos de regras. Por exemplo, eis a expressão regular para a série
de passos que acabamos de descrever:
aa*bbbbb(cc)*(d|e)
Essa string pode parecer um pouco assustadora à primeira vista, mas se
tornará mais clara se for separada em seus componentes:
aa*
A letra a é escrita, seguida de a* (leia como a asterisco), que significa
“qualquer quantidade de as, incluindo nenhum”. Dessa forma, podemos
garantir que a letra a seja escrita pelo menos uma vez.
bbbbb
Não há efeitos especiais nesse caso – somente cinco bs em sequência.
(cc)*
Qualquer número par de itens pode ser agrupado em pares, portanto,
para impor essa regra sobre itens pares, podemos escrever dois cs, colocá-
los entre parênteses e escrever um asterisco depois, o que significa que
podemos ter qualquer número de pares de cs (observe que isso pode
significar nenhum par também).
(d|e)
Acrescentar uma barra no meio de duas expressões significa que ela pode
ser “isto ou aquilo”. Nesse caso, estamos dizendo para “adicionar um d ou
um e”. Assim, garantimos que haja exatamente um desses dois caracteres.
Acessando atributos
Até agora, vimos como acessar e filtrar tags e acessar conteúdos dentro
delas. No entanto, com frequência no web scraping, não estaremos
procurando o conteúdo de uma tag, mas os seus atributos. Isso será
particularmente útil com tags como a, em que o URL para o qual elas
apontam está contido no atributo href, ou para a tag img, em que a imagem
desejada está no atributo src.
Com objetos de tag, uma lista Python com os atributos pode ser
automaticamente acessada por meio de uma chamada como esta:
myTag.attrs
Tenha em mente que esse código devolve literalmente um objeto de
dicionário Python, fazendo com que seja trivial acessar e manipular esses
atributos. O local em que está a fonte de uma imagem, por exemplo, pode
ser encontrado com a linha a seguir:
myImgTag.attrs['src']
Expressões lambda
Se você teve uma educação formal em ciência da computação,
provavelmente deve ter conhecido as expressões lambda enquanto
estudava e jamais voltou a usá-las. Se não teve, talvez não as conheça (ou
conheça somente como “aquele assunto que pretendia estudar algum
dia”). Esta seção não entra em detalhes sobre esses tipos de funções, mas
mostra como elas podem ser úteis no web scraping.
Essencialmente, uma expressão lambda é uma função que é passada para
outra função como uma variável; em vez de definir uma função como f(x,
y), é possível defini-la como f(g(x), y) ou até mesmo como f(g(x), h(x)).
O BeautifulSoup permite passar determinados tipos de funções como
parâmetros da função find_all.
A única restrição é que essas funções devem aceitar um objeto tag como
argumento e devolver um booleano. Todo objeto tag encontrado pelo
BeautifulSoup é avaliado por essa função, e as tags avaliadas como True são
devolvidas, enquanto as demais são descartadas.
Por exemplo, o código a seguir obtém todas as tags que tenham
exatamente dois atributos:
bs.find_all(lambda tag: len(tag.attrs) == 2)
Nesse caso, a função passada como argumento é len(tag.attrs) == 2.
Quando ela for True, a função find_all devolverá a tag. Isso significa que ela
encontrará as tags com dois atributos, por exemplo:
<div class="body" id="content"></div>
<span style="color:red" class="title"></span>
As funções lambda são tão úteis que você pode usá-las até mesmo para
substituir funções do BeautifulSoup:
bs.find_all(lambda tag: tag.get_text() ==
'Or maybe he\'s only resting?')
Isso pode ser feito sem uma função lambda:
bs.find_all('', text='Or maybe he\'s only resting?')
Todavia, se você se lembrar da sintaxe da função lambda e de como acessar
propriedades de tags, talvez não seja necessário se lembrar de mais
nenhuma outra sintaxe do BeautifulSoup novamente!
Como a função lambda fornecida pode ser qualquer função que devolva
um valor True ou False, podemos até mesmo combiná-la com expressões
regulares para encontrar tags com um atributo que corresponda a
determinado padrão de string.
1 Se você estiver tentando obter uma lista com todas as tags h<algum_nível> do documento, há
maneiras mais sucintas de escrever esse código e fazer o mesmo. Veremos outras formas de
abordar esses tipos de problemas na seção “Expressões regulares e o BeautifulSoup”.
2 O Guia de Referência da Linguagem Python (Python Language Reference) contém uma lista
completa das palavras reservadas protegidas
(https://docs.python.org/3/reference/lexical_analysis.html#keywords).
3 Você pode estar se perguntando: “Há expressões ‘irregulares’?”. Expressões não regulares estão
além do escopo deste livro, mas incluem strings como “escreva um número primo de as, seguido
exatamente do dobro desse número de bs” ou “escreva um palíndromo”. É impossível identificar
strings desse tipo com uma expressão regular. Felizmente, jamais me vi em uma situação em que
meu web scraper tivesse de identificar esses tipos de strings.
CAPÍTULO 3
Escrevendo web crawlers
Até agora, vimos páginas estáticas únicas, com exemplos, de certo modo,
artificiais. Neste capítulo, começaremos a analisar problemas do mundo
real, com scrapers percorrendo várias páginas – e até mesmo vários sites.
Os web crawlers (rastreadores web) recebem esse nome porque rastreiam
(crawl) a web. Em seu núcleo, encontra-se um elemento de recursão. Eles
devem obter o conteúdo da página de um URL, analisar essa página em
busca de outro URL e obter essa página, ad infinitum.
Saiba, porém, que não é só porque você pode rastrear a web que deve
sempre fazê-lo. Os scrapers usados nos exemplos anteriores funcionam
muito bem nas situações em que todos os dados necessários estão em uma
só página. Com os web crawlers, você deve ser extremamente zeloso
quanto ao volume de banda que usar, fazendo o máximo de esforço para
determinar se há alguma maneira de aliviar a carga do servidor consultado.
html = urlopen('http://en.wikipedia.org/wiki/Kevin_Bacon')
bs = BeautifulSoup(html, 'html.parser')
for link in bs.find_all('a'):
if 'href' in link.attrs:
print(link.attrs['href'])
Se observarmos a lista de links gerada, percebemos que todos os artigos
esperados estão presentes: “Apollo 13”, “Philadelphia”, “Primetime Emmy
Award”, e assim por diante. No entanto, há alguns itens indesejados
também:
//wikimediafoundation.org/wiki/Privacy_policy
//en.wikipedia.org/wiki/Wikipedia:Contact_us
De fato, a Wikipédia está repleta de links para caixas de texto, rodapés e
cabeçalhos, presentes em todas as páginas, além de links para as páginas de
categoria, páginas de discussão e outras páginas que não contêm artigos
diferentes:
/wiki/Category:Articles_with_unsourced_statements_from_April_2014
/wiki/Talk:Kevin_Bacon
Recentemente, um amigo meu, enquanto trabalhava em um projeto
semelhante de coleta de dados da Wikipédia, mencionou que havia escrito
uma função longa de filtragem, com mais de cem linhas de código, a fim de
determinar se um link interno da Wikipédia era de uma página de artigo.
Infelizmente, ele não havia investido muito tempo antes tentando
encontrar padrões entre “links para artigos” e “outros links”; do contrário,
ele teria descoberto o truque. Se analisarmos os links que apontam para
páginas de artigos (em oposição a outras páginas internas), veremos que
todos eles têm três características em comum:
• Estão na div com o id definido com bodyContent.
• Os URLs não contêm dois-pontos.
• Os URLs começam com /wiki/.
Essas regras podem ser usadas para uma pequena revisão no código a fim
de obter somente os links desejados para artigos, usando a expressão
regular ^(/wiki/)((?!:).)*$"):
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
html = urlopen('http://en.wikipedia.org/wiki/Kevin_Bacon')
bs = BeautifulSoup(html, 'html.parser')
for link in bs.find('div', {'id':'bodyContent'}).find_all(
'a', href=re.compile('^(/wiki/)((?!:).)*$')):
if 'href' in link.attrs:
print(link.attrs['href'])
Se esse código for executado, veremos uma lista de todos os URLs de
artigos para os quais o artigo da Wikipédia sobre Kevin Bacon aponta.
É claro que ter um script que encontre todos os links de artigos em um
único artigo da Wikipédia previamente definido, apesar de ser interessante,
é um tanto quanto inútil na prática. É necessário transformar esse código
em algo mais parecido com o seguinte:
• Uma única função, getLinks, que receba um URL de um artigo da
Wikipédia no formato /wiki/<Nome_do_Artigo> e devolva uma lista com os
URLs de todos os artigos associados, no mesmo formato.
• Uma função principal que chame getLinks com um artigo inicial, escolha
um link de artigo aleatório na lista devolvida e chame getLinks
novamente, até que você interrompa o programa ou nenhum link de
artigo seja encontrado na nova página.
Eis o código completo para isso:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import datetime
import random
import re
random.seed(datetime.datetime.now())
def getLinks(articleUrl):
html = urlopen('http://en.wikipedia.org{}'.format(articleUrl))
bs = BeautifulSoup(html, 'html.parser')
return bs.find('div', {'id':'bodyContent'}).find_all('a',
href=re.compile('^(/wiki/)((?!:).)*$'))
links = getLinks('/wiki/Kevin_Bacon')
while len(links) > 0:
newArticle = links[random.randint(0, len(links)-1)].attrs['href']
print(newArticle)
links = getLinks(newArticle)
A primeira tarefa do programa, depois de importar as bibliotecas
necessárias, é definir a semente (seed) para o gerador de números
aleatórios com o horário atual do sistema. Isso praticamente garante que
haja um caminho aleatório novo e interessante pelos artigos da Wikipédia
sempre que o programa executar.
Números pseudoaleatórios e sementes aleatórias
No exemplo anterior, o gerador de números aleatórios de Python foi usado para selecionar um
artigo aleatório em cada página a fim de continuar percorrendo a Wikipédia aleatoriamente. No
entanto, os números aleatórios devem ser utilizados com cautela.
Embora sejam ótimos para calcular respostas corretas, os computadores são péssimos para
inventar algo. Por esse motivo, os números aleatórios podem ser um desafio. A maioria dos
algoritmos para números aleatórios se esforça em gerar uma sequência de números
uniformemente distribuídos e difíceis de prever, mas um número para “semente” (seed) deve ser
fornecido a esses algoritmos para que tenham um valor com o qual poderão trabalhar
inicialmente. A mesma semente sempre produzirá exatamente a mesma sequência de números
“aleatórios”; por esse motivo, usei o relógio do sistema para iniciar novas sequências de números
aleatórios e, desse modo, novas sequências de artigos aleatórios. Isso faz com que executar o
programa seja um pouco mais emocionante.
Para os curiosos, o gerador de números pseudoaleatórios de Python usa o algoritmo Mersenne
Twister. Embora gere números aleatórios difíceis de prever e uniformemente distribuídos, ele
exige um pouco do processador. Números aleatórios bons assim não são baratos!
Em seguida, o programa define a função getLinks, que aceita o URL de um
artigo no formato /wiki/..., insere o nome de domínio da Wikipédia,
http://en.wikipedia.org, como prefixo e obtém o objeto BeautifulSoup para o
HTML que está nesse domínio. Então, uma lista de tags com links para
artigos é gerada com base nos parâmetros discutidos antes, e essa lista é
devolvida.
O corpo principal do programa começa definindo uma lista de tags de links
para artigos (a variável links) com a lista de links da página inicial:
https://en.wikipedia.org/wiki/Kevin_Bacon. Em seguida, o código executa
um laço, encontrando uma tag de link aleatória para um artigo na página,
extraindo o atributo href dela, exibindo a página e obtendo uma nova lista
de links do URL extraído.
É claro que um pouco mais de trabalho é necessário para resolver o
problema do Six Degrees of Wikipedia, além de construir um scraper que
ande de página em página. Também é necessário armazenar e analisar os
dados resultantes. Para ver uma continuação da solução desse problema,
leia o Capítulo 6.
pages = set()
random.seed(datetime.datetime.now())
#Obtém uma lista de todos os links internos encontrados em uma página
def getInternalLinks(bs, includeUrl):
includeUrl = '{}://{}'.format(urlparse(includeUrl).scheme,
urlparse(includeUrl).netloc)
internalLinks = []
#Encontra todos os links que começam com "/"
for link in bs.find_all('a',
href=re.compile('^(/|.*'+includeUrl+')')):
if link.attrs['href'] is not None:
if link.attrs['href'] not in internalLinks:
if(link.attrs['href'].startswith('/')):
internalLinks.append(
includeUrl+link.attrs['href'])
else:
internalLinks.append(link.attrs['href'])
return internalLinks
def followExternalOnly(startingSite):
externalLink = getRandomExternalLink(startingSite)
print('Random external link is: {}'.format(externalLink))
followExternalOnly(externalLink)
followExternalOnly('http://oreilly.com')
O programa anterior começa em http://oreilly.com e pula aleatoriamente de
um link externo para outro link externo. Eis um exemplo da saída que ele
gera:
http://igniteshow.com/
http://feeds.feedburner.com/oreilly/news
http://hire.jobvite.com/CompanyJobs/Careers.aspx?c=q319
http://makerfaire.com/
Nem sempre é possível garantir que links externos serão encontrados na
primeira página de um site. Para encontrar links externos nesse caso, um
método semelhante àquele usado no exemplo anterior de rastreamento é
empregado para explorar recursivamente os níveis mais profundos de um
site até que um link externo seja encontrado.
A Figura 3.1 mostra a operação na forma de um fluxograma.
1 Obrigado ao The Oracle of Bacon (http://oracleofbacon.org) por satisfazer minha curiosidade sobre
essa cadeia em particular.
2 Veja o artigo “Exploring a ‘Deep Web’ that Google Can’t Grasp” (http://nyti.ms/2pohZmu,
Explorando a ‘deep web’ que o Google não alcança) de Alex Wright.
3 Veja o artigo “Hacker Lexicon: What is the Dark Web?” (http://bit.ly/2psIw2M, Hacker Lexicon: o
que é a dark web?) de Andy Greenberg.
CAPÍTULO 4
Modelos de web crawling
url = 'https://www.brookings.edu/blog/future-development'
'/2018/01/26/delivering-inclusive-urban-access-3-unc'
'omfortable-truths/'
content = scrapeBrookings(url)
print('Title: {}'.format(content.title))
print('URL: {}\n'.format(content.url))
print(content.body)
url = 'https://www.nytimes.com/2018/01/25/opinion/sunday/'
'silicon-valley-immortality.html"
content = scrapeNYTimes(url)
print('Title: {}'.format(content.title))
print('URL: {}\n'.format(content.url))
print(content.body)
À medida que começamos a acrescentar funções de coleta de dados para
novos sites de notícias, poderemos notar um padrão se formando. Toda
função de parsing de sites faz essencialmente o mesmo:
• seleciona o elemento de título e extrai o texto do título;
• seleciona o conteúdo principal do artigo;
• seleciona outros itens de conteúdo conforme for necessário;
• devolve um objeto Content instanciado com as strings encontradas antes.
As únicas variáveis que realmente dependem do site, nesse caso, são os
seletores CSS usados para obter cada informação. As funções find e find_all
do BeautifulSoup aceitam dois argumentos: uma tag na forma de string e
um dicionário de atributos chave/valor. Assim, podemos passar esses
argumentos como parâmetros que definem a estrutura do próprio site e a
localização dos dados desejados.
Para que tudo fique mais conveniente ainda, em vez de lidar com todos
esses argumentos de tag e pares chave/valor, a função select do
BeautifulSoup pode ser usada com uma única string de seletor CSS para
cada informação que queremos coletar, e podemos colocar todos esses
seletores em um objeto de dicionário:
class Content:
"""
Classe-base comum para todos os artigos/páginas
"""
def __init__(self, url, title, body):
self.url = url
self.title = title
self.body = body
def print(self):
"""
Uma função flexível de exibição controla a saída
"""
print("URL: {}".format(self.url))
print("TITLE: {}".format(self.title))
print("BODY:\n{}".format(self.body))
class Website:
"""
Contém informações sobre a estrutura do site
"""
def __init__(self, name, url, titleTag, bodyTag):
self.name = name
self.url = url
self.titleTag = titleTag
self.bodyTag = bodyTag
Observe que a classe Website não armazena informações coletadas das
páginas individuais, mas instruções sobre como coletar esses dados. Ela
não armazena o título “Título da minha página”. Ela simplesmente
armazena a string de tag h1 que indica o lugar em que os títulos podem ser
encontrados. É por isso que a classe se chama Website (as informações nessa
classe são pertinentes a todo site), e não Content (que contém informações
de apenas uma única página).
Ao usar as classes Content e Website, podemos então escrever um Crawler para
coletar o título e o conteúdo de qualquer URL fornecido para uma dada
página web de um dado site:
import requests
from bs4 import BeautifulSoup
class Crawler:
def getPage(self, url):
try:
req = requests.get(url)
except requests.exceptions.RequestException:
return None
return BeautifulSoup(req.text, 'html.parser')
siteData = [
['O\'Reilly Media', 'http://oreilly.com',
'h1', 'section#product-description'],
['Reuters', 'http://reuters.com', 'h1',
'div.StandardArticleBody_body_1gnLA'],
['Brookings', 'http://www.brookings.edu',
'h1', 'div.post-body'],
['New York Times', 'http://nytimes.com',
'h1', 'p.story-content']
]
websites = []
for row in siteData:
websites.append(Website(row[0], row[1], row[2], row[3]))
crawler.parse(websites[0], 'http://shop.oreilly.com/product/'\
'0636920028154.do')
crawler.parse(websites[1], 'http://www.reuters.com/article/'\
'us-usa-epa-pruitt-idUSKBN19W2D0')
crawler.parse(websites[2], 'https://www.brookings.edu/blog/'\
'techtank/2016/03/01/idea-to-retire-old-methods-of-policy-education/')
crawler.parse(websites[3], 'https://www.nytimes.com/2018/01/'\
'28/business/energy-environment/oil-boom.html')
Embora, à primeira vista, esse novo método não pareça excepcionalmente
mais simples do que escrever uma nova função Python para cada novo site,
pense no que acontecerá se você passar de um sistema com 4 sites para um
sistema com 20 ou 200.
Cada lista de strings é relativamente fácil de escrever. Ela não ocupa muito
espaço e pode ser carregada a partir de um banco de dados ou de um
arquivo CSV. Pode ser importada de uma fonte remota, ou podemos
entregá-la para uma pessoa que não seja um programador, mas tenha
alguma experiência em frontend, a fim de que preencha e acrescente novos
sites, sem que ela jamais tenha de olhar para uma única linha de código.
É claro que a desvantagem é que teremos de abrir mão de certa dose de
flexibilidade. No primeiro exemplo, cada site recebe a própria função em
formato livre, na qual selecionamos e fazemos parse de HTML conforme
necessário a fim de obter o resultado. No segundo exemplo, cada site deve
ter uma determinada estrutura em que se garante que os campos existam,
os dados devem ser limpos quando extraídos do campo e cada campo
desejado deve ter um seletor CSS único e confiável.
No entanto, acredito que a eficácia e a relativa flexibilidade dessa
abordagem mais que compensam suas desvantagens reais ou percebidas.
Na próxima seção, descreveremos aplicações e expansões específicas desse
template básico para que você possa, por exemplo, lidar com campos
ausentes, coletar diferentes tipos de dados, rastrear somente partes
específicas de um site e armazenar informações mais complexas sobre as
páginas.
Estruturando os crawlers
Criar tipos de layout flexíveis e modificáveis para os sites não é muito
vantajoso se ainda tivermos de localizar manualmente cada link do qual
queremos coletar dados. O capítulo anterior mostrou vários métodos de
rastreamento de sites e como encontrar novas páginas de forma
automatizada.
Esta seção descreve como incorporar esses métodos em um web crawler
bem estruturado e expansível, capaz de coletar links e descobrir dados de
modo automatizado. Apresentarei apenas três estruturas básicas de web
crawlers, embora acredite que eles se aplicam à maioria das situações com
as quais provavelmente você vai deparar quando rastrear sites por aí, talvez
com algumas modificações aqui e ali. Caso se veja em uma situação
incomum, com os próprios problemas de rastreamento, também espero
que seja possível usar essas estruturas como inspiração para criar um
design elegante e robusto para o crawler.
crawler = Crawler()
siteData = [
['O\'Reilly Media', 'http://oreilly.com',
'https://ssearch.oreilly.com/?q=','article.product-result',
'p.title a', True, 'h1', 'section#product-description'],
['Reuters', 'http://reuters.com',
'http://www.reuters.com/search/news?blob=',
'div.search-result-content','h3.search-result-title a',
False, 'h1', 'div.StandardArticleBody_body_1gnLA'],
['Brookings', 'http://www.brookings.edu',
'https://www.brookings.edu/search/?s=',
'div.list-content article', 'h4.title a', True, 'h1',
'div.post-body']
]
sites = []
for row in siteData:
sites.append(Website(row[0], row[1], row[2],
row[3], row[4], row[5], row[6], row[7]))
class Content:
def __init__(self, url, title, body):
self.url = url
self.title = title
self.body = body
def print(self):
print("URL: {}".format(self.url))
print("TITLE: {}".format(self.title))
print("BODY:\n{}".format(self.body))
A classe Content é a mesma usada no exemplo do primeiro crawler.
A classe Crawler foi escrita para começar na página inicial do site, localizar
os links internos e fazer parse do conteúdo de cada link interno
encontrado:
import re
class Crawler:
def __init__(self, site):
self.site = site
self.visited = []
Instalando o Scrapy
O Scrapy disponibiliza o download (http://scrapy.org/download/) da
ferramenta em seu site, assim como instruções para instalá-lo com
gerenciadores de terceiros, como o pip.
Por causa do tamanho relativamente grande e da complexidade, o Scrapy
em geral não é um framework possível de instalar do modo tradicional
com:
$ pip install Scrapy
Note que eu disse “em geral” porque, embora seja teoricamente possível,
com frequência deparo com um ou mais problemas complicados de
dependência, incompatibilidade de versões e bugs não resolvidos.
Se você estiver determinado a instalar o Scrapy com o pip, usar um
ambiente virtual (veja a seção “Mantendo as bibliotecas organizadas em
ambientes virtuais” para obter mais informações sobre ambientes virtuais) é
extremamente recomendável.
Meu método preferido de instalação é usar o gerenciador de pacotes
Anaconda (https://docs.continuum.io/anaconda/). O Anaconda é um
produto da empresa Continuum, projetado para reduzir o atrito quando se
trata de encontrar e instalar pacotes Python conhecidos para ciência de
dados. Muitos dos pacotes que ele administra, por exemplo, o NumPy e o
NLTK, serão usados nos próximos capítulos também.
Depois que o Anaconda estiver disponível, o Scrapy poderá ser instalado
com o comando a seguir:
conda install -c conda-forge scrapy
Se você deparar com problemas ou precisar de informações atualizadas,
consulte o guia de instalação do Scrapy
(https://doc.scrapy.org/en/latest/intro/install.html) para obter mais
informações.
Iniciando um novo spider
Depois que o framework Scrapy estiver instalado, é necessária uma
pequena configuração para cada spider. Um spider1 constitui um projeto
Scrapy que, como seu aracnídeo homônimo, é projetado para rastrear
webs. Neste capítulo, uso o termo “spider” para descrever especificamente
um projeto Scrapy, e “crawler” para “qualquer programa genérico que
rastreie a web usando ou não o Scrapy”.
Para criar um novo spider no diretório atual, execute o seguinte na linha de
comando:
$ scrapy startproject wikiSpider
Esse comando cria um novo subdiretório chamado wikiSpider no diretório
em que o projeto foi criado. Nesse diretório, temos a seguinte estrutura de
arquivos:
• scrapy.cfg
• wikiSpider
– spiders
– __init.py__
– items.py
– middlewares.py
– pipelines.py
– settings.py
– __init.py__
Esses arquivos Python são inicializados com código stub, oferecendo um
meio rápido para criar um novo projeto spider. Todas as seções deste
capítulo trabalham com esse projeto wikiSpider.
Criando itens
Até agora, vimos várias maneiras de encontrar, fazer parse e rastrear sites
com o Scrapy, mas este também oferece ferramentas úteis para manter os
itens coletados organizados e armazenados em objetos personalizados,
com campos bem definidos.
Para ajudar a organizar todas as informações coletadas, é necessário criar
um objeto Article. Defina um novo item chamado Article no arquivo
items.py.
Ao abrir o arquivo items.py, ele deverá conter o seguinte:
# -*- coding: utf-8 -*-
class ArticleSpider(CrawlSpider):
name = 'articleItems'
allowed_domains = ['wikipedia.org']
start_urls = ['https://en.wikipedia.org/wiki/Benevolent'
'_dictator_for_life']
rules = [
Rule(LinkExtractor(allow='(/wiki/)((?!:).)*$'),
callback='parse_items', follow=True),
]
def parse_items(self, response):
article = Article()
article['url'] = response.url
article['title'] = response.css('h1::text').extract_first()
article['text'] = response.xpath('//div[@id='
'"mw-content-text"]//text()').extract()
lastUpdated = response.css('li#footer-info-lastmod::text')
.extract_first()
article['lastUpdated'] = lastUpdated.replace('This page was '
'last edited on ', '')
return article
Quando esse arquivo é executado com:
$ scrapy runspider articleItems.py
a saída mostrará os dados usuais de depuração do Scrapy, junto com cada
item de artigo na forma de um dicionário Python:
2018-01-21 22:52:38 [scrapy.spidermiddlewares.offsite] DEBUG:
Filtered offsite request to 'wikimediafoundation.org':
<GET https://wikimediafoundation.org/wiki/Terms_of_Use>
2018-01-21 22:52:38 [scrapy.core.engine] DEBUG: Crawled (200)
<GET https://en.wikipedia.org/wiki/Benevolent_dictator_for_life
# mw-head> (referer: https://en.wikipedia.org/wiki/Benevolent_dictator_for_life)
2018-01-21 22:52:38 [scrapy.core.scraper] DEBUG: Scraped from
<200 https://en.wikipedia.org/wiki/Benevolent_dictator_for_life>
{'lastUpdated': ' 13 December 2017, at 09:26.',
'text': ['For the political term, see ',
'Benevolent dictatorship',
'.',
...
Usar o recurso Items do Scrapy não serve apenas para ter uma boa
organização no código ou dispô-lo de maneira mais legível. Os itens
oferecem muitas ferramentas para apresentação e processamento dos
dados; elas serão discutidas nas próximas seções.
Apresentando itens
O Scrapy usa os objetos Item para determinar quais informações das
páginas visitadas devem ser salvas. Essas informações podem ser salvas
pelo Scrapy em diversos formatos, por exemplo, arquivos CSV, JSON ou
XML, usando os comandos a seguir:
$ scrapy runspider articleItems.py -o articles.csv -t csv
$ scrapy runspider articleItems.py -o articles.json -t json
$ scrapy runspider articleItems.py -o articles.xml -t xml
Cada um desses comandos executa o scraper articleItems e escreve a saída
no formato e no arquivo especificados. O arquivo será criado caso ainda
não exista.
Talvez você tenha notado que, no spider de artigos criado nos exemplos
anteriores, a variável de texto é uma lista de strings, e não uma única
string. Cada string nessa lista representa o texto em um único elemento
HTML, enquanto o conteúdo em <div id="mw-content-text">, do qual estamos
coletando os dados de texto, é composto de vários elementos filhos.
O Scrapy administra bem esses valores mais complexos. No formato CSV,
por exemplo, ele converte listas de strings e escapa todas as vírgulas, de
modo que uma lista de texto é exibida em uma única célula do CSV.
Em XML, cada elemento dessa lista é preservado em tags filhas:
<items>
<item>
<url>https://en.wikipedia.org/wiki/Benevolent_dictator_for_life</url>
<title>Benevolent dictator for life</title>
<text>
<value>For the political term, see </value>
<value>Benevolent dictatorship</value>
...
</text>
<lastUpdated> 13 December 2017, at 09:26.</lastUpdated>
</item>
....
No formato JSON, as listas são preservadas como listas.
É claro que você pode usar os objetos Item por conta própria e escrevê-los
em um arquivo ou banco de dados do modo que quiser, simplesmente
acrescentando o código apropriado na função de parsing no crawler.
Pipeline de itens
Apesar de executar em uma só thread, o Scrapy é capaz de fazer e tratar
várias requisições de modo assíncrono. Isso faz com que ele seja mais
rápido que os scrapers que escrevemos até agora neste livro, embora
sempre acreditei firmemente que mais rápido nem sempre é melhor
quando se trata de web scraping.
O servidor web do site do qual você está tentando coletar dados deve tratar
cada uma dessas requisições, e é importante ser um bom cidadão e avaliar
se esse tipo de carga imposta ao servidor é apropriada (ou se, inclusive, é
uma atitude inteligente em seu interesse próprio, pois muitos sites têm a
capacidade e a disposição para bloquear o que poderiam ver como uma
atividade maliciosa de coleta de dados). Para mais informações sobre o
código de ética no web scraping, assim como sobre a importância de fazer
um throttling apropriado nos scrapers, consulte o Capítulo 18.
Apesar do que foi dito, usar o pipeline de itens do Scrapy pode melhorar
mais ainda a velocidade de seu web scraper, pois todo o processamento de
dados é feito enquanto se espera que as requisições sejam devolvidas, em
vez de esperar que os dados sejam processados antes de fazer outra
requisição. Esse tipo de otimização às vezes é inclusive necessário se o
processamento de dados exigir bastante tempo ou cálculos com intenso
uso do processador tiverem de ser feitos.
Para criar um pipeline de itens, reveja o arquivo settings.py, criado no início
do capítulo. Você verá as seguintes linhas comentadas:
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
# ITEM_PIPELINES = {
# 'wikiSpider.pipelines.WikispiderPipeline': 300,
#}
Remova o caractere de comentário das três últimas linhas, substituindo-as
pelo seguinte:
ITEM_PIPELINES = {
'wikiSpider.pipelines.WikispiderPipeline': 300,
}
Com isso, temos uma classe Python, wikiSpider.pipelines.WikispiderPipeline,
que será usada para processar os dados, assim como um inteiro que
representa a ordem de execução do pipeline se houver várias classes de
processamento. Embora seja possível usar qualquer inteiro nesse caso, os
números de 0 a 1000 são tipicamente utilizados, e serão executados na
ordem crescente.
Agora temos de acrescentar a classe de pipeline e reescrever o spider
original de modo que ele colete dados e o pipeline faça o trabalho pesado
de processá-los. Pode ser tentador fazer o método parse_items do spider
original devolver a resposta e deixar o pipeline criar o objeto Article:
def parse_items(self, response):
return response
No entanto, o framework Scrapy não permite isso, e um objeto Item (por
exemplo, um Article, que estende Item) deve ser devolvido. Assim, o
objetivo de parse_items agora é extrair os dados brutos, fazendo o mínimo
possível de processamento, a fim de que esses dados sejam passados para o
pipeline:
from scrapy.contrib.linkextractors import LinkExtractor
from scrapy.contrib.spiders import CrawlSpider, Rule
from wikiSpider.items import Article
class ArticleSpider(CrawlSpider):
name = 'articlePipelines'
allowed_domains = ['wikipedia.org']
start_urls = ['https://en.wikipedia.org/wiki/Benevolent_dictator_for_life']
rules = [
Rule(LinkExtractor(allow='(/wiki/)((?!:).)*$'),
callback='parse_items', follow=True),
]
def parse_items(self, response):
article = Article()
article['url'] = response.url
article['title'] = response.css('h1::text').extract_first()
article['text'] = response.xpath('//div[@id='
'"mw-content-text"]//text()').extract()
article['lastUpdated'] = response.css('li#'
'footer-info-lastmod::text').extract_first()
return article
Esse arquivo foi salvo no repositório do GitHub como articlePipelines.py.
É claro que agora é necessário associar o arquivo settings.py e o spider
atualizado com o acréscimo do pipeline. Quando o projeto Scrapy foi
iniciado, havia um arquivo em wikiSpider/wikiSpider/settings.py:
# -*- coding: utf-8 -*-
Outros recursos
O Scrapy é uma ferramenta eficaz, que cuida de vários problemas
associados ao rastreamento da web. Ele coleta automaticamente todos os
URLs e os compara em relação a regras predefinidas, garante que todos os
URLs sejam únicos, normaliza URLs relativos quando necessário e oferece
recursão para alcançar níveis mais profundos nas páginas.
Embora este capítulo mal tenha tocado a superfície dos recursos do Scrapy,
incentivo você a consultar a documentação do Scrapy
(https://doc.scrapy.org/en/latest/news.html), bem como o livro Learning
Scrapy de Dimitrios Kouzis-Loukas (O’Reilly,
http://shop.oreilly.com/product/9781784399788.do), que têm um conteúdo
completo sobre o framework.
O Scrapy é uma biblioteca muito grande e abrangente, com vários recursos.
Suas funcionalidades estão bem integradas, mas há muitas áreas de
sobreposição que permitem aos usuários desenvolver o próprio estilo com
facilidade. Se houver algo que você queira fazer com o Scrapy, mas que não
tenha sido mencionado neste capítulo, é provável que haja uma maneira
(ou várias) de fazê-lo!
Embora exibir dados no terminal seja muito divertido, não é muito útil
quando se trata de agregá-los e analisá-los. Para que a maioria dos web
scrapers seja minimamente útil, é necessário poder salvar as informações
que eles coletam.
Este capítulo descreve três métodos principais de gerenciamento de dados
que são suficientes para praticamente qualquer aplicação imaginável. Você
precisa alimentar o backend de um site ou criar a sua própria API?
Provavelmente vai querer que seus scrapers escrevam em um banco de
dados. Precisa de um modo rápido e fácil de coletar documentos da
internet e colocá-los em seu disco rígido? É provável que queira criar um
stream de arquivos para isso. Precisa de alertas ocasionais ou de dados
agregados uma vez ao dia? Envie um email a si mesmo!
Muito além do web scraping, a capacidade de armazenar e de interagir com
grandes volumes de dados é muito importante para praticamente qualquer
aplicação de programação moderna. Com efeito, as informações deste
capítulo são necessárias para implementar vários dos exemplos das futuras
seções do livro. Recomendo que você pelo menos leia rapidamente este
capítulo caso não tenha familiaridade com a armazenagem automática de
dados.
Arquivos de mídia
Há duas maneiras principais de armazenar arquivos de mídia: por
referência e fazendo download do arquivo propriamente dito. Podemos
armazenar um arquivo por referência guardando o URL em que está o
arquivo. Essa opção tem as seguintes vantagens:
• Os scrapers executam mais rápido e exigem muito menos banda
quando não precisam fazer download dos arquivos.
• Você economiza espaço em suas próprias máquinas ao armazenar
somente os URLs.
• É mais fácil escrever um código que armazene somente URLs e não
tenha de lidar com downloads adicionais de arquivos.
• Podemos reduzir a carga no servidor host ao evitar downloads de
arquivos grandes.
Eis as desvantagens:
• Incluir esses URLs em seu próprio site ou aplicação é conhecido como
hotlinking, e fazer isso é uma maneira rápida de se colocar em uma
situação problemática na internet.
• Você não deve usar os ciclos de servidor de outra pessoa para hospedar
mídias das próprias aplicações.
• O arquivo hospedado em qualquer URL em particular está sujeito a
mudança. Isso pode ter efeitos constrangedores se, por exemplo, você
estiver incluindo um hotlink para uma imagem em um blog público. Se
estiver armazenando os URLs com o intuito de obter o arquivo mais
tarde para pesquisas no futuro, em algum momento ele poderá deixar de
existir ou mudar para algo totalmente irrelevante.
• Navegadores web de verdade não requisitam simplesmente o HTML de
uma página e seguem adiante. Eles também fazem download de todos os
recursos necessários à página. Fazer download de arquivos pode ajudar a
fazer seu scraper parecer um ser humano navegando no site, e isso pode
ser uma vantagem.
Se estiver em dúvida quanto a armazenar um arquivo ou um URL em um
arquivo, pergunte a si mesmo se é mais provável que você visualize ou leia
esse arquivo mais de uma ou duas vezes, ou se esse banco de dados de
arquivos ficará parado, acumulando poeira eletrônica na maior parte de
sua vida. Se a resposta for a última opção, provavelmente será melhor
apenas armazenar o URL. Se for a primeira, continue lendo!
A biblioteca urllib, usada para obter o conteúdo de páginas web, também
contém funções para obter o conteúdo de arquivos. O programa a seguir
usa urllib.request.urlretrieve para fazer download de imagens de um URL
remoto:
from urllib.request import urlretrieve
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('http://www.pythonscraping.com')
bs = BeautifulSoup(html, 'html.parser')
imageLocation = bs.find('a', {'id': 'logo'}).find('img')['src']
urlretrieve (imageLocation, 'logo.jpg')
Esse código faz o download do logo de http://pythonscraping.com e o
armazena como logo.jpg no mesmo diretório em que o script está
executando.
O código funciona bem se for necessário fazer o download de apenas um
arquivo e você souber como ele se chama e qual a sua extensão. A maioria
dos scrapers, porém, não faz download de um só arquivo e dá o trabalho
por encerrado. Os downloads a seguir são de arquivos internos, associados
ao atributo src de qualquer tag, da página inicial de
http://pythonscraping.com:
import os
from urllib.request import urlretrieve
from urllib.request import urlopen
from bs4 import BeautifulSoup
downloadDirectory = 'downloaded'
baseUrl = 'http://pythonscraping.com'
def getAbsoluteURL(baseUrl, source):
if source.startswith('http://www.'):
url = 'http://{}'.format(source[11:])
elif source.startswith('http://'):
url = source
elif source.startswith('www.'):
url = source[4:]
url = 'http://{}'.format(source)
else:
url = '{}/{}'.format(baseUrl, source)
if baseUrl not in url:
return None
return url
def getDownloadPath(baseUrl, absoluteUrl, downloadDirectory):
path = absoluteUrl.replace('www.', '')
path = path.replace(baseUrl, '')
path = downloadDirectory+path
directory = os.path.dirname(path)
if not os.path.exists(directory):
os.makedirs(directory)
return path
html = urlopen('http://www.pythonscraping.com')
bs = BeautifulSoup(html, 'html.parser')
downloadList = bs.findAll(src=True)
for download in downloadList:
fileUrl = getAbsoluteURL(baseUrl, download['src'])
if fileUrl is not None:
print(fileUrl)
html = urlopen('http://en.wikipedia.org/wiki/'
'Comparison_of_text_editors')
bs = BeautifulSoup(html, 'html.parser')
# A tabela principal de comparação é atualmente a primeira tabela da página
table = bs.findAll('table',{'class':'wikitable'})[0]
rows = table.findAll('tr')
csvFile = open('editors.csv', 'wt+')
writer = csv.writer(csvFile)
try:
for row in rows:
csvRow = []
for cell in row.findAll(['td', 'th']):
csvRow.append(cell.get_text())
writer.writerow(csvRow)
finally:
csvFile.close()
MySQL
MySQL (pronunciado oficialmente como “mai esse-que-ele,” embora
muitos digam, “mai siquel”) é o sistema de gerenciamento de banco de
dados relacional de código aberto mais conhecido hoje em dia. De modo
um pouco incomum para um projeto de código aberto com grandes
concorrentes, a popularidade do MySQL, historicamente, vem sendo
disputada palmo a palmo com dois outros grandes sistemas de banco de
dados de código fechado: o SQL Server da Microsoft e o DBMS da Oracle.
Sua popularidade não é desprovida de razão. Para a maioria das aplicações,
é difícil cometer um erro caso a escolha seja o MySQL. É um DBMS
escalável, robusto e completo, usado por grandes sites: o YouTube1, o
Twitter2 e o Facebook3, entre vários outros.
Por causa de sua ubiquidade, do preço (“gratuito” é um ótimo preço) e da
usabilidade imediata, o MySQL é um banco de dados incrível para projetos
de web scraping e será usado no resto deste livro.
Banco de dados “relacional”?
Dados relacionais são dados que apresentam relações. Fico feliz por ter esclarecido essa questão!
Brincadeirinha! Quando falam de dados relacionais, os cientistas da computação estão se
referindo a dados que não existem de forma isolada – são dados cujas propriedades os
relacionam a outras porções de dados. Por exemplo, “O Usuário A estuda na Instituição B”, em
que o Usuário A pode ser encontrado na tabela de Usuários do banco de dados e a Instituição B
pode ser encontrada na tabela de Instituições.
Mais adiante neste capítulo, veremos como modelar diferentes tipos de relações e armazenar
dados no MySQL (ou em qualquer outro banco de dados relacional) de modo eficaz.
Instalando o MySQL
Se o MySQL for uma novidade para você, instalar um banco de dados pode
parecer um pouco intimidador (se já tiver experiência nisso, sinta-se à
vontade para ignorar esta seção). Na verdade, é tão simples quanto instalar
praticamente qualquer outro tipo de software. Em seu núcleo, o MySQL
funciona com um conjunto de arquivos de dados, armazenados em um
servidor ou no computador local, contendo todas as informações
guardadas no banco de dados. A camada de software do MySQL acima
desses dados oferece uma maneira conveniente de interagir com os dados
por meio de uma interface de linha de comando. Por exemplo, o comando
a seguir percorre os arquivos de dados e devolve uma lista de todos os
usuários no banco de dados cujo primeiro nome seja “Ryan”:
SELECT * FROM users WHERE firstname = "Ryan"
Se você estiver em uma distribuição Linux baseada em Debian (ou em um
sistema com apt-get), instalar o MySQL é simples e basta executar o
seguinte:
$ sudo apt-get install mysql-server
Preste atenção no processo de instalação, aprove os requisitos de memória
e insira uma nova senha para o novo usuário root quando for solicitada.
No macOS e no Windows, a situação é um pouco mais complicada. Caso
ainda não tenha uma conta Oracle, crie uma antes de fazer download do
pacote.
Se estiver no macOS, é necessário obter antes o pacote de instalação
(http://dev.mysql.com/downloads/mysql/).
Selecione o pacote .dmg e faça login com sua conta Oracle – ou crie uma –
para fazer download do arquivo. Depois de abri-lo, você receberá
orientações de um assistente de instalação razoavelmente simples (Figure
6.1).
Os passos default da instalação devem ser suficientes; neste livro, partirei
do pressuposto de que você tem uma instalação MySQL default.
É claro que essa tabela ainda está vazia. Podemos inserir dados de teste na
tabela pages com a linha a seguir:
> INSERT INTO pages (title, content) VALUES ("Test page title",
"This is some test page content. It can be up to 10,000 characters long.");
Perceba que, apesar de a tabela ter quatro colunas (id, title, content,
created), é necessário definir apenas duas delas (title e content) para inserir
uma linha. Isso ocorre porque a coluna id é incrementada de modo
automático (o MySQL soma 1 automaticamente a cada vez que uma nova
linha é inserida) e, em geral, é capaz de cuidar de si mesma. Além disso, a
coluna timestamp foi definida para conter o horário atual como default.
É claro que podemos sobrescrever esses defaults:
> INSERT INTO pages (id, title, content, created) VALUES (3,
"Test page title",
"This is some test page content. It can be up to 10,000 characters
long.", "2014-09-21 10:25:32");
Desde que o inteiro fornecido para a coluna id não exista ainda no banco
de dados, essa sobrescrita funcionará perfeitamente. No entanto, fazer isso
em geral não é uma boa prática; é melhor deixar que o MySQL cuide das
colunas id e timestamp, a menos que haja um motivo convincente para
proceder de outra forma.
Agora que temos alguns dados na tabela, podemos usar vários métodos
para selecioná-los. Eis alguns exemplos de instruções SELECT:
> SELECT * FROM pages WHERE id = 2;
Essa instrução diz o seguinte ao MySQL: “Selecione tudo de pages cujo id
seja 2”. O asterisco (*) atua como um caractere-curinga, devolvendo todas
as linhas em que a cláusula (where id equals 2) é verdadeira. A instrução
devolve a segunda linha da tabela, ou um resultado vazio se não houver
nenhuma linha cujo id seja 2. Por exemplo, a consulta a seguir, que não
diferencia letras maiúsculas de minúsculas, devolve todas as linhas cujo
campo title contenha “test” (o símbolo % atua como um caractere-curinga
em strings MySQL):
> SELECT * FROM pages WHERE title LIKE "%test%";
O que aconteceria se tivéssemos uma tabela com várias colunas e
quiséssemos somente que uma porção específica dos dados fosse
devolvida? Em vez de selecionar tudo, podemos executar um comando
como:
> SELECT id, title FROM pages WHERE content LIKE "%page content%";
Essa instrução devolve id e title quando o conteúdo contiver a expressão
“page content”.
Instruções DELETE têm praticamente a mesma sintaxe das instruções SELECT:
> DELETE FROM pages WHERE id = 1;
Por esse motivo, é uma boa ideia, sobretudo quando trabalhamos com
bancos de dados importantes, impossíveis de serem facilmente restaurados,
escrever qualquer instrução DELETE como uma instrução SELECT antes (nesse
caso, SELECT * FROM pages WHERE id = 1), testar para garantir que apenas as
linhas que queremos apagar serão devolvidas, e então substituir SELECT *
por DELETE. Muitos programadores contam histórias terríveis sobre erros na
cláusula de uma instrução DELETE – ou, pior ainda, sobre como se
esqueceram totalmente de defini-la por estarem com pressa – arruinando
os dados do cliente. Não deixe que isso aconteça com você!
Precauções semelhantes devem ser tomadas com instruções UPDATE:
> UPDATE pages SET title="A new title",
content="Some new content" WHERE id=2;
Neste livro, trabalharemos apenas com instruções MySQL simples, fazendo
seleções, inserções e atualizações básicas. Se você estiver interessado em
conhecer outros comandos e técnicas relacionadas a essa ferramenta de
banco de dados eficaz, recomendo o livro MySQL Cookbook de Paul
DuBois (O’Reilly, http://shop.oreilly.com/product/0636920032274.do).
def getLinks(articleUrl):
html = urlopen('http://en.wikipedia.org'+articleUrl)
bs = BeautifulSoup(html, 'html.parser')
title = bs.find('h1').get_text()
content = bs.find('div', {'id':'mw-content-text'}).find('p')
.get_text()
store(title, content)
return bs.find('div', {'id':'bodyContent'}).findAll('a',
href=re.compile('^(/wiki/)((?!:).)*$'))
links = getLinks('/wiki/Kevin_Bacon')
try:
while len(links) > 0:
newArticle = links[random.randint(0, len(links)-1)].attrs['href']
print(newArticle)
links = getLinks(newArticle)
finally:
cur.close()
conn.close()
Há alguns detalhes a serem observados nesse caso: em primeiro lugar,
"charset='utf8'" foi adicionado na string de conexão com o banco de dados.
Isso diz à conexão que ela deve enviar todas as informações ao banco de
dados como UTF-8 (e, é claro, o banco de dados já deve estar configurado
para tratá-las).
Em segundo lugar, observe o acréscimo de uma função store. Ela aceita
duas variáveis do tipo string, title e content, e as adiciona em uma instrução
INSERT executada pelo cursor e cujo commit é feito pela conexão do cursor.
Esse é um excelente exemplo da separação entre o cursor e a conexão;
embora o cursor tenha armazenado informações sobre o banco de dados e
o seu próprio contexto, ele deve atuar por meio da conexão para enviar
informações de volta ao banco de dados e inserir informações.
Por fim, vemos que uma instrução finally foi adicionada no laço principal
do programa, no final do código. Isso garante que, não importa como o
programa seja interrompido ou as exceções sejam lançadas durante a
execução (considerando que a web é confusa, sempre suponha que
exceções serão lançadas), o cursor e a conexão serão ambos fechados de
imediato antes que o programa termine. É uma boa ideia incluir uma
instrução try...finally como essa sempre que estiver coletando dados da
web e houver uma conexão aberta com o banco de dados.
Apesar de o PyMySQL não ser um pacote enorme, há um número razoável
de funções úteis que não foram descritas neste livro. Você pode conferir a
documentação (https://pymysql.readthedocs.io/en/latest/) no site do
PyMySQL.
Um índice poderia muito bem ser adicionado nessa tabela (além do índice
já esperado com base no id) na coluna definition para que as consultas
nessa coluna sejam mais rápidas. Lembre-se, porém, de que adicionar
indexação exige mais espaço para o novo índice bem como tempo
adicional de processamento para a inserção de novas linhas. Em especial
quando lidamos com grandes volumes de dados, devemos considerar com
cuidado o custo-benefício entre seus índices e o volume a ser indexado.
Para deixar esse índice das “definições” mais leve, podemos dizer ao
MySQL que indexe apenas os primeiros caracteres do valor da coluna. O
comando a seguir cria um índice com os 16 primeiros caracteres do campo
definition:
CREATE INDEX definition ON dictionary (id, definition(16));
Esse índice deixará suas consultas mais rápidas quando procurarmos
palavras com base em sua definição completa (sobretudo se os 16
primeiros caracteres dos valores da definição tiverem a tendência de ser
bem diferentes uns dos outros), sem exigir muito no tocante a espaço extra
ou tempo prévio de processamento.
Quando se trata de tempo de consulta versus tamanho do banco de dados
(uma das tarefas fundamentais relacionadas a equilíbrio na engenharia de
banco de dados), um dos erros mais comuns, particularmente com web
scraping envolvendo grandes volumes de textos em línguas naturais, é
armazenar muitos dados repetidos. Por exemplo, suponha que queremos
contabilizar a frequência de determinadas expressões que apareçam em
vários sites. Essas expressões podem ser encontradas em uma dada lista ou
ser automaticamente geradas por um algoritmo de análise de textos.
Poderíamos ficar tentados a armazenar os dados de modo semelhante a
este:
Email
Assim como as páginas web são enviadas por meio de HTTP, um email é
enviado via SMTP (Simple Mail Transfer Protocol, ou Protocolo Simples de
Transferência de Correio). E, assim como usamos um cliente de servidor
web para cuidar do envio de páginas web via HTTP, os servidores usam
diversos clientes de email, como Sendmail, Postfix ou Mailman para enviar
e receber emails.
Embora enviar email com Python seja relativamente simples, isso exige que
você tenha acesso a um servidor executando SMTP. Configurar um cliente
SMTP em seu servidor ou computador local é complicado e está fora do
escopo deste livro, mas vários recursos excelentes podem ajudar nessa
tarefa, em particular se você estiver executando Linux ou macOS.
Os códigos de exemplo a seguir partem do pressuposto de que você está
executando um cliente SMTP localmente. (Para modificar este código e
usar um cliente SMTP remoto, mude localhost para o endereço de seu
servidor remoto.)
Enviar um email com Python exige apenas nove linhas de código:
import smtplib
from email.mime.text import MIMEText
sendMail('It\'s Christmas!',
'According to http://itischristmas.com, it is Christmas!')
Esse script em particular verifica o site https://isitchristmas.com (cuja
característica principal é apresentar um YES ou NO gigante, dependendo
do dia do ano) uma vez por hora. Se algo diferente de NO for visto, você
receberá um email avisando que é Natal.
Embora esse programa em particular talvez não pareça mais útil que um
calendário pendurado na parede, ele pode ser um pouco ajustado para
fazer diversas tarefas extremamente úteis. O programa pode enviar emails
de alerta em resposta a interrupções de serviço em sites, a falhas em testes
ou até mesmo em resposta à disponibilidade de um produto que você
estava esperando na Amazon, cujo estoque havia se esgotado – seu
calendário na parede não é capaz de fazer nada disso.
1 Joab Jackson, “YouTube Scales MySQL with Go Code” (YouTube escala MySQL com código Go,
http://bit.ly/1LWVmc8), PCWorld, 15 de dezembro de 2012.
2 Jeremy Cole e Davi Arnaut, “MySQL at Twitter” (MySQL no Twitter, http://bit.ly/1KHDKns), The
Twitter Engineering Blog, 9 de abril de 2012.
3 ”MySQL and Database Engineering: Mark Callaghan” (MySQL e engenharia de banco de dados:
Mark Callaghan, http://on.fb.me/1RFMqvw), Facebook Engineering, 4 de março de 2012.
PARTE II
Coleta de dados avançada
Codificação de documentos
A codificação de um documento diz às aplicações – sejam elas o sistema
operacional de seu computador ou o seu próprio código Python – como ele
deve ser lido. Em geral, essa codificação pode ser deduzida a partir da
extensão do arquivo, embora essa extensão não seja exigida pela
codificação. Por exemplo, eu poderia salvar minhaImagem.jpg como
minhaImagem.txt sem que houvesse problemas – pelo menos até que meu
editor de texto tentasse abri-lo. Felizmente, essa situação é rara e a
extensão de arquivo de um documento em geral é tudo que é preciso saber
para lê-lo de forma correta.
No nível básico, todos os documentos são codificados com 0s e 1s. Acima
disso, os algoritmos de codificação definem detalhes como “quantos bits
por caractere” ou “quantos bits representam a cor de cada pixel” (no caso
de arquivos de imagens). No próximo nível, pode haver uma camada de
compactação ou algum algoritmo para redução de espaço, como no caso
dos arquivos PNG.
Embora lidar com arquivos que não sejam HTML pareça assustador à
primeira vista, fique tranquilo porque, com a biblioteca correta, Python
estará equipado de forma adequada para lidar com qualquer formato de
informações que você queira lhe passar. A única diferença entre um
arquivo-texto, um arquivo de vídeo e um arquivo de imagem é o modo
como seus 0s e 1s são interpretados. Este capítulo descreve vários tipos de
arquivos comuns: texto, CSV, PDFs e documentos Word.
Note que, fundamentalmente, todos esses arquivos armazenam texto. Para
obter informações sobre como trabalhar com imagens, recomendo ler este
capítulo para se habituar com diferentes tipos de arquivos e aprender a
armazená-los, e então consulte o Capítulo 13, que contém mais
informações sobre processamento de imagens!
Texto
De certo modo, não é comum ter arquivos online armazenados como texto
simples, mas é frequente que sites antigos ou mais simples tenham
repositórios grandes de arquivos-texto. Por exemplo, o IETF (Internet
Engineering Task Force, ou Força-tarefa de Engenharia da Internet)
armazena todos os seus documentos publicados como HTML, PDF e
arquivos-texto (veja https://www.ietf.org/rfc/rfc1149.txt como exemplo). A
maioria dos navegadores não terá problemas para exibir esses arquivos-
texto, e você poderá coletar seus dados.
Para documentos mais básicos em formato texto, por exemplo, o arquivo
usado como exercício em
http://www.pythonscraping.com/pages/warandpeace/chapter1.txt, podemos
usar o método a seguir:
from urllib.request import urlopen
textPage = urlopen('http://www.pythonscraping.com/'\
'pages/warandpeace/chapter1.txt')
print(textPage.read())
Em geral, quando obtemos uma página com urlopen, nós a transformamos
em um objeto BeautifulSoup para fazer parse do HTML. Nesse caso,
podemos ler a página de forma direta. Transformá-la em um objeto
BeautifulSoup, embora seja perfeitamente possível, seria contraproducente
– não há nenhum HTML para fazer parse, portanto a biblioteca seria inútil.
Depois que o arquivo-texto é lido na forma de string, basta analisá-lo como
faríamos com qualquer string lida em Python. Está claro que a
desvantagem, nesse caso, está no fato de não podermos usar tags HTML
como pistas do contexto para saber onde estão os textos que realmente
queremos e excluir os textos indesejados. Pode ser um desafio se
estivermos tentando extrair determinadas informações dos arquivos-texto.
textPage = urlopen('http://www.pythonscraping.com/'\
'pages/warandpeace/chapter1-ru.txt')
print(str(textPage.read(), 'utf-8'))
O código terá o seguinte aspecto se esse conceito for usado com o
BeautifulSoup e Python 3.x:
html = urlopen('http://en.wikipedia.org/wiki/Python_(programming_language)')
bs = BeautifulSoup(html, 'html.parser')
content = bs.find('div', {'id':'mw-content-text'}).get_text()
content = bytes(content, 'UTF-8')
content = content.decode('UTF-8')
Python 3.x codifica todos os caracteres em UTF-8 por padrão. Você pode
se sentir tentado a deixar de lado essa questão e usar a codificação UTF-8
em todo web scraper que escrever. Afinal de contas, o UTF-8 também
lidará tranquilamente tanto com caracteres ASCII quanto com idiomas
estrangeiros. No entanto, é importante lembrar dos 9% de sites por aí que
usam alguma versão da codificação ISO também, portanto não será
possível evitar esse problema por completo.
Infelizmente, no caso de documentos de texto, é impossível determinar a
codificação usada por um documento de forma concreta. Algumas
bibliotecas são capazes de analisar o documento e dar um bom palpite
(usando um pouco de lógica para perceber que “раÑ?Ñ?казє
provavelmente não é uma palavra); porém, muitas vezes, estarão erradas.
Felizmente, no caso de páginas HTML, em geral a codificação está contida
em uma tag que se encontra na seção <head> do site. A maioria dos sites, em
particular os sites em inglês, têm esta tag:
<meta charset="utf-8" />
Por outro lado, o site da ECMA International (http://www.ecma-
international.org/) tem esta tag3:
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
Se você planeja fazer muito web scraping, particularmente de sites
internacionais, procurar essa tag meta e usar a codificação recomendada
por ela ao ler o conteúdo da página pode ser uma atitude inteligente.
CSV
Ao fazer web scraping, é provável que você encontre um arquivo CSV ou
um colega de trabalho que goste de dados formatados dessa maneira.
Felizmente, Python tem uma biblioteca incrível
(https://docs.python.org/3.4/library/csv.html) tanto para ler quanto para
escrever arquivos CSV. Embora essa biblioteca seja capaz de lidar com
muitas variações de CSV, esta seção tem como foco principal o formato
padrão. Se houver algum caso especial com o qual você tenha de lidar,
consulte a documentação.
data = urlopen('http://pythonscraping.com/files/MontyPythonAlbums.csv')
.read().decode('ascii', 'ignore')
dataFile = StringIO(data)
csvReader = csv.reader(dataFile)
for row in csvReader:
print(row)
Eis a saída do código:
['Name', 'Year']
["Monty Python's Flying Circus", '1970']
['Another Monty Python Record', '1971']
["Monty Python's Previous Record", '1972']
...
Como podemos ver a partir do código de exemplo, o objeto de leitura
devolvido por csv.reader é iterável e é composto de objetos lista de Python.
Por causa disso, cada linha do objeto csvReader é acessível da seguinte
maneira:
for row in csvReader:
print('The album "'+row[0]+'" was released in '+str(row[1]))
Eis a saída:
The album "Name" was released in Year
The album "Monty Python's Flying Circus" was released in 1970
The album "Another Monty Python Record" was released in 1971
The album "Monty Python's Previous Record" was released in 1972
...
Observe a primeira linha: The album "Name" was released in Year. Embora esse
seja um resultado que pode ser ignorado com facilidade ao escrever um
código de exemplo, você não iria querer que ele fosse incluído em seus
dados no mundo real. Um programador mais inexperiente poderia
simplesmente ignorar a primeira linha do objeto csvReader, ou escrever um
caso especial para tratá-la. Felizmente, uma alternativa à função csv.reader
cuida de tudo isso para você de modo automático. Entra em cena um
DictReader:
from urllib.request import urlopen
from io import StringIO
import csv
data = urlopen('http://pythonscraping.com/files/MontyPythonAlbums.csv')
.read().decode('ascii', 'ignore')
dataFile = StringIO(data)
dictReader = csv.DictReader(dataFile)
print(dictReader.fieldnames)
for row in dictReader:
print(row)
devolve os valores de cada linha do arquivo CSV na forma de
csv.DictReader
objetos dicionário, em vez de objetos lista, com os nomes dos campos
armazenados na variável dictReader.fieldnames e como chaves em cada objeto
dicionário:
['Name', 'Year']
{'Name': 'Monty Python's Flying Circus', 'Year': '1970'}
{'Name': 'Another Monty Python Record', 'Year': '1971'}
{'Name': 'Monty Python's Previous Record', 'Year': '1972'}
A desvantagem, evidentemente, é que demora um pouco mais para criar,
processar e exibir esses objetos DictReader, em comparação com csvReader,
porém a conveniência e a usabilidade muitas vezes compensam o overhead
adicional. Tenha em mente também que, quando se trata de web scraping,
o overhead necessário para requisitar e obter dados dos sites de um
servidor externo quase sempre será o fator limitante inevitável para
qualquer programa que você escrever, portanto preocupar-se com qual
técnica proporcionará uma redução de alguns microssegundos no tempo
total de execução muitas vezes será uma questão discutível!
PDF
Como usuária de Linux, sei do sofrimento de receber um arquivo .docx que
meu software que não é Microsoft mutila, e da luta para tentar encontrar
os codecs para interpretar algum novo formato de mídia da Apple. Em
certos aspectos, a Adobe foi revolucionária ao criar seu PDF (Portable
Document Format, ou Formato de Documento Portável) em 1993. Os
PDFs permitiram que usuários de diferentes plataformas vissem imagens e
documentos de texto exatamente do mesmo modo, não importando a
plataforma em que fossem visualizados.
Embora armazenar PDFs na web seja, de certo modo, ultrapassado (por
que armazenar conteúdo em um formato estático, lento para carregar,
quando ele poderia ser escrito em HTML?), os PDFs continuam presentes
em todos os lugares, particularmente quando lidamos com formulários
oficiais e arquivamentos.
Em 2009, um britânico chamado Nick Innes foi destaque nos noticiários
quando solicitou informações sobre resultados de testes de estudantes da
rede pública ao Buckinghamshire City Council (Câmara Municipal de
Buckinghamshire), disponíveis por conta da Freedom of Information Act
(Lei da Liberdade de Informação) do Reino Unido. Depois de algumas
requisições e recusas repetidas, ele finalmente recebeu a informação que
procurava – na forma de 184 documentos PDF.
Embora Innes tenha persistido e, em algum momento, recebido um banco
de dados formatado de modo mais apropriado, se fosse expert em web
scraper, provavelmente teria evitado muito desperdício de tempo nos
tribunais e usado os documentos PDF de forma direta, com um dos muitos
módulos de parsing de PDF de Python.
Infelizmente, muitas das bibliotecas de parsing de PDF disponíveis para
Python 2.x não foram atualizadas com o lançamento de Python 3.x.
Entretanto, como o PDF é um formato de documento relativamente
simples e de código aberto, muitas bibliotecas Python boas, mesmo em
Python 3.x, são capazes de lê-lo.
O PDFMiner3K é uma dessas bibliotecas relativamente fáceis de usar. É
flexível e pode ser usada na linha de comando ou integrada com um código
existente. Também é capaz de lidar com várias codificações de idiomas –
mais uma vez, é um recurso que, com frequência, é conveniente na web.
Podemos instalar a biblioteca como de costume, usando pip ou fazendo
download do módulo Python em https://pypi.org/project/pdfminer3k/;
instale-o descompactando a pasta e executando o seguinte comando:
$ python setup.py install
A documentação está disponível em /pdfminer3k-1.3.0/docs/index.html na
pasta extraída, embora a documentação atual esteja mais voltada para a
interface de linha de comando do que uma integração com código Python.
Eis uma implementação básica que permite ler PDFs arbitrários em uma
string, dado um objeto de arquivo local:
from urllib.request import urlopen
from pdfminer.pdfinterp import PDFResourceManager, process_pdf
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from io import StringIO
from io import open
def readPDF(pdfFile):
rsrcmgr = PDFResourceManager()
retstr = StringIO()
laparams = LAParams()
device = TextConverter(rsrcmgr, retstr, laparams=laparams)
process_pdf(rsrcmgr, device, pdfFile)
device.close()
content = retstr.getvalue()
retstr.close()
return content
pdfFile = urlopen('http://pythonscraping.com/'
'pages/warandpeace/chapter1.pdf')
outputString = readPDF(pdfFile)
print(outputString)
pdfFile.close()
Esse código tem como saída o texto simples que já conhecemos:
CHAPTER I
"Well, Prince, so Genoa and Lucca are now just family estates of
the Buonapartes. But I warn you, if you don't tell me that this
means war, if you still try to defend the infamies and horrors
perpetrated by that Antichrist- I really believe he is Antichrist- I will
O aspecto interessante sobre esse leitor de PDF é que, se estivermos
trabalhando com arquivos locais, podemos substituir um objeto comum de
arquivo Python pelo objeto devolvido por urlopen e usar esta linha:
pdfFile = open('../pages/warandpeace/chapter1.pdf', 'rb')
A saída talvez não seja perfeita, em especial para PDFs com imagens, textos
formatados de maneira inusitada ou organizados em tabelas ou gráficos.
No entanto, para a maioria dos PDFs contendo apenas texto, a saída não
deverá ser diferente de um PDF que fosse um arquivo-texto.
1 Esse bit de “preenchimento” voltará a nos assombrar com os padrões ISO um pouco mais adiante.
2 De acordo com a W3Techs (http://w3techs.com/technologies/history_overview/character_encodin),
que usa web crawlers para obter esses tipos de estatística.
3 A ECMA era uma das colaboradoras originais do padrão ISO, portanto não é nenhuma surpresa
que seu site esteja codificado com uma variante do ISO.
CAPÍTULO 8
Limpando dados sujos
Normalização de dados
Todo mundo já deparou com um formulário web com design ruim: “Insira
o seu número de telefone. O número do telefone deve estar no formato
‘xxx-xxx-xxxx’”.
Como um bom programador, é provável que você pense consigo mesmo:
“Por que eles simplesmente não removem os caracteres não numéricos que
eu inserir e fazem isso eles mesmos?”. Normalização de dados é o processo
de garantir que strings equivalentes do ponto de vista linguístico ou lógico,
por exemplo, os números de telefone (555) 123-4567 e 555-123-4567,
sejam exibidos – ou pelo menos comparados – como equivalentes.
Usando o código de n-grama da seção anterior, recursos para normalização
de dados podem ser acrescentados.
Um problema evidente desse código é que ele contém vários bigramas
duplicados. Todo bigrama encontrado pelo código é adicionado à lista,
sem que sua frequência seja registrada. Não só é interessante anotar a
frequência desses bigramas, em vez de registrar apenas a sua existência,
como também esses dados podem ser convenientes para colocar os efeitos
das mudanças nos algoritmos de limpeza e de normalização de dados em
um gráfico. Se os dados forem normalizados com sucesso, o número total
de n-gramas únicos será reduzido, enquanto o contador total de n-gramas
encontrados (isto é, o número de itens únicos e não únicos identificados
como n-gramas) não diminuirá. Em outras palavras, haverá menos
“buckets” para o mesmo número de n-gramas.
Podemos fazer isso modificando o código que coleta os n-gramas para que
estes sejam adicionados em um objeto Counter, e não em uma lista:
from collections import Counter
def getNgrams(content, n):
content = cleanInput(content)
ngrams = Counter()
for sentence in content:
newNgrams = [' '.join(ngram) for ngram in
getNgramsFromSentence(sentence, 2)]
ngrams.update(newNgrams)
return(ngrams)
Há várias maneiras diferentes de fazer isso, por exemplo, adicionar n-
gramas em um objeto dicionário no qual o valor da lista aponte para um
contador do número de vezes em que um n-grama foi visto. Essa solução
tem a desvantagem de exigir um pouco mais de gerenciamento e complicar
a ordenação. No entanto, usar um objeto Counter também tem uma
desvantagem: ele não é capaz de armazenar listas (não é possível ter hashes
com listas), portanto é necessário fazer antes uma conversão para strings
usando um ' '.join(ngram) em uma list comprehension para cada n-grama.
Eis o resultado:
Counter({'Python Software': 37, 'Software Foundation': 37, 'of the': 34,
'of Python': 28, 'in Python': 24, 'in the': 23, 'van Rossum': 20, 'to the':
20, 'such as': 19, 'Retrieved February': 19, 'is a': 16, 'from the': 16,
'Python Enhancement': 15,...
Quando escrevi este livro, havia um total de 7.275 bigramas e 5.628
bigramas únicos; o bigrama mais popular era “Software Foundation”,
seguido de “Python Software”. No entanto, a análise do resultado mostra
que “Python Software” aparece na forma de “Python software” mais duas
vezes. De modo semelhante, “van Rossum” e “Van Rossum” aparecem de
forma separada na lista.
Acrescentar a linha:
content = content.upper()
na função cleanInput mantém constante o número total de bigramas
encontrado, 7.275, enquanto reduz o número de bigramas únicos para
5.479.
Além disso, em geral é bom parar e considerar a capacidade de
processamento que você está disposto a usar na normalização dos dados.
Há uma série de situações em que palavras grafadas de modo diferente são
equivalentes, mas, para resolver essa equivalência, é necessário executar
uma verificação em cada uma das palavras a fim de ver se ela corresponde a
algum de seus equivalentes previamente programados.
Por exemplo, tanto “Python 1st” quanto “Python first” aparecem na lista
de bigramas. No entanto, fazer uma regra abrangente que determine que
“todo first, second, third etc. será resolvido para 1st, 2nd, 3rd etc. (ou vice-
versa)” resultaria em cerca de dez verificações adicionais por palavra.
De modo semelhante, o uso inconsistente de hifens (“co-ordinated” versus
“coordinated”), erros de ortografia e outras incongruências das línguas
naturais afetarão o agrupamento de n-gramas e poderão deturpar o
resultado caso as incongruências sejam muito comuns.
Uma solução, no caso de palavras com hifens, seria removê-los totalmente
e tratar a palavra como uma única string, o que exigiria apenas uma
operação. Contudo, isso significaria também que expressões com hifens
(uma ocorrência comum em inglês, como em all-too-common occurrence)
seriam tratadas como uma única palavra. Por outro lado, tratar hifens
como espaços poderia ser uma solução mais apropriada. Basta ficar
preparado para o ocasional surgimento de um “co ordinated” e um
“ordinated attack”!
OpenRefine
O OpenRefine (http://openrefine.org/) é um projeto de código aberto,
iniciado por uma empresa chamada Metaweb em 2009. A Google adquiriu
a Metaweb em 2010, alterando o nome do projeto de Freebase Gridworks
para Google Refine. Em 2012, a Google deixou de dar suporte para o
Refine e mudou o nome de novo, agora para OpenRefine, e qualquer
pessoa é bem-vinda para contribuir com o desenvolvimento do projeto.
Instalação
O OpenRefine é peculiar porque, embora sua interface seja executada em
um navegador, do ponto de vista técnico, é uma aplicação desktop que
deve ser baixada e instalada. Faça download da aplicação para Linux,
Windows e macOS a partir do site (http://openrefine.org/download.html).
Se você é usuário de Mac e deparar com algum problema para abrir o arquivo, acesse System
Preferences → Security & Privacy → General (Preferências do sistema → Segurança e
Privacidade → Geral). Em “Allow apps downloaded from” (Permitir apps transferidos de),
selecione Anywhere (Qualquer lugar). Infelizmente, durante a transição de um projeto Google
para um projeto de código aberto, o OpenRefine parece ter perdido sua legitimidade aos
olhos da Apple.
Para usar o OpenRefine, é necessário salvar seus dados como um arquivo
CSV (consulte a seção “Armazenando dados no formato CSV” caso precise
relembrar como fazer isso). De modo alternativo, se seus dados estiverem
armazenados em um banco de dados, talvez seja possível exportá-los para
um arquivo CSV.
Usando o OpenRefine
Nos exemplos a seguir, usaremos dados coletados da tabela de comparação
entre editores de texto da Wikipédia: “Comparison of Text Editors”
(https://en.wikipedia.org/wiki/Comparison_of_text_editors) – veja a Figura
8.1. Apesar de estar relativamente bem formatada, essa tabela contém
muitas modificações feitas pelas pessoas com o passar do tempo, portanto
apresenta pequenas inconsistências de formatação. Além disso, como a
intenção é que os dados sejam lidos por seres humanos e não por
máquinas, algumas das opções de formatação (por exemplo, o uso de
“Free” [Gratuito] em vez de “$0.00”) não são apropriadas como entrada
para programas.
Resumindo dados
No Capítulo 8, vimos como separar um conteúdo de texto em n-gramas,
isto é, conjuntos de expressões com n palavras. No nível básico, isso pode
ser usado para determinar quais conjuntos de palavras e expressões tendem
a ser mais comuns em uma seção de texto. Além disso, o código pode ser
usado para criar resumos de dados que pareçam naturais se retornarmos ao
texto original e extrairmos sentenças que acompanham essas expressões
mais populares.
Um texto que usaremos como exemplo para isso é o discurso de posse do
nono presidente dos Estados Unidos, William Henry Harrison. O governo
de Harrison estabeleceu dois recordes na história da Presidência: um para o
discurso de posse mais longo e outro para o período mais breve na
presidência – 32 dias.
Usaremos o texto completo de seu discurso
(http://pythonscraping.com/files/inaugurationSpeech.txt) como fonte de
dados para muitos dos códigos de exemplo neste capítulo.
Modificando um pouco o código usado para encontrar n-gramas no
Capítulo 8, podemos gerar um código que procure conjuntos de bigramas e
devolva um objeto Counter com todos eles:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import string
from collections import Counter
def cleanSentence(sentence):
sentence = sentence.split(' ')
sentence = [word.strip(string.punctuation+string.whitespace)
for word in sentence]
sentence = [word for word in sentence if len(word) > 1
or (word.lower() == 'a' or word.lower() == 'i')]
return sentence
def cleanInput(content):
content = content.upper()
content = re.sub('\n', ' ', content)
content = bytes(content, "UTF-8")
content = content.decode("ascii", "ignore")
sentences = content.split('. ')
return [cleanSentence(sentence) for sentence in sentences]
def getNgramsFromSentence(content, n):
output = []
for i in range(len(content)-n+1):
output.append(content[i:i+n])
return output
def getNgrams(content, n):
content = cleanInput(content)
ngrams = Counter()
ngrams_list = []
for sentence in content:
newNgrams = [' '.join(ngram) for ngram in
getNgramsFromSentence(sentence, 2)]
ngrams_list.extend(newNgrams)
ngrams.update(newNgrams)
return(ngrams)
content = str(
urlopen('http://pythonscraping.com/files/inaugurationSpeech.txt')
.read(), 'utf-8')
ngrams = getNgrams(content, 2)
print(ngrams)
Eis uma parte da saída gerada:
Counter({'OF THE': 213, 'IN THE': 65, 'TO THE': 61, 'BY THE': 41,
'THE CONSTITUTION': 34, 'OF OUR': 29, 'TO BE': 26, 'THE PEOPLE': 24,
'FROM THE': 24, 'THAT THE': 23,...
Entre esses bigramas, “the constitution” parece um assunto razoavelmente
comum no discurso, mas “of the”, “in the” e “to the” não parecem ter
especial relevância. Como podemos nos livrar de palavras indesejadas de
modo preciso e automático?
Felizmente, há pessoas por aí que estudam com afinco as diferenças entre
palavras “interessantes” e “não interessantes”, e o trabalho delas pode nos
ajudar a fazer exatamente isso. Mark Davies, um professor de linguística da
Brigham Young University, mantém o Corpus of Contemporary American
English (Corpus do inglês americano contemporâneo,
http://corpus.byu.edu/coca/): uma coleção com mais de 450 milhões de
palavras de publicações norte-americanas conhecidas, aproximadamente
da última década.
A lista das 5 mil palavras encontradas com mais frequência está disponível
de forma gratuita e, felizmente, ela é muito mais que suficiente como um
filtro básico para eliminar os bigramas mais comuns. Somente com as 100
primeiras palavras, junto com o acréscimo de uma função isCommon,
melhoramos bastante o resultado:
def isCommon(ngram):
commonWords = ['THE', 'BE', 'AND', 'OF', 'A', 'IN', 'TO', 'HAVE', 'IT', 'I',
'THAT', 'FOR', 'YOU', 'HE', 'WITH', 'ON', 'DO', 'SAY', 'THIS', 'THEY',
'IS', 'AN', 'AT', 'BUT', 'WE', 'HIS', 'FROM', 'THAT', 'NOT', 'BY',
'SHE', 'OR', 'AS', 'WHAT', 'GO', 'THEIR', 'CAN', 'WHO', 'GET', 'IF',
'WOULD', 'HER', 'ALL', 'MY', 'MAKE', 'ABOUT', 'KNOW', 'WILL', 'AS',
'UP', 'ONE', 'TIME', 'HAS', 'BEEN', 'THERE', 'YEAR', 'SO', 'THINK',
'WHEN', 'WHICH', 'THEM', 'SOME', 'ME', 'PEOPLE', 'TAKE', 'OUT', 'INTO',
'JUST', 'SEE', 'HIM', 'YOUR', 'COME', 'COULD', 'NOW', 'THAN', 'LIKE',
'OTHER', 'HOW', 'THEN', 'ITS', 'OUR', 'TWO', 'MORE', 'THESE', 'WANT',
'WAY', 'LOOK', 'FIRST', 'ALSO', 'NEW', 'BECAUSE', 'DAY', 'MORE', 'USE',
'NO', 'MAN', 'FIND', 'HERE', 'THING', 'GIVE', 'MANY', 'WELL']
for word in ngram:
if word in commonWords:
return True
return False
Com isso, geramos os bigramas a seguir, encontrados mais de duas vezes
no corpo do texto:
Counter({'UNITED STATES': 10, 'EXECUTIVE DEPARTMENT': 4,
'GENERAL GOVERNMENT': 4, 'CALLED UPON': 3, 'CHIEF MAGISTRATE': 3,
'LEGISLATIVE BODY': 3, 'SAME CAUSES': 3, 'GOVERNMENT SHOULD': 3,
'WHOLE COUNTRY': 3,...
De modo apropriado, os dois primeiros itens da lista são “United States”
(Estados Unidos) e “executive department” (Poder Executivo), que seriam
esperados no discurso de posse de um presidente.
É importante observar que estamos usando uma lista de palavras comuns
de uma era relativamente moderna para filtrar o resultado, o que talvez não
seja apropriado, considerando que o texto foi escrito em 1841. No entanto,
como estamos usando aproximadamente apenas as 100 primeiras palavras
da lista – podemos supor que elas são mais estáveis no tempo do que, por
exemplo, as últimas 100 palavras – e parece que os resultados são
satisfatórios, é provável que possamos evitar o esforço de identificar ou
criar uma lista das palavras mais comuns em 1841 (ainda que um esforço
como esse fosse interessante).
Agora que alguns tópicos essenciais foram extraídos do texto, como isso
nos ajudaria a escrever um resumo desse texto? Uma maneira é procurar a
primeira sentença que contenha cada n-grama “popular”, com base na
teoria de que a primeira ocorrência produzirá uma visão geral satisfatória
do conteúdo. Os primeiros cinco bigramas mais populares resultam nas
sentenças listadas a seguir:
• The Constitution of the United States is the instrument containing this
grant of power to the several departments composing the Government. (A
Constituição dos Estados Unidos é o instrumento que contém essa
concessão de poder aos vários departamentos que compõem o
Governo.)
• Such a one was afforded by the executive department constituted by the
Constitution. (Isso foi possibilitado pelo Poder Executivo estabelecido
pela Constituição.)
• The General Government has seized upon none of the reserved rights of
the States. (O governo federal não coibiu nenhum dos direitos reservados
aos estados.)
• Called from a retirement which I had supposed was to continue for the
residue of my life to fill the chief executive office of this great and free
nation, I appear before you, fellow-citizens, to take the oaths which the
constitution prescribes as a necessary qualification for the performance of
its duties; and in obedience to a custom coeval with our government and
what I believe to be your expectations I proceed to present to you a
summary of the principles which will govern me in the discharge of the
duties which I shall be called upon to perform. (Chamado a trabalhar
enquanto desfrutava uma aposentadoria que supunha perdurar pelo
resto de minha vida para ocupar a Presidência desta grande e livre nação,
eu me apresento diante de vocês, caros cidadãos, para fazer os
juramentos prescritos pela Constituição como uma qualificação
necessária para cumprir o que ela determina; em obediência a uma
tradição coetânea de nosso governo e àquilo que acredito ser as vossas
expectativas, prossigo apresentando uma síntese dos princípios que
governarão a mim no cumprimento das obrigações a que me caberão.)
• The presses in the necessary employment of the Government should never
be used to “clear the guilty or to varnish crime”. (A imprensa no emprego
necessário do Governo jamais deve ser usada para “exonerar a culpa ou
encobrir um crime”.)
Claro que isso não será publicado no CliffsNotes tão cedo, mas,
considerando que o documento original continha 217 sentenças, e que a
quarta sentenças (“Called from a retirement...”) sintetiza de modo bem
razoável o tópico principal, não foi ruim para uma primeira execução.
Com blocos de texto mais longos ou mais variados, talvez valha a pena
procurar trigramas ou até mesmo 4-gramas para obter as sentenças “mais
importantes” de uma passagem. Nesse exemplo, apenas um trigrama é
usado várias vezes: “exclusive metallic currency” (moeda exclusivamente
metálica) – dificilmente seria uma expressão definidora de um discurso de
posse presidencial. Em passagens mais longas, usar trigramas poderia ser
apropriado.
Outra abordagem é procurar sentenças que contenham os n-gramas mais
populares. É claro que eles tenderão a ser sentenças mais longas, portanto,
se isso se tornar um problema, você poderá procurar sentenças com os
maiores percentuais de palavras que sejam n-gramas populares, ou criar
uma métrica própria de pontuação, combinando diversas técnicas.
Modelos de Markov
Talvez você já tenha ouvido falar dos geradores de texto de Markov. Eles se
tornaram populares para entretenimento, como no aplicativo “That can be
my next tweet!” (http://yes.thatcan.be/my/next/tweet/), assim como pelo seu
uso para gerar emails spam que pareçam reais a fim de enganar sistemas de
detecção.
Todos esses geradores de texto são baseados no modelo de Markov, muitas
vezes usado para analisar grandes conjuntos de eventos aleatórios, em que
um evento discreto é seguido de outro evento discreto com determinada
probabilidade.
Por exemplo, poderíamos construir um modelo de Markov de um sistema
de previsão do tempo, conforme mostra a Figura 9.1.
Figura 9.1 – Modelo de Markov que descreve um sistema teórico de previsão
do tempo.
Nesse modelo, para cada dia ensolarado, há 70% de chances de o dia
seguinte também ser ensolarado, com 20% de chances de ser nublado e
apenas 10% de chances de chuva. Se o dia for chuvoso, há 50% de chances
de chover no dia seguinte, 25% de chances de estar ensolarado e 25% de
chances de estar nublado.
Podemos notar diversas propriedades nesse modelo de Markov:
• Todos os percentuais que saem de um único nó devem somar
exatamente 100%. Não importa quão complicado é o sistema, sempre
deve haver uma chance de 100% de ele ser levado a outro ponto no
próximo passo.
• Embora haja apenas três possibilidades para o clima em qualquer dado
instante, esse modelo pode ser usado para gerar uma lista infinita de
estados meteorológicos.
• Somente o estado do nó atual em que você estiver influenciará o estado
no qual você estará a seguir. Se você estiver no nó Ensolarado, não
importa se os 100 dias anteriores tenham sido ensolarados ou chuvosos
– as chances de ter sol no próximo dia serão exatamente as mesmas:
70%.
• Pode ser mais difícil alcançar alguns nós do que outros. A matemática
por trás disso é um tanto quanto complicada, mas deve ser
razoavelmente fácil ver que Chuvoso (com menos de “100%” de setas
que apontam para ele) é um estado muito menos provável de ser
alcançado nesse sistema, em qualquer instante, do que Ensolarado ou
Nublado.
É evidente que esse é um sistema simples, e os modelos de Markov podem
se tornar arbitrariamente maiores. O algoritmo de classificação de páginas
do Google é, em parte, baseado em um modelo de Markov, com sites
representados como nós e links de entrada/saída representados como
conexões entre os nós. A “probabilidade” de chegar em um nó em
particular representa a relativa popularidade do site. Isso significa que, se
nosso sistema de previsão do tempo representasse uma internet
extremamente pequena, “chuvoso” seria uma página com uma posição
mais baixa na classificação, enquanto “nublado” estaria em uma posição
mais alta.
Com tudo isso em mente, vamos considerar um exemplo mais concreto:
analisar e escrever texto.
Mais uma vez, usando o discurso de posse de William Henry Harrison
analisado no exemplo anterior, podemos escrever o código a seguir, que
gera cadeias de Markov arbitrariamente longas (com o tamanho da cadeia
definida com 100) com base na estrutura do texto:
from urllib.request import urlopen
from random import randint
def wordListSum(wordList):
sum = 0
for word, value in wordList.items():
sum += value
return sum
def retrieveRandomWord(wordList):
randIndex = randint(1, wordListSum(wordList))
for word, value in wordList.items():
randIndex -= value
if randIndex <= 0:
return word
def buildWordDict(text):
# Remove quebras de linha e aspas
text = text.replace('\n', ' ');
text = text.replace('"', '');
# Garante que sinais de pontuação sejam tratados como "palavras" próprias,
# de modo que sejam incluídos na cadeia de Markov
punctuation = [',','.',';',':']
for symbol in punctuation:
text = text.replace(symbol, ' {} '.format(symbol));
words = text.split(' ')
# Filtra palavras vazias
words = [word for word in words if word != '']
wordDict = {}
for i in range(1, len(words)):
if words[i-1] not in wordDict:
# Cria um novo dicionário para essa palavra
wordDict[words[i-1]] = {}
if words[i] not in wordDict[words[i-1]]:
wordDict[words[i-1]][words[i]] = 0
wordDict[words[i-1]][words[i]] += 1
return wordDict
text = str(urlopen('http://pythonscraping.com/files/inaugurationSpeech.txt')
.read(), 'utf-8')
wordDict = buildWordDict(text)
# Gera uma cadeia de Markov de tamanho 100
length = 100
chain = ['I']
for i in range(0, length):
newWord = retrieveRandomWord(wordDict[chain[-1]])
chain.append(newWord)
print(' '.join(chain))
A saída muda sempre que esse código é executado, mas eis um exemplo do
misterioso texto sem sentido que ele gera:
I sincerely believe in Chief Magistrate to make all necessary sacrifices and
oppression of the remedies which we may have occurred to me in the arrangement
and disbursement of the democratic claims them , consolatory to have been best
political power in fervently commending every other addition of legislation , by
the interests which violate that the Government would compare our aboriginal
neighbors the people to its accomplishment . The latter also susceptible of the
Constitution not much mischief , disputes have left to betray . The maxim which
may sometimes be an impartial and to prevent the adoption or
O que está acontecendo no código?
A função buildWordDict recebe a string de texto, obtida da internet. Ela então
faz algumas limpezas e um pouco de formatação, removendo aspas e
inserindo espaços em torno de outros sinais de pontuação para que sejam
efetivamente tratados como uma palavra separada. Depois disso, um
dicionário bidimensional é construído – um dicionário de dicionários –
com o seguinte formato:
{word_a : {word_b : 2, word_c : 1, word_d : 1},
word_e : {word_b : 5, word_d : 2},...}
Nesse dicionário de exemplo, “word_a” foi encontrada quatro vezes, das
quais duas ocorrências foram seguidas da palavra “word_b”, uma
ocorrência seguida de “word_c” e outra seguida de “word_d”. “Word_e”
foi seguida sete vezes, cinco vezes por “word_b” e duas por “word_d”.
Se desenhássemos um modelo de nós desse resultado, o nó representando
word_a teria uma seta de 50% apontando para “word_b” (que a seguiu duas
de quatro vezes), uma seta de 25% apontando para “word_c” e 25%
apontando para “word_d”.
Depois de construído, esse dicionário pode ser usado como uma tabela de
consulta para saber aonde ir depois, independentemente da palavra do
texto em que você por acaso estiver3. Usando o exemplo do dicionário de
dicionários, você poderia estar em “word_e” no momento, o que significa
que o dicionário {word_b : 5, word_d: 2} seria passado para a função
retrieveRandomWord. Essa função, por sua vez, obtém uma palavra aleatória do
dicionário, levando em consideração o número de vezes que ela ocorre.
Ao começar com uma palavra inicial aleatória (nesse caso, a palavra “I”
que está em toda parte), podemos percorrer a cadeia de Markov com
facilidade, gerando quantas palavras quisermos.
Essas cadeias de Markov tendem a melhorar no que concerne ao seu
“realismo” quanto mais textos forem coletados, sobretudo de fontes com
estilos semelhantes de escrita. Embora esse exemplo tenha usado bigramas
para criar a cadeia (em que a palavra anterior prevê a próxima palavra),
trigramas ou n-gramas de mais alta ordem podem ser utilizados, nos quais
duas ou mais palavras preveem a próxima palavra.
Apesar de entreter e ser um bom uso para os megabytes de texto que você
tenha acumulado durante seu web scraping, aplicações como essas podem
dificultar ver o lado prático das cadeias de Markov. Conforme
mencionamos antes nesta seção, as cadeias de Markov modelam a ligação
entre os sites, de uma página para a próxima. Grandes coleções desses
links como ponteiros podem compor grafos em forma de teias, úteis para
serem armazenados, percorridos e analisados. Nesse sentido, as cadeias de
Markov compõem a base tanto para pensar sobre web crawling quanto
para saber como seus web crawlers podem pensar.
Instalação e configuração
O módulo nltk pode ser instalado do mesmo modo que outros módulo
Python, seja fazendo download do pacote direto do site do NLTK ou
usando qualquer um dos instaladores de terceiros com a palavra-chave
“nltk”. Para ver instruções completas sobre a instalação, consulte o site do
NLTK (http://www.nltk.org/install.html).
Depois de instalar o módulo, é uma boa ideia fazer download de seus
repositórios de texto predefinidos para que você teste as funcionalidades de
modo mais fácil. Digite o seguinte na linha de comando Python:
>>> import nltk
>>> nltk.download()
O NLTK Downloader (Figura 9.2) será iniciado.
Recomendo instalar todos os pacotes disponíveis ao testar o corpus do
NLTK pela primeira vez. Você pode desinstalar facilmente os pacotes a
qualquer momento.
Figura 9.2 – O NLTK Downloader permite navegar pelos pacotes opcionais e
bibliotecas de texto associadas ao módulo nltk e fazer o seu download.
Recursos adicionais
Processar, analisar e compreender idiomas naturais por computador é uma
das tarefas mais difíceis em ciência da computação, e inúmeros volumes e
artigos de pesquisa já foram escritos sobre o assunto. Espero que o assunto
discutido neste livro inspire você a pensar além do web scraping
convencional ou, pelo menos, possa lhe dar uma direção inicial para saber
por onde começar quando assumir um projeto que exija análise de línguas
naturais.
Muitos recursos excelentes estão disponíveis sobre introdução ao
processamento de idiomas e sobre o Natural Language Toolkit de Python.
Em particular, o livro Natural Language Processing with Python (O’Reilly,
http://oreil.ly/1HYt3vV) de Steven Bird, Ewan Klein e Edward Loper
apresenta uma abordagem ampla e ao mesmo tempo introdutória ao
assunto.
Além disso, o livro Natural Language Annotations for Machine Learning de
James Pustejovsky e Amber Stubbs (O’Reilly, http://oreil.ly/S3BudT) é um
guia teórico um pouco mais avançado. É necessário ter conhecimento de
Python para implementar os exercícios; os tópicos discutidos funcionam
perfeitamente com o Natural Language Toolkit de Python.
1 Embora muitas das técnicas descritas neste capítulo possam ser aplicadas a todos os idiomas, ou à
maioria deles, por enquanto não há problema em manter o foco do processamento de idiomas
naturais apenas no inglês. Ferramentas como o Natural Language Toolkit de Python, por exemplo,
têm o inglês como foco. Cinquenta e seis porcento da internet ainda usa inglês (seguido do
alemão, com apenas 6%, de acordo com o W3Techs
(https://w3techs.com/technologies/overview/content_language/all)). Mas quem sabe? É quase certo
que a predominância do inglês na maior parte da internet sofrerá mudanças no futuro, e outras
atualizações talvez sejam necessárias nos próximos anos.
2 Oriol Vinyals et al, “A Picture Is Worth a Thousand (Coherent) Words: Building a Natural
Description of Images” (Uma imagem vale mais que mil palavras (coerentes): construindo uma
descrição natural de imagens, http://bit.ly/1HEJ8kX), Google Research Blog, 17 de novembro de
2014.
3 A exceção é a última palavra do texto, pois nada vem depois dela. Em nosso texto de exemplo, a
última palavra é um ponto final (.), que é conveniente, pois há outras 215 ocorrências no texto e,
desse modo, não representa um beco sem saída. Contudo, em implementações do gerador de
Markov no mundo real, a última palavra do texto talvez seja algo que se deva levado em
consideração.
CAPÍTULO 10
Rastreando formulários e logins
Figura 10.2 – O usuário deve fornecer um nome de usuário e uma senha para
acessar a página protegida pela autenticação de acesso básica.
Como sempre nesses exemplos, você pode fazer login com qualquer nome
de usuário, mas a senha deve ser “password”.
O pacote Requests contém um módulo auth especificamente concebido
para lidar com autenticação HTTP:
import requests
from requests.auth import AuthBase
from requests.auth import HTTPBasicAuth
auth = HTTPBasicAuth('ryan', 'password')
r = requests.post(url='http://pythonscraping.com/pages/auth/login.php',
auth=auth)
print(r.text)
Embora essa pareça uma requisição POST comum, um objeto HTTPBasicAuth é
passado como o argumento auth na requisição. O texto resultante será a
página protegida pelo nome do usuário e a senha (ou uma página Access
Denied [Acesso negado], caso a requisição falhe).
Para obter ajuda com os CAPTCHAs, consulte o Capítulo 13, que aborda processamento de
imagens e reconhecimento de textos em Python.
Se você deparar com um erro misterioso, o servidor pode estar rejeitando
sua submissão de formulário por algum motivo desconhecido, consulte o
Capítulo 14, que aborda os honeypots, campos ocultos e outras medidas de
segurança adotadas pelos sites para proteger seus formulários.
CAPÍTULO 11
Scraping de JavaScript
</script>
Esse script trata cookies específicos do Google Analytics, usados para
monitorar o seu acesso de página em página. Às vezes, isso pode ser um
problema para os web scrapers criados a fim de executar JavaScript e tratar
cookies (como aqueles que usam o Selenium, discutido mais adiante neste
capítulo).
Se um site usa Google Analytics ou um sistema semelhante de análise web
(web analytics), e você não quer que ele saiba que está sendo rastreado ou
que seus dados estão sendo coletados, não se esqueça de descartar todos os
cookies usados na análise – ou todos os cookies.
Google Maps
Se você já passou um tempo na internet, é quase certo que já tenha visto o
Google Maps incluído em um site. Sua API facilita bastante disponibilizar
mapas com informações personalizadas em qualquer site.
Se estiver coletando qualquer tipo de dado de localização, entender como o
Google Maps funciona facilitará obter coordenadas bem formatadas com
latitudes/longitudes e até mesmo endereços. Um dos modos mais comuns
de representar uma localização no Google Maps é usar um marcador
(também conhecido como pino).
Os marcadores podem ser inseridos em qualquer Google Map usando um
código como este:
var marker = new google.maps.Marker({
position: new google.maps.LatLng(-25.363882,131.044922),
map: map,
title: 'Some marker text'
});
Python facilita extrair todas as ocorrências de coordenadas que estiverem
entre google.maps.LatLng( e ) a fim de obter uma lista de coordenadas com
latitudes/longitudes.
Com a API Reverse Geocoding do Google
(https://developers.google.com/maps/documentation/javascript/examples/geocoding-
reverse), é possível converter esses pares de coordenadas em endereços bem
formatados para armazenagem e análise.
driver = webdriver.PhantomJS(executable_path='')
driver.get('http://pythonscraping.com/pages/javascript/ajaxDemo.html')
try:
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, 'loadedButton')))
finally:
print(driver.find_element_by_id('content').text)
driver.close()
Esse script inclui várias importações novas, com destaque para WebDriverWait
e expected_conditions, ambos combinados nesse caso para compor o que o
Selenium chama de espera implícita.
Uma espera implícita difere de uma espera explícita por esperar que um
determinado estado no DOM ocorra antes de prosseguir, enquanto uma
espera explícita define um tempo fixo, como no exemplo anterior, em que
a espera era de três segundos. Em uma espera implícita, o estado do DOM
a ser detectado é definido por expected_condition (observe que há um cast
para EC na importação; essa é uma convenção comum, usada para ser mais
conciso). As condições esperadas podem variar bastante na biblioteca
Selenium, incluindo:
• uma caixa de alerta é exibida;
• um elemento (por exemplo, uma caixa de texto) é colocado em um
estado selecionado;
• há uma mudança no título da página, ou um texto agora é exibido na
página ou em um elemento específico;
• um elemento agora está visível no DOM, ou um elemento desapareceu
do DOM.
A maioria dessas condições esperadas exige que você especifique um
elemento a ser observado, antes de tudo. Os elementos são especificados
com localizadores (locators). Observe que os localizadores não são iguais
aos seletores (consulte a seção “Seletores do Selenium” para ver mais
informações sobre os seletores). Um localizador é uma linguagem de
consulta abstrata, que usa o objeto By; esse objeto pode ser utilizado de
diversas maneiras, inclusive na criação de seletores.
No código a seguir, um localizador é usado para encontrar elementos cujo
ID é loadedButton:
EC.presence_of_element_located((By.ID, 'loadedButton'))
Os localizadores também podem ser usados para criar seletores, com a
função find_element do WebDriver:
print(driver.find_element(By.ID, 'content').text)
É claro que, do ponto de vista da funcionalidade, isso equivale à linha do
código de exemplo:
print(driver.find_element_by_id('content').text)
Se não houver necessidade de usar um localizador, não use; você evitará
uma importação. No entanto, essa ferramenta conveniente é utilizada em
diversas aplicações e tem um alto grau de flexibilidade.
As estratégias de seleção de localizadores a seguir podem ser usadas com o
objeto By:
ID
Usado no exemplo; encontra elementos com base no atributo id do
HTML.
CLASS_NAME
Usado para encontrar elementos com base no atributo class do HTML.
Por que essa função se chama CLASS_NAME, e não apenas CLASS? Usar o
formato object.CLASS criaria problemas para a biblioteca Java do Selenium,
na qual .class é um método reservado. Para manter a sintaxe do Selenium
consistente entre as linguagens, CLASS_NAME foi usada.
CSS_SELECTOR
Encontra elementos com base no nome de sua class, id ou tag, usando a
convenção #idName, .className, tagName.
LINK_TEXT
Encontra tags HTML <a> com base no texto que contêm. Por exemplo,
um link chamado “Next” pode ser selecionado com (By.LINK_TEXT, "Next").
PARTIAL_LINK_TEXT
Semelhante a LINK_TEXT, mas faz a correspondência com uma string parcial.
NAME
Encontra tags HTML com base em seu atributo name. É conveniente para
formulários HTML.
TAG_NAME
Encontra tags HTML com base no nome das tags.
XPATH
Usa uma expressão XPath (cuja sintaxe está descrita na caixa de texto a
seguir) para selecionar elementos correspondentes.
Sintaxe do XPath
XPath (forma abreviada de XML Path) é uma linguagem de consulta usada para navegar em
partes de um documento XML e selecioná-las. Criada pelo W3C em 1999, é ocasionalmente
usada em linguagens como Python, Java e C# para lidar com documentos XML.
O BeautifulSoup não tem suporte para XPath, mas muitas das demais bibliotecas usadas neste
livro, como Scrapy e Selenium, têm. Com frequência, o XPath pode ser usado do mesmo modo
que os seletores CSS (como mytag#idname), apesar de ter sido projetado para trabalhar com
documentos XML mais genéricos, em vez de documentos HTML em particular.
A sintaxe do XPath apresenta quatro conceitos principais:
• Nós raiz versus nõs que não são raiz
– /div selecionará o nó div somente se estiver na raiz do documento.
– //div seleciona todos os divs em qualquer ponto do documento.
• Seleção de atributos
– //@href seleciona qualquer nó com o atributo href.
– //a[@href='http://google.com'] seleciona todos os links do documento que apontem
para o Google.
• Seleção de nós com base na posição
– //a[3] seleciona o terceiro link do documento.
– //table[last()] seleciona a última tabela do documento.
– //a[position() < 3] seleciona os três primeiros links do documento.
• Asteriscos (*) correspondem a qualquer conjunto de caracteres ou nós, e podem ser usados em
várias situações.
– //table/tr/* seleciona todos os filhos de tags tr em todas as tabelas (é conveniente para
selecionar células que usem tags tanto th como td).
– //div[@*] seleciona todas as tags div com qualquer atributo.
A sintaxe do XPath também tem muitos recursos avançados. Ao longo dos anos, ela se
desenvolveu e se transformou em uma linguagem de consulta relativamente complicada, com
lógica booleana, funções (como position()) e diversos operadores não discutidos nesta seção.
Se você tiver algum problema de seleção de HTML ou de XML que não possa ser resolvido com
as funções apresentadas, consulte a página da Microsoft sobre a sintaxe do XPath
(https://msdn.microsoft.com/en-us/enus/library/ms256471).
def waitForLoad(driver):
elem = driver.find_element_by_tag_name("html")
count = 0
while True:
count += 1
if count > 20:
print('Timing out after 10 seconds and returning')
return
time.sleep(.5)
try:
elem == driver.find_element_by_tag_name('html')
except StaleElementReferenceException:
return
driver = webdriver.PhantomJS(executable_path='<Path to Phantom JS>')
driver.get('http://pythonscraping.com/pages/javascript/redirectDemo1.html')
waitForLoad(driver)
print(driver.page_source)
Esse script verifica a página a cada meio segundo, com um timeout de 10
segundos, porém os tempos usados para verificação e timeout podem ser
ajustados com facilidade, para cima ou para baixo, conforme necessário.
Como alternativa, podemos escrever um laço semelhante que verifique o
URL atual da página até que ele mude, ou até que corresponda a um URL
específico que estamos procurando.
Esperar que elementos apareçam e desapareçam é uma tarefa comum no
Selenium, e a mesma função WebDriverWait do exemplo anterior da carga do
botão pode ser usada. Nesse caso, estamos especificando um timeout de 15
segundos e um seletor XPath que procura o conteúdo do corpo da página
para executar a mesma tarefa:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
driver = webdriver.PhantomJS(executable_path=
'drivers/phantomjs/phantomjs-2.1.1-macosx/bin/phantomjs')
driver.get('http://pythonscraping.com/pages/javascript/redirectDemo1.html')
try:
bodyElement = WebDriverWait(driver, 15).until(EC.presence_of_element_located(
(By.XPATH, '//body[contains(text(),
"This is the page you are looking for!)]")))
print(bodyElement.text)
except TimeoutException:
print('Did not find the element')
1 A postagem de blog de Dave Methvin, “The State of jQuery 2014” (O estado da jQuery em 2014,
http://bitly.com/2pry8aU/), de 13 de janeiro de 2014, contém um detalhamento das estatísticas.
2 W3Techs, “Usage Statistics and Market Share of Google Analytics for Websites” (Estatísticas de uso
e fatia de mercado do Google Analytics em sites, http://w3techs.com/technologies/details/ta-
googleanalytics/all/all),
3 W3Techs, “Usage of JavaScript for Websites” (Uso de JavaScript em sites,
http://w3techs.com/technologies/details/cp-javascript/all/all).
CAPÍTULO 12
Rastreando por meio de APIs
Parsing de JSON
Neste capítulo, vimos vários tipos de APIs e como funcionam, além de
exemplos de respostas JSON dessas APIs. Vamos ver agora como fazer
parse dessas informações e usá-las.
No início do capítulo, vimos o exemplo de freegeoip.net IP, que converte
endereços IP em endereços físicos:
http://freegeoip.net/json/50.78.253.58
Podemos tomar o resultado dessa requisição e usar as funções de parsing
de JSON de Python para decodificá-lo:
import json
from urllib.request import urlopen
def getCountry(ipAddress):
response = urlopen('http://freegeoip.net/json/'+ipAddress).read()
.decode('utf-8')
responseJson = json.loads(response)
return responseJson.get('country_code')
print(getCountry('50.78.253.58'))
O código do país para o endereço IP 50.78.253.58 é exibido.
A biblioteca de parsing de JSON usada faz parte da biblioteca nuclear de
Python. Basta digitar import json no início, e pronto! De modo diferente de
muitas linguagens capazes de fazer parse de JSON e gerar um objeto
especial ou um nó JSON, Python usa uma abordagem mais flexível e
transforma objetos JSON em dicionários, arrays JSON em listas, strings
JSON em strings, e assim por diante. Desse modo, é muito fácil acessar e
manipular valores armazenados em JSON.
O código a seguir faz uma demonstração rápida de como a biblioteca
JSON de Python trata os valores que podem ser encontrados em uma
string JSON:
import json
jsonString = '{"arrayOfNums":[{"number":0},{"number":1},{"number":2}],
"arrayOfFruits":[{"fruit":"apple"},{"fruit":"banana"},
{"fruit":"pear"}]}'
jsonObj = json.loads(jsonString)
print(jsonObj.get('arrayOfNums'))
print(jsonObj.get('arrayOfNums')[1])
print(jsonObj.get('arrayOfNums')[1].get('number') +
jsonObj.get('arrayOfNums')[2].get('number'))
print(jsonObj.get('arrayOfFruits')[2].get('fruit'))
Eis a saída:
[{'number': 0}, {'number': 1}, {'number': 2}]
{'number': 1}
3
pear
A linha 1 é uma lista de objetos dicionário, a linha 2 é um objeto
dicionário, a linha 3 é um inteiro (a soma dos inteiros acessados nos
dicionários) e a linha 4 é uma string.
optional arguments:
-h, --help show this help message and exit
-u [U] Target URL. If not provided, target directory will be scanned
for har files.
random.seed(datetime.datetime.now())
def getLinks(articleUrl):
html = urlopen('http://en.wikipedia.org{}'.format(articleUrl))
bs = BeautifulSoup(html, 'html.parser')
return bs.find('div', {'id':'bodyContent'}).findAll('a',
href=re.compile('^(/wiki/)((?!:).)*$'))
def getHistoryIPs(pageUrl):
# Este é o formato das páginas de histórico de revisões:
# http://en.wikipedia.org/w/index.php?title=Title_in_URL&action=history
pageUrl = pageUrl.replace('/wiki/', '')
historyUrl = 'http://en.wikipedia.org/w/index.php?title={}&action=history'
.format(pageUrl)
print('history url is: {}'.format(historyUrl))
html = urlopen(historyUrl)
bs = BeautifulSoup(html, 'html.parser')
# encontra apenas os links cuja classe seja "mw-anonuserlink" e
# tenha endereços IP em vez de nomes de usuário
ipAddresses = bs.findAll('a', {'class':'mw-anonuserlink'})
addressList = set()
for ipAddress in ipAddresses:
addressList.add(ipAddress.get_text())
return addressList
links = getLinks('/wiki/Python_(programming_language)')
while(len(links) > 0):
for link in links:
print('-'*20)
historyIPs = getHistoryIPs(link.attrs['href'])
for historyIP in historyIPs:
print(historyIP)
links = getLinks('/wiki/Python_(programming_language)')
1 Essa API converte endereços IP para localizações geográficas e é uma das APIs que usaremos mais
adiante neste capítulo.
2 Na verdade, muitas APIs usam requisições POST no lugar de PUT para atualizar informações. Se
uma nova entidade será criada ou é uma antiga que está sendo atualizada em geral fica a cargo de
como a própria requisição da API está estruturada. Apesar disso, é bom saber a diferença e, com
frequência, você verá requisições PUT em APIs comumente usadas.
CAPÍTULO 13
Processamento de imagens e reconhecimento
de texto
Pillow
Apesar de o Pillow talvez não ser a mais completa das bibliotecas de
processamento de imagens, ele tem todos os recursos de que você
provavelmente precisará e mais alguns – a menos que esteja planejando
reescrever o Photoshop em Python e, nesse caso, você estaria lendo o livro
errado! O Pillow também tem a vantagem de ser uma das bibliotecas de
terceiros mais bem documentada, e é muito simples de usar de imediato.
Tendo originado da PIL (Python Imaging Library, ou Biblioteca de Imagens
de Python) para Python 2.x, o Pillow acrescenta suporte para Python 3.x.
Assim como seu antecessor, o Pillow permite importar e manipular
imagens com facilidade, e tem diversos filtros, máscaras e até mesmo
transformações específicas para pixels:
from PIL import Image, ImageFilter
kitten = Image.open('kitten.jpg')
blurryKitten = kitten.filter(ImageFilter.GaussianBlur)
blurryKitten.save('kitten_blurred.jpg')
blurryKitten.show()
No exemplo anterior, a imagem kitten.jpg será aberta em seu visualizador
de imagens padrão, de forma desfocada, e também será salva nesse estado
como kitten_blurred.jpg no mesmo diretório.
Usaremos o Pillow para fazer um pré-processamento das imagens de modo
a deixá-las mais legíveis para o computador, mas, conforme mencionamos
antes, é possível fazer muito mais com a biblioteca além dessas aplicações
simples de filtros. Para outras informações, consulte a documentação do
Pillow (http://pillow.readthedocs.org/).
Tesseract
O Tesseract, uma biblioteca de OCR patrocinada pelo Google (uma
empresa obviamente muito conhecida por suas tecnologias de OCR e de
aprendizado de máquina), é considerado por muitos o melhor e mais
preciso sistema de OCR de código aberto à disposição.
Além de ser preciso, o Tesseract é extremamente flexível. Pode ser treinado
para reconhecer qualquer quantidade de fontes (desde que elas sejam
relativamente consistentes entre si, como veremos em breve), e pode ser
expandido para reconhecer qualquer caractere Unicode.
Este capítulo usa tanto o programa de linha de comando Tesseract como o
seu wrapper de terceiros para Python, o pytesseract. Ambos serão
explicitamente nomeados desse modo, portanto, saiba que, quando
“Tesseract” for usado, estarei me referindo ao software da linha de
comando, e, quando usar “pytesseract”, estarei falando especificamente do
wrapper de terceiros para Python.
Instalando o Tesseract
Para os usuários de Windows, há um instalador executável
(https://code.google.com/p/tesseract-ocr/downloads/list) conveniente.
Atualmente (quando este livro foi escrito), a versão é a 3.02, embora
versões mais recentes também deverão ser apropriadas.
Usuários de Linux podem instalar o Tesseract com o apt-get:
$ sudo apt-get tesseract-ocr
A instalação do Tesseract em um Mac é um pouco mais complicada,
embora possa ser feita com facilidade com um dos vários instaladores de
terceiros, como o Homebrew (http://brew.sh/), que usamos no Capítulo 6
para instalar o MySQL. Por exemplo, podemos instalar o Homebrew e usá-
lo para instalar o Tesseract com duas linhas:
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/ \
install/master/install)"
$ brew install tesseract
O Tesseract também pode ser instalado a partir do código-fonte, que está
na página de download do projeto (https://code.google.com/p/tesseract-
ocr/downloads/list).
Para usar alguns recursos do Tesseract, como treinamento do software para
reconhecer novos caracteres, que veremos mais adiante nesta seção, é
necessário definir também uma nova variável de ambiente, $TESSDATA_PREFIX,
para que ele saiba em que local os arquivos de dados estão armazenados.
Podemos fazer isso na maioria dos sistemas Linux e no macOS da seguinte
maneira:
$ export TESSDATA_PREFIX=/usr/local/share/
Observe que /usr/local/share/ é o local default para os dados do Tesseract,
embora você deva verificar se isso vale para a sua instalação.
De modo semelhante, no Windows, o comando a seguir pode ser usado
para definir a variável de ambiente:
# setx TESSDATA_PREFIX C:\Program Files\Tesseract OCR\
pytesseract
Depois que o Tesseract estiver instalado, estaremos prontos para instalar a
biblioteca wrapper para Python, pytesseract, que usa a sua instalação de
Tesseract para ler arquivos de imagens e gerar strings e objetos que
poderão ser utilizados em scripts Python.
print(pytesseract.image_to_data(Image.open('files/test.png'),
output_type=Output.DICT))
print(pytesseract.image_to_string(Image.open('files/test.png'),
output_type=Output.BYTES))
Este capítulo usa uma combinação da biblioteca pytesseract, assim como
do Tesseract para linha de comando, e o disparo do Tesseract a partir de
Python usando a biblioteca subprocess. Apesar de a biblioteca pytesseract ser
útil e conveniente, há algumas funcionalidades do Tesseract que ela não
inclui, portanto é bom ter familiaridade com todos os métodos.
NumPy
Embora o NumPy não seja necessário em um OCR simples, você precisará
dele se quiser treinar o Tesseract para que reconheça conjuntos de
caracteres ou fontes adicionais, introduzidos mais adiante neste capítulo.
Também usaremos o NumPy para tarefas matemáticas simples (por
exemplo, cálculos de médias ponderadas) em alguns dos exemplos de
código mais tarde.
O NumPy é uma biblioteca eficaz, usada para álgebra linear e outras
aplicações matemáticas de larga escala. Funciona bem com o Tesseract em
razão de sua capacidade de representar e manipular as imagens
matematicamente, na forma de arrays grandes de pixels.
O NumPy pode ser instalado com qualquer instalador Python de terceiros,
como o pip, ou podemos fazer download do pacote
(https://pypi.org/project/numpy/) e instalá-lo com $ python setup.py install.
Mesmo que você não pretenda executar nenhum dos exemplos de código
que o utilizam, recomendo instalá-lo ou acrescentá-lo ao seu arsenal
Python. Ele serve para complementar a biblioteca matemática embutida de
Python, e tem muitos recursos úteis, sobretudo para operações com listas
de números.
Por convenção, o NumPy é importado como np, e pode ser usado assim:
import numpy as np
numbers = [100, 102, 98, 97, 103]
print(np.std(numbers))
print(np.mean(numbers))
Esse exemplo exibe o desvio-padrão e a média do conjunto de números
fornecido a ele.
Figura 13.1 – Amostra de texto salvo como um arquivo .tiff, para ser lido
pelo Tesseract.
O Tesseract pode ser executado na linha de comando para que leia esse
arquivo e escreva o resultado em um arquivo-texto:
$ tesseract text.tif textoutput | cat textoutput.txt
O resultado é uma linha com informações sobre a biblioteca Tesseract para
informar que ela está executando, seguida do conteúdo do arquivo
textoutput.txt recém-criado:
Tesseract Open Source OCR Engine v3.02.02 with Leptonica
This is some text, written in Arial, that will be read by
Tesseract. Here are some symbols: !@#$%"&'()
Podemos ver que o resultado, em sua maior parte, é exato, apesar de os
símbolos ^ e * terem sido interpretados com um caractere de aspas duplas
e aspas simples, respectivamente. De modo geral, porém, isso permitiu que
o texto fosse lido com muita facilidade.
Depois de desfocar o texto da imagem, criar alguns artefatos de
compactação JPG e adicionar um pequeno gradiente no plano de fundo, o
resultado piora bastante (veja a Figura 13.2).
def getConfidence(image):
data = pytesseract.image_to_data(image, output_type=Output.DICT)
text = data['text']
confidences = []
numChars = []
for i in range(len(text)):
if data['conf'][i] > -1:
confidences.append(data['conf'][i])
numChars.append(len(text[i]))
return np.average(confidences, weights=numChars), sum(numChars)
filePath = 'files/textBad.png'
start = 80
step = 5
end = 200
for threshold in range(start, end, step):
image = cleanFile(filePath, threshold)
scores = getConfidence(image)
print("threshold: " + str(threshold) + ", confidence: "
+ str(scores[0]) + " numChars " + str(scores[1]))
Esse script tem duas funções:
cleanFile
Recebe um arquivo original “ruim” e uma variável de limiar com os quais
executa a ferramenta de limiar da PIL. Processa o arquivo e devolve o
objeto de imagem da PIL.
getConfidence
Recebe o objeto de imagem limpo da PIL e o submete ao Tesseract.
Calcula o nível de confiança médio para cada string reconhecida
(ponderado pelo número de caracteres dessa string), assim como o
número de caracteres reconhecidos.
Variando o valor do limiar e obtendo o nível de confiança e a quantidade
de caracteres reconhecidos para cada valor, teremos a seguinte saída:
threshold: 80, confidence: 61.8333333333 numChars 18
threshold: 85, confidence: 64.9130434783 numChars 23
threshold: 90, confidence: 62.2564102564 numChars 39
threshold: 95, confidence: 64.5135135135 numChars 37
threshold: 100, confidence: 60.7878787879 numChars 66
threshold: 105, confidence: 61.9078947368 numChars 76
threshold: 110, confidence: 64.6329113924 numChars 79
threshold: 115, confidence: 69.7397260274 numChars 73
threshold: 120, confidence: 72.9078947368 numChars 76
threshold: 125, confidence: 73.582278481 numChars 79
threshold: 130, confidence: 75.6708860759 numChars 79
threshold: 135, confidence: 76.8292682927 numChars 82
threshold: 140, confidence: 72.1686746988 numChars 83
threshold: 145, confidence: 75.5662650602 numChars 83
threshold: 150, confidence: 77.5443037975 numChars 79
threshold: 155, confidence: 79.1066666667 numChars 75
threshold: 160, confidence: 78.4666666667 numChars 75
threshold: 165, confidence: 80.1428571429 numChars 70
threshold: 170, confidence: 78.4285714286 numChars 70
threshold: 175, confidence: 76.3731343284 numChars 67
threshold: 180, confidence: 76.7575757576 numChars 66
threshold: 185, confidence: 79.4920634921 numChars 63
threshold: 190, confidence: 76.0793650794 numChars 63
threshold: 195, confidence: 70.6153846154 numChars 65
Há uma tendência clara tanto para o nível médio de confiança no resultado
como para o número de caracteres reconhecidos. Ambos tendem a ter um
pico em torno de um limiar igual a 145, que é próximo do resultado
“ideal” de 143, encontrado manualmente.
Os limiares 140 e 145 proporcionam o número máximo de caracteres
reconhecidos (83), mas um limiar de 145 resulta no nível de confiança mais
alto para os caracteres encontrados, portanto talvez você queira optar por
esse resultado e devolver o texto reconhecido com esse limiar como o
“melhor palpite” para o texto contido na imagem.
É claro que apenas encontrar a “maioria” dos caracteres não significa
necessariamente que todos esses caracteres sejam reais. Com alguns
limiares, o Tesseract poderia separar caracteres únicos em vários, ou
interpretar um ruído aleatório na imagem como um caractere de texto que
na verdade não existe. Nesse caso, talvez você queira levar mais em
consideração o nível médio de confiança de cada valor.
Por exemplo, se encontrar um resultado em que se lê (em parte) o seguinte:
threshold: 145, confidence: 75.5662650602 numChars 83
threshold: 150, confidence: 97.1234567890 numChars 82
é provável que não precisássemos pensar duas vezes para optar pelo
resultado que oferece mais de 20% de confiança adicional, com perda de
apenas um caractere, e supor que o resultado com um limiar de 145 estava
simplesmente incorreto ou, quem sabe, houve um caractere separado ou
algo que não estava presente foi encontrado.
Em casos como esse, um pouco de experimentos prévios para aperfeiçoar o
algoritmo de seleção de limiar poderá ser conveniente. Por exemplo, talvez
você queira selecionar o valor para a qual o produto entre o nível de
confiança e o número de caracteres seja maximizado (nesse caso, 145
continuaria vencendo, com um produto igual a 6272; em nosso exemplo
imaginário, o limiar de 150 venceria, com um produto igual a 7964), ou
decida usar alguma outra métrica.
Observe que esse tipo de algoritmo de seleção também funciona com
outros valores arbitrários da ferramenta PIL diferentes do threshold. Além
do mais, podemos usá-lo para selecionar dois ou mais valores, variando-os
e selecionando o resultado com a melhor pontuação, de modo semelhante.
Obviamente, esse tipo de algoritmo de seleção exige um processamento
intenso. Executaremos tanto a PIL como o Tesseract muitas vezes em cada
imagem, enquanto, se conhecêssemos previamente os valores “ideais” para
os limiares, seria necessário executá-los apenas uma vez.
Tenha em mente que, à medida que começar a trabalhar com imagens a
serem processadas, você poderá começar a perceber padrões nos valores
“ideais” encontrados. Em vez de tentar todos os limiares de 80 a 200,
sendo realista, talvez fosse necessário testar apenas limiares de 130 a 180.
Você poderia até mesmo adotar outra abordagem e escolher limiares, por
exemplo, com um intervalo de 20 na primeira execução, e então usar um
algoritmo guloso (greedy algorithm) para aprimorar o melhor resultado,
decrementando o tamanho de seu passo para limiares entre as “melhores”
soluções encontradas na iteração anterior. Isso também pode funcionar
melhor se estivermos lidando com diversas variáveis.
driver.quit()
Apesar de esse script teoricamente poder ser executado com qualquer tipo
de webdriver Selenium, percebi que, no momento, ele é mais confiável com
o Chrome.
Como já vimos antes com a ferramenta de leitura do Tesseract, esse código
exibe várias passagens longas do livro que, em sua maior parte, são
legíveis, como vemos na prévia do primeiro capítulo:
Chapter I
During an Interval In the Melvmskl trial In the large
building of the Law Courts the members and public
prosecutor met in [van Egorowch Shebek's private
room, where the conversation turned on the celebrated
Krasovski case. Fedor Vasillevich warmly maintained
that it was not subject to their jurisdiction, Ivan
Egorovich maintained the contrary, while Peter
ivanowch, not havmg entered into the discussmn at
the start, took no part in it but looked through the
Gazette which had Just been handed in.
"Gentlemen," he said, "Ivan Ilych has died!"
No entanto, muitas palavras têm erros óbvios, como “Melvmskl” no lugar
do nome “Melvinski” e “discussmn” em vez de “discussion”. Vários erros
desse tipo podem ser corrigidos com palpites baseados em uma lista de
palavras de dicionário (talvez com acréscimos de nomes próprios
relevantes como “Melvinski”).
Ocasionalmente, um erro pode se estender por uma palavra inteira, como
na página 3 do texto:
it is he who is dead and not 1.
Nesse exemplo, a palavra “I” foi substituída pelo caractere “1”. Uma
análise de cadeia de Markov poderia ser útil nesse caso, além do acréscimo
de um dicionário de palavras. Se alguma parte do texto contiver uma
expressão muito incomum (“and not 1”), poderíamos supor que o texto,
na verdade, será a expressão mais comum (“and not I”).
É claro que o fato de essas substituições de caractere seguirem padrões
previsíveis ajuda: “vi” torna-se “w” e “I” torna-se “1”. Se essas
substituições ocorrerem com frequência em seu texto, você poderia criar
uma lista delas e usá-las para “testar” novas palavras e expressões,
escolhendo a solução que fizer mais sentido. Uma abordagem seria
substituir caracteres confundidos com frequência, e usar uma solução que
faça a correspondência com uma palavra do dicionário ou com um n-
grama reconhecido (ou mais comum).
Se você adotar essa abordagem, não se esqueça de ler o Capítulo 9, que
contém outras informações sobre como trabalhar com textos e fazer
processamento de idiomas naturais.
Embora o texto nesse exemplo tenha uma fonte sans-serif comum e o
Tesseract deva ser capaz de reconhecê-la com relativa facilidade, às vezes
um novo treinamento também pode ajudar a melhorar a precisão. A
próxima seção discute outra abordagem para resolver o problema de um
texto deturpado, exigindo um pequeno investimento prévio de tempo.
Se fornecermos ao Tesseract uma coleção grande de imagens de texto com
valores conhecidos, ele poderá ser “ensinado” a reconhecer a mesma fonte
no futuro, com muito mais minúcia e precisão, mesmo que haja problemas
ocasionais com o plano de fundo e com o posicionamento no texto.
Treinando o Tesseract
Para treinar o Tesseract de modo que ele reconheça uma escrita, seja de
uma fonte obscura e difícil de ler seja de um CAPTCHA, é necessário lhe
dar vários exemplos de cada caractere.
É nessa parte que você vai querer ouvir um bom podcast ou assistir a um
filme porque serão algumas horas de trabalho um tanto quanto tedioso. O
primeiro passo é fazer download de vários exemplos de seu CAPTCHA em
um único diretório. O número de exemplos que você reunir dependerá da
complexidade do CAPTCHA; usei uma amostra com 100 arquivos (um
total de 500 caracteres, ou aproximadamente 8 exemplos por símbolo, na
média) para o treinamento de meu CAPTCHA, e isso parece ter
funcionado muito bem.
Recomendo nomear a imagem com base na solução do CAPTCHA que ele representa (por
exemplo, 4MmC3.jpg). Percebi que isso ajuda a fazer uma verificação rápida de erro em um
grande número de arquivos de uma só vez; você pode visualizar todos os arquivos como
miniaturas e comparar facilmente as imagens com seus nomes. Além do mais, isso ajudará
bastante na verificação de erros nos passos subsequentes.
O segundo passo é dizer ao Tesseract o que é exatamente cada caractere e
onde ele está na imagem. Esse passo envolve criar arquivos de caixa (box
files), um para cada imagem CAPTCHA. Um arquivo de caixa tem o
seguinte aspecto:
4 15 26 33 55 0
M 38 13 67 45 0
m 79 15 101 26 0
C 111 33 136 60 0
3 147 17 176 45 0
O primeiro símbolo é o caractere representado, os quatro números
seguintes representam coordenadas para uma caixa retangular que
contorna a imagem, e o último número é um “número de página” usado
para treinamento com documentos de várias páginas (0 para nós).
É claro que não é divertido criar esses arquivos de caixa manualmente, mas
diversas ferramentas podem ajudar. Gosto da ferramenta online Tesseract
OCR Chopper (https://pp19dd.com/tesseract-ocr-chopper/) porque ela não
exige instalação nem bibliotecas adicionais, executa em qualquer máquina
que tenha um navegador e é relativamente fácil de usar. Faça o upload da
imagem, clique no botão Add (Adicionar) na parte inferior se precisar de
caixas adicionais, ajuste o tamanho das caixas se necessário e copie e cole o
texto em um novo arquivo .box.
Os arquivos de caixa devem ser salvos em formato texto simples, com a
extensão .box. Como no caso dos arquivos de imagem, é conveniente
nomear os arquivos de caixa de acordo com as soluções dos CAPTCHAs
que representam (por exemplo, 4MmC3.box). Mais uma vez, isso facilita
fazer uma comparação entre o conteúdo do arquivo .box e o nome do
arquivo e, novamente, com o arquivo de imagem associado se você ordenar
todos os arquivos de seu diretório de dados com base em seus nomes.
Você deverá criar cerca de 100 desses arquivos para garantir que terá dados
suficientes. Além disso, o Tesseract ocasionalmente descartará arquivos
por estarem ilegíveis, portanto talvez você queira ter um pouco de folga
quanto a esse número. Se achar que seus resultados de OCR não são tão
bons quanto gostaria, ou o Tesseract está tendo dificuldades com
determinados caracteres, criar dados de treinamento adicionais e tentar
novamente é um bom passo para depuração.
Depois de criar uma pasta de dados cheia de arquivos .box e arquivos de
imagens, copie esses dados para uma pasta de backup antes de fazer novas
manipulações. Embora seja pouco provável que executar scripts de
treinamento nos dados vá apagar alguma informação, é melhor prevenir do
que remediar quando horas de trabalho investidas na criação de arquivos
.box estão em jogo. Além do mais, é bom poder se desfazer de um diretório
confuso, cheio de dados, e tentar novamente.
Há meia dúzia de passos para fazer todas as análises de dados e criar os
arquivos de treinamento necessários ao Tesseract. Algumas ferramentas
fazem isso se você lhes fornecer a imagem original e os arquivos .box
correspondentes, mas nenhuma delas, até agora, funciona para o Tesseract
3.02, infelizmente.
Escrevi uma solução em Python (https://github.com/REMitchell/tesseract-
trainer) que atua em um diretório contendo tanto os arquivos de imagens
quanto os arquivos de caixa; todos os arquivos de treinamento necessários
serão criados de modo automático.
As configurações iniciais e os passos executados por esse programa podem
ser vistos nos métodos __init__ e runAll da classe:
def __init__(self):
languageName = 'eng'
fontName = 'captchaFont'
directory = '<path to images>'
def runAll(self):
self.createFontFile()
self.cleanImages()
self.renameFiles()
self.extractUnicode()
self.runShapeClustering()
self.runMfTraining()
self.runCnTraining()
self.createTessData()
As três únicas variáveis que você deverá definir nesse programa são bem
simples:
languageName
É o código de três letras que o Tesseract usa para saber qual é o idioma
que está vendo. Na maioria das vezes, é provável que você queira usar eng
para inglês.
fontName
É o nome da fonte escolhida. Pode ter qualquer valor, mas deve ser uma
palavra única, sem espaços.
directory
É o diretório que contém todos os seus arquivos de imagens e os arquivos
de caixa. Recomendo que seja um path absoluto, mas, se usar um path
relativo, esse deverá ser relativo ao local em que você estiver executando o
seu código Python. Se for um path absoluto, o código poderá ser
executado de qualquer lugar em seu computador.
Vamos analisar as funções individuais usadas.
createFontFile cria um arquivo necessário, font_properties, que permite que
o Tesseract saiba qual é a nova fonte que você está criando:
captchaFont 0 0 0 0 0
Esse arquivo é composto do nome da fonte, seguido de 1s e 0s que indicam
se itálico, negrito ou outras versões da fonte devem ser consideradas.
(Treinar fontes com essas propriedades é um exercício interessante, mas,
infelizmente, está além do escopo deste livro.)
cleanImages cria versões com mais contraste de todos os arquivos de imagem
encontrados, converte-os para escalas de cinza e executa outras operações
que deixam os arquivos de imagem mais fáceis de ler para os programas de
OCR. Se você estiver lidando com imagens CAPTCHA com lixo visual que
possa ser mais facilmente filtrado no pós-processamento, este seria o local
para acrescentar esse processamento adicional.
renameFiles renomeia todos os seus arquivos .box e os arquivos de imagem
correspondentes com os nomes exigidos pelo Tesseract (os números dos
arquivos, nesse caso, são dígitos sequenciais para manter os vários arquivos
separados):
• <nomeDoIdioma>.<nomeDaFonte>.exp<númeroDoArquivo>.box
• <nomeDoIdioma>.<nomeDaFonte>.exp<númeroDoArquivo>.tiff
extractUnicode olha para todos os arquivos .box criados e determina o
conjunto total de caracteres disponíveis para treinamento. O arquivo
Unicode resultante informará quantos caracteres diferentes foram
encontrados, e poderia ser uma ótima maneira de ver rapidamente se está
faltando algo.
As três próximas funções, runShapeClustering, runMfTraining e runCtTraining,
criam os arquivos shapetable, pfftable e normproto, respectivamente. Todas
elas fornecem informações sobre a geometria e o formato de cada
caractere, bem como informações estatísticas usadas pelo Tesseract para
calcular a probabilidade de um dado caractere ser de um tipo ou de outro.
Por fim, o Tesseract renomeia cada uma das pastas com dados reunidos
para que sejam prefixadas com o nome do idioma necessário (por exemplo,
shapetable é renomeado para eng.shapetable) e processa todos esses
arquivos gerando o arquivo de dados final de treinamento, eng.traineddata.
O único passo que você deve executar manualmente é mover o arquivo
eng.traineddata criado para a sua pasta-raiz tessdata usando os comandos a
seguir no Linux ou no Mac:
$cp /path/to/data/eng.traineddata $TESSDATA_PREFIX/tessdata
Depois desses passos, você não deverá ter problemas para solucionar os
CAPTCHAs de cujo tipo o Tesseract foi treinado. A partir de agora,
quando pedir ao Tesseract que leia a imagem de exemplo, a resposta
correta será obtida:
$ tesseract captchaExample.png output|cat output.txt
4MmC3
Sucesso! Uma melhoria significativa em relação à interpretação anterior da
imagem como 4N\,,,C<3.
Essa é apenas uma visão geral rápida de todo o potencial do treinamento
de fontes e dos recursos de reconhecimento do Tesseract. Se estiver
interessado em treinar intensivamente o Tesseract, quem sabe dando início
à sua própria biblioteca de arquivos de treinamento de CAPTCHAs, ou
compartilhando recursos de reconhecimento de novas fontes com o
mundo, recomendo que consulte a documentação
(https://github.com/tesseract-ocr/).
1 Quando se trata de processar um texto para o qual não tenha sido treinado, o Tesseract se sai
muito melhor com edições de livros que usem fontes grandes, sobretudo se as imagens forem
pequenas. A próxima seção discute como treinar o Tesseract com fontes diferentes, o que poderá
ajudá-lo a ler tamanhos muito menores de fontes, incluindo visualizações prévias de edições de
livros cujas fontes não sejam grandes!
2 Veja https://gizmodo.com/google-has-finally-killed-the-captcha-1793190374.
CAPÍTULO 14
Evitando armadilhas no scraping
Poucas coisas são mais frustrantes que fazer scraping de um site, visualizar
o resultado e não ver os dados que estão claramente visíveis em seu
navegador. Ou submeter um formulário que deveria estar perfeitamente
correto, mas ser recusado pelo servidor web. Ou ter seu endereço IP
bloqueado por um site por razões desconhecidas.
Esses são alguns dos bugs mais difíceis de solucionar, não só porque
podem ser tão inesperados (um script que funciona muito bem em um site
pode não funcionar em outro, aparentemente idêntico), mas porque não
têm propositalmente nenhuma mensagem de erro esclarecedora nem stack
traces para serem usados. Você foi identificado como um bot, foi rejeitado
e não sabe por quê.
Neste livro, descrevi várias maneiras de fazer tarefas intrincadas em sites
(submeter formulários, extrair e limpar dados complicados, executar
JavaScript etc.). Este capítulo é uma espécie de guarda-chuva, pois aborda
várias técnicas com raízes em uma ampla gama de assuntos (cabeçalhos
HTTP, CSS e formulários HTML, para mencionar alguns). No entanto,
todas têm um ponto em comum: foram criadas para superar um obstáculo
colocado somente com o propósito de impedir um web scraping
automatizado em um site.
Não importa até que ponto essa informação seja útil de imediato no
momento para você, porém recomendo que ao menos passe os olhos neste
capítulo. Nunca se sabe quando ele poderá ajudá-lo a resolver um bug
difícil ou evitar totalmente um problema.
session = requests.Session()
headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5)'
'AppleWebKit 537.36 (KHTML, like Gecko) Chrome',
'Accept':'text/html,application/xhtml+xml,application/xml;'
'q=0.9,image/webp,*/*;q=0.8'}
url = 'https://www.whatismybrowser.com/'\
'developers/what-http-headers-is-my-browser-sending'
req = session.get(url, headers=headers)
bs = BeautifulSoup(req.text, 'html.parser')
print(bs.find('table', {'class':'table-striped'}).get_text)
O resultado deve mostrar que os cabeçalhos agora são os mesmos
definidos no objeto dicionário headers no código.
Embora seja possível que os sites verifiquem se “é um ser humano” com
base em qualquer uma das propriedades nos cabeçalhos HTTP, percebi
que, em geral, a única configuração que realmente importa é a de User-Agent.
É uma boa ideia manter esse cabeçalho configurado com um valor mais
discreto que Python-urllib/3.4, não importa o projeto no qual você estiver
trabalhando. Além do mais, se algum dia você deparar com um site muito
desconfiado, preencher um dos cabeçalhos de uso comum, porém
raramente verificado, por exemplo, Accept-Language, poderia ser a chave para
convencê-lo de que você é um ser humano.
Cabeçalhos mudam o modo como vemos o mundo
Suponha que queremos escrever um tradutor de idiomas usando aprendizado de máquina para
um projeto de pesquisa, mas não temos grandes quantidades de textos traduzidos para testá-lo.
Muitos sites grandes apresentam traduções diferentes para o mesmo conteúdo, com base nas
preferências de idioma informadas em seus cabeçalhos. Apenas mudar Accept-Language:en-US
para Accept-Language:fr em seus cabeçalhos poderá fazer você receber um “Bonjour” dos sites
com escala e orçamento para lidar com traduções (grandes empresas internacionais em geral são
uma boa aposta).
Os cabeçalhos também podem fazer os sites mudarem o formato do conteúdo que apresentam.
Por exemplo, dispositivos móveis navegando pela internet muitas vezes veem uma versão
reduzida dos sites, sem banners de anúncios, Flash e outras distrações. Se você tentar modificar
seu User-Agent para outro valor, como o que vemos a seguir, talvez perceba que será um pouco
mais fácil fazer scraping dos sites!
User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X)
AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257
Safari/9537.53
Tempo é tudo
Alguns sites bem protegidos podem impedir que você submeta formulários
ou interaja com o site se você fizer isso com muita rapidez. Mesmo se esses
recursos de segurança não estiverem presentes, fazer download de muitas
informações de um site de modo significativamente mais rápido do que um
ser humano comum faria é uma boa maneira de ser notado e bloqueado.
Assim, embora a programação multithreaded seja uma ótima maneira de
carregar páginas rapidamente – permitindo processar dados em uma
thread enquanto as páginas são carregadas de forma contínua em outra –, é
uma estratégia ruim para escrever bons scrapers. Procure sempre manter as
cargas de página individuais e as requisições de dados em um nível
mínimo. Se for possível, procure fazê-los com intervalos de alguns
segundos, mesmo que seja necessário acrescentar um código extra:
import time
time.sleep(3)
O fato de precisar desses segundos extras entre as cargas de páginas em
geral é determinado de modo experimental. Muitas vezes, já tive
dificuldades para coletar dados de um site, tendo que provar que “eu não
era um robô” a intervalos de alguns minutos (resolvendo um CAPTCHA
manualmente, colando meus cookies recém-obtidos no scraper para que o
site considerasse que ele havia “provado que é um ser humano”), mas
acrescentar um time.sleep resolvia meus problemas e permitia que eu fizesse
scraping por tempo indeterminado.
Às vezes é preciso reduzir a velocidade para ir mais rápido!
Evitando honeypots
Apesar de, na maioria das vezes, o CSS facilitar bastante a vida quando se
trata de diferenciar entre informações úteis e inúteis (por exemplo, pela
leitura das tags id e class), ocasionalmente, ele pode ser um problema para
os web scrapers. Se um campo em um formulário web estiver oculto a um
usuário via CSS, é razoável supor que um usuário comum acessando o site
não seja capaz de preenchê-lo porque esse campo não será exibido no
navegador. Se o formulário for preenchido, é provável que haja um bot
atuando e o post será descartado.
Isso se aplica não só aos formulários, mas também a links, imagens,
arquivos e qualquer outro item no site que seja lido por um bot, mas esteja
oculto a um usuário comum acessando o site com um navegador. Um
acesso de página a um link “oculto” em um site pode facilmente disparar
um script do lado do servidor que bloqueará o endereço IP do usuário, fará
logout do usuário nesse site ou tomará alguma outra atitude para evitar
novos acessos. De fato, muitos modelos de negócio estão baseados
exatamente nesse conceito.
Tome, por exemplo, a página que está em
http://pythonscraping.com/pages/itsatrap.html. Essa página contém dois
links, um oculto por CSS e outro visível. Além disso, ela contém um
formulário com dois campos ocultos:
<html>
<head>
<title>A bot-proof form</title>
</head>
<style>
body {
overflow-x:hidden;
}
.customHidden {
position:absolute;
right:50000px;
}
</style>
<body>
<h2>A bot-proof form</h2>
<a href=
"http://pythonscraping.com/dontgohere" style="display:none;">Go here!</a>
<a href="http://pythonscraping.com">Click me!</a>
<form>
<input type="hidden" name="phone" value="valueShouldNotBeModified"/><p/>
<input type="text" name="email" class="customHidden"
value="intentionallyBlank"/><p/>
<input type="text" name="firstName"/><p/>
<input type="text" name="lastName"/><p/>
<input type="submit" value="Submit"/><p/>
</form>
</body>
</html>
Os três elementos a seguir estão ocultos ao usuário usando três métodos:
• O primeiro link está oculto com um simples atributo display:none do
CSS.
• O campo de número de telefone é um campo de entrada oculto.
• O campo de email está oculto por estar a 50 mil pixels à direita
(presumivelmente fora da tela dos monitores de qualquer pessoa), e a
barra de rolagem que poderia denunciá-lo está oculta.
Felizmente, como o Selenium renderiza as páginas que acessa, ele é capaz
de fazer a distinção entre os elementos visíveis na página e os que não
estão. O fato de um elemento estar visível na página pode ser determinado
com a função is_displayed().
Por exemplo, o código a seguir obtém a página descrita antes e procura
links e campos de entrada ocultos no formulário:
from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
def tearDown(self):
print('Tearing down the test')
def test_twoPlusTwo(self):
total = 2+2
self.assertEqual(4, total);
if __name__ == '__main__':
unittest.main()
Embora setUp e tearDown não ofereçam nenhuma funcionalidade útil nesse
caso, elas foram incluídas para ilustrar. Observe que essas funções são
executadas antes e depois de cada teste individual, e não antes e depois de
todos os testes da classe.
A saída da função de teste, quando executada da linha de comando, deverá
ser:
Setting up the test
Tearing down the test
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Esse resultado mostra que o teste executou com sucesso, e que 2 + 2 é
realmente igual a 4.
Executando o unittest em notebooks Jupyter
Os scripts de teste de unidade neste capítulo são todos disparados com:
if __name__ == '__main__':
unittest.main()
A linha if __name__ == '__main__' será verdadeira somente se a linha for executada
diretamente em Python, e não por meio de uma instrução de importação. Isso permite a você
executar o seu teste de unidade, com a classe unittest.TestCase que ele estende, diretamente
da linha de comando.
Em um notebook Jupyter, a situação é um pouco diferente. Os parâmetros argv criados pelo
Jupyter podem causar erros no teste de unidade; como o framework unittest, por padrão, sai
de Python depois que o teste é executado (isso causa problemas no kernel do notebook), também
é necessário evitar que isso aconteça.
Nos notebooks Jupyter, usaremos o seguinte para iniciar os testes de unidade:
if __name__ == '__main__':
unittest.main(argv=[''], exit=False)
%reset
A segunda linha define todas as variáveis de argv (argumentos da linha de comando) com uma
string vazia, que é ignorada por unnittest.main. Ela também evita que unittest saia depois
que o teste é executado.
A linha %reset é útil porque reinicia a memória e destrói todas as variáveis criadas pelo usuário
no notebook Jupyter. Sem ela, cada teste de unidade que você escrever no notebook conterá
todos os métodos de todos os testes executados antes, que também herdaram de
unittest.TestCase, incluindo os métodos setUp e tearDown. Isso também significa que cada
teste de unidade executaria todos os métodos dos testes de unidade antes dele!
Usar %reset, porém, cria um passo manual extra para o usuário na execução dos testes. Ao
executar o teste, o notebook perguntará se o usuário tem certeza de que quer reiniciar a
memória. Basta digitar y e teclar Enter para fazer isso.
Testando a Wikipédia
Testar o frontend de seu site (excluindo o JavaScript, que será discutido
depois) é simples e basta combinar a biblioteca Python unittest com um
web scraper:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import unittest
class TestWikipedia(unittest.TestCase):
bs = None
def setUpClass():
url = 'http://en.wikipedia.org/wiki/Monty_Python'
TestWikipedia.bs = BeautifulSoup(urlopen(url), 'html.parser')
def test_titleText(self):
pageTitle = TestWikipedia.bs.find('h1').get_text()
self.assertEqual('Monty Python', pageTitle);
def test_contentExists(self):
content = TestWikipedia.bs.find('div',{'id':'mw-content-text'})
self.assertIsNotNone(content)
if __name__ == '__main__':
unittest.main()
Há dois testes desta vez: o primeiro verifica se o título da página é “Monty
Python” conforme esperado, enquanto o segundo garante que a página tem
uma div de conteúdo.
Observe que o conteúdo da página é carregado apenas uma vez e o objeto
global bs é compartilhado entre os testes. Isso é feito com o uso da função
setUpClass especificada pelo unittest, a qual é executada apenas uma vez no
início da classe (de modo diferente de setUp, executada antes de cada teste
individual). Usar setUpClass em vez de setUp evita cargas desnecessárias da
página; podemos adquirir o conteúdo uma vez e executar vários testes com
ele.
Uma grande diferença de arquitetura entre setUpClass e setUp, além de
quando e com qual frequência são executadas, consiste em setUpClass ser
um método estático que “pertence” à própria classe e tem variáveis globais
de classe, enquanto setUp é uma função de instância, que pertence a uma
instância da classe em particular. É por isso que setUp pode definir atributos
em self – a instância em particular dessa classe – enquanto setUpClass pode
acessar somente atributos estáticos da classe TestWikipedia.
Embora testar uma única página de cada vez talvez não pareça tão eficaz
nem muito interessante, como você deve se lembrar do que vimos no
Capítulo 3, é relativamente fácil construir web crawlers capazes de
percorrer as páginas de um site de modo interativo. O que acontecerá se
combinarmos um web crawler com um teste de unidade que faça uma
asserção sobre cada página?
Há muitas maneiras de executar um teste de forma repetida, mas você deve
tomar cuidado e carregar cada página apenas uma vez para cada conjunto
de testes que quiser executar nela, e deve evitar também a armazenagem de
grandes quantidades de informações na memória ao mesmo tempo. A
configuração a seguir faz exatamente isso:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import unittest
import re
import random
from urllib.parse import unquote
class TestWikipedia(unittest.TestCase):
def test_PageProperties(self):
self.url = 'http://en.wikipedia.org/wiki/Monty_Python'
# Testa as 10 primeiras páginas encontradas
for i in range(1, 10):
self.bs = BeautifulSoup(urlopen(self.url), 'html.parser')
titles = self.titleMatchesURL()
self.assertEquals(titles[0], titles[1])
self.assertTrue(self.contentExists())
self.url = self.getNextLink()
print('Done!')
def titleMatchesURL(self):
pageTitle = self.bs.find('h1').get_text()
urlTitle = self.url[(self.url.index('/wiki/')+6):]
urlTitle = urlTitle.replace('_', ' ')
urlTitle = unquote(urlTitle)
return [pageTitle.lower(), urlTitle.lower()]
def contentExists(self):
content = self.bs.find('div',{'id':'mw-content-text'})
if content is not None:
return True
return False
def getNextLink(self):
# Devolve um link aleatório da página, usando a técnica mostrada na
Capítulo 3
links = self.bs.find('div', {'id':'bodyContent'}).find_all(
'a', href=re.compile('^(/wiki/)((?!:).)*$'))
randomLink = random.SystemRandom().choice(links)
return 'https://wikipedia.org{}'.format(randomLink.attrs['href'])
if __name__ == '__main__':
unittest.main()
Há alguns pontos a serem observados. Em primeiro lugar, há apenas um
teste nessa classe. As outras funções, tecnicamente, são apenas funções
auxiliares, apesar de estarem fazendo a maior parte do trabalho de
processamento para determinar se um teste passa. Como a função de teste
executa as instruções de asserção, os resultados dos testes são passados de
volta para a função na qual a asserção ocorre.
Além disso, enquanto contentExists devolve um booleano, titleMatchesURL
devolve os próprios valores para avaliação. Para ver por que deveríamos
passar os valores e não apenas um booleano, compare o resultado de uma
asserção booleana:
Qual delas é mais fácil de depurar? (Nesse caso, o erro ocorre por causa de
um redirecionamento, quando o artigo http://wikipedia.org/wiki/u-
2%20spy%20plane é redirecionado para um artigo cujo título é “Lockheed
U-2”).
print(driver.find_element_by_tag_name('body').text)
driver.close()
O Método 1 chama send_keys nos dois campos e então clica no botão de
submissão. O Método 2 usa uma única cadeia de ações para clicar e inserir
um texto em cada campo, o que acontece em uma sequência depois que o
método perform é chamado. Esse script atua do mesmo modo, não importa
se o primeiro ou o segundo método é usado, e exibe a linha a seguir:
Hello there, Ryan Mitchell!
Há outra diferença entre os dois métodos, além dos objetos que usam para
tratar os comandos: observe que o primeiro método clica no botão Submit,
e o segundo utiliza a tecla Return para submeter o formulário enquanto a
caixa de texto é submetida. Como há várias maneiras de pensar na
sequência de eventos que completam a mesma ação, há diversas maneiras
de executar a mesma ação com o Selenium.
Arrastar e soltar
Clicar em botões e inserir texto é um dos recursos, mas o Selenium
realmente se destaca na capacidade de lidar com formas relativamente
novas de interação na web. O Selenium permite manipular interfaces do
tipo arrastar-e-soltar (drag-and-drop) com facilidade. Usar sua função de
arrastar-e-soltar exige que você especifique um elemento de origem (o
elemento que será arrastado) e um offset para arrastá-lo ou um elemento-
alvo sobre o qual ele será arrastado.
A página de demonstração em
http://pythonscraping.com/pages/javascript/draggableDemo.html contém um
exemplo desse tipo de interface:
from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver import ActionChains
driver = webdriver.PhantomJS(executable_path='<Path to Phantom JS>')
driver.get('http://pythonscraping.com/pages/javascript/draggableDemo.html')
print(driver.find_element_by_id('message').text)
element = driver.find_element_by_id('draggable')
target = driver.find_element_by_id('div2')
actions = ActionChains(driver)
actions.drag_and_drop(element, target).perform()
print(driver.find_element_by_id('message').text)
Duas mensagens são exibidas na div message da página de demonstração. A
primeira contém:
Prove you are not a bot, by dragging the square from the blue area to the red
area!
Então, logo depois que a tarefa é concluída, o conteúdo é exibido mais
uma vez, no qual agora se lê:
You are definitely not a bot!
Conforme a página de demonstração sugere, arrastar elementos para
provar que você não é um bot é recorrente em muitos CAPTCHAs.
Embora os bots sejam capazes de arrastar objetos por aí há muito tempo (é
apenas uma questão de clicar, segurar e mover), de certo modo, a ideia de
usar “arraste isto” como uma verificação para saber se é um ser humano
fazendo a operação simplesmente não morre.
Além do mais, essas bibliotecas de CAPTCHAs para arrastar raramente
usam alguma tarefa difícil para os bots, como “arraste a imagem do
gatinho sobre a imagem da vaca” (que exige identificar as figuras como
“um gatinho” e “uma vaca” ao interpretar as instruções); em vez disso, em
geral, elas envolvem ordenação de números ou outra tarefa razoavelmente
trivial, como no exemplo anterior.
É claro que sua robustez está no fato de haver muitas variações e elas não
serem usadas com frequência; é improvável que alguém se preocupe em
criar um bot capaz de derrotar todas elas. De qualquer modo, esse exemplo
deve bastar para mostrar por que jamais devemos usar essa técnica em sites
de grande porte.
Capturando imagens de tela
Além dos recursos usuais de teste, o Selenium tem um truque interessante
na manga, o qual pode facilitar um pouco seus testes (ou impressionar seu
chefe): imagens de tela. Sim, evidências fotográficas da execução dos testes
de unidade podem ser geradas sem a necessidade de pressionar a tecla
PrtScn:
driver = webdriver.PhantomJS()
driver.get('http://www.pythonscraping.com/')
driver.get_screenshot_as_file('tmp/pythonscraping.png')
Esse script acessa http://pythonscraping.com e então armazena uma imagem
da tela da página inicial na pasta tmp local (a pasta já deve existir para que
a imagem seja armazenada corretamente). As imagens de tela podem ser
salvas em diversos formatos.
unittest ou Selenium?
O rigor sintático e a verbosidade do unittest Python talvez sejam desejáveis
à maioria das suítes de teste grandes, enquanto a flexibilidade e a eficácia
de um teste Selenium talvez sejam a sua única opção para testar algumas
funcionalidades dos sites. Qual deles devemos usar?
Eis o segredo: você não precisa escolher. O Selenium pode ser usado com
facilidade para obter informações sobre um site, e o unittest pode avaliar se
essas informações atendem aos critérios para passar no teste. Não há
motivos para não importar as ferramentas do Selenium no unittest de
Python, combinando o melhor dos dois mundos.
Por exemplo, o script a seguir cria um teste de unidade para uma interface
com a operação de arrastar em um site, confirmando se ele mostra que
“You are not a bot!” (Você não é um bot!) corretamente, depois que um
elemento é arrastado para outro:
from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver import ActionChains
import unittest
class TestDragAndDrop(unittest.TestCase):
driver = None
def setUp(self):
self.driver = webdriver.PhantomJS(executable_path='<Path to PhantomJS>')
url = 'http://pythonscraping.com/pages/javascript/draggableDemo.html'
self.driver.get(url)
def tearDown(self):
print("Tearing down the test")
def test_drag(self):
element = self.driver.find_element_by_id('draggable')
target = self.driver.find_element_by_id('div2')
actions = ActionChains(self.driver)
actions.drag_and_drop(element, target).perform()
self.assertEqual('You are definitely not a bot!',
self.driver.find_element_by_id('message').text)
if __name__ == '__main__':
unittest.main(argv=[''], exit=False)
Praticamente tudo em um site pode ser testado com a combinação entre o
unittest Python e o Selenium. Com efeito, se os combinarmos com algumas
das bibliotecas de processamento de imagens do Capítulo 13, podemos até
mesmo obter uma imagem da tela do site e testar o que ela deve conter,
pixel a pixel!
CAPÍTULO 16
Web Crawling em paralelo
Módulo threading
O módulo Python _thread é um módulo de nível razoavelmente baixo, que
permite administrar suas threads de forma minuciosa, mas não oferece
muitas funções de nível alto para facilitar a sua vida. O módulo threading é
uma interface de nível mais alto que permite usar threads de modo mais
organizado, ao mesmo tempo que ainda expõe todos os recursos do
módulo _thread subjacente.
Por exemplo, podemos usar funções estáticas como enumerate para obter
uma lista de todas as threads ativas inicializadas com o módulo threading
sem a necessidade de manter o controle delas por conta própria. A função
activeCount, de modo semelhante, informa o número total de threads.
Muitas funções de _thread recebem nomes mais convenientes ou fáceis de
lembrar, como currentThread em vez de get_ident para obter o nome da thread
atual.
Eis um exemplo simples de uso de threading:
import threading
import time
def print_time(threadName, delay, iterations):
start = int(time.time())
for i in range(0,iterations):
time.sleep(delay)
seconds_elapsed = str(int(time.time()) - start)
print ('{} {}'.format(seconds_elapsed, threadName))
threading.Thread(target=print_time, args=('Fizz', 3, 33)).start()
threading.Thread(target=print_time, args=('Buzz', 5, 20)).start()
threading.Thread(target=print_time, args=('Counter', 1, 100)).start()
A mesma saída “FizzBuzz” do exemplo anterior simples que usava _thread é
gerada.
Um dos aspectos interessantes sobre o módulo threading é a facilidade de
criar dados locais de thread indisponíveis a outras threads. Pode ser um
recurso conveniente se houver várias threads, cada uma fazendo scraping
de um site diferente, e cada uma mantendo a própria lista local das páginas
acessadas.
Esses dados locais podem ser criados em qualquer ponto da função de
thread com uma chamada a threading.local():
import threading
def crawler(url):
data = threading.local()
data.visited = []
# Rastreia o site
threading.Thread(target=crawler, args=('http://brookings.edu')).start()
Isso resolve o problema das condições de concorrência que ocorrem entre
objetos compartilhados nas threads. Sempre que um objeto não tiver de ser
compartilhado, não o compartilhe e mantenha-o na memória local da
thread. Para compartilhar objetos de modo seguro entre as threads,
podemos fazer uso de Queue, que vimos na seção anterior.
O módulo threading atua como uma espécie de babá de threads, e pode ser
bastante personalizado para definir o que essa atividade de babá implica. A
função isAlive, por padrão, verifica se a thread continua ativa. Será
verdadeira até uma thread acabar de fazer seu rastreamento (ou falhar).
Com frequência, os crawlers são projetados para executar por muito
tempo. O método isAlive pode garantir que, se uma thread falhar, ela seja
reiniciada:
threading.Thread(target=crawler)
t.start()
while True:
time.sleep(1)
if not t.isAlive():
t = threading.Thread(target=crawler)
t.start()
Outros métodos de monitoração podem ser acrescentados se o objeto
threading.Thread for estendido:
import threading
import time
class Crawler(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.done = False
def isDone(self):
return self.done
def run(self):
time.sleep(5)
self.done = True
raise Exception('Something bad happened!')
t = Crawler()
t.start()
while True:
time.sleep(1)
if t.isDone():
print('Done')
break
if not t.isAlive():
t = Crawler()
t.start()
Essa nova classe Crawler contém um método isDone que pode ser usado para
verificar se o crawler terminou de fazer o rastreamento. Pode ser útil se
houver alguns métodos adicionais de logging que devam ser terminados
para que a thread seja encerrada, mas o trabalho de rastreamento já tenha
sido feito. Em geral, isDone pode ser substituído por algum tipo de status ou
medida de progresso – quantas páginas foram registradas, ou a página
atual, por exemplo.
Qualquer exceção lançada por Crawler.run fará a classe ser reiniciada até
isDone ser True e o programa terminar.
Estender threading.Thread em suas classes de crawler pode melhorar sua
robustez e flexibilidade, bem como sua capacidade de monitorar qualquer
propriedade de vários crawlers ao mesmo tempo.
processes = []
processes.append(Process(target=print_time, args=('Counter', 1, 100)))
processes.append(Process(target=print_time, args=('Fizz', 3, 33)))
processes.append(Process(target=print_time, args=('Buzz', 5, 20)))
for p in processes:
p.start()
for p in processes:
p.join()
Lembre-se de que cada processo é tratado como um programa individual
independente pelo sistema operacional. Se observar seus processos com o
monitor de atividades de seu sistema operacional ou com o gerenciador de
tarefas, você deverá ver o reflexo disso, como mostra a Figura 16.1.
for p in processes:
p.join()
print('Program complete')
...
Fizz
99
Buzz
100
Program complete
Se quiser interromper prematuramente a execução do programa, é claro
que podemos usar Ctrl-C para encerrar o processo pai. O término do
processo pai também fará com que qualquer processo filho gerado a partir
dele termine, portanto usar Ctrl-C é seguro, e você não tem de se
preocupar com o fato de restar acidentalmente algum processo em
execução em segundo plano.
visited = []
def get_links(bs):
print('Getting links in {}'.format(os.getpid()))
links = bs.find('div', {'id':'bodyContent'}).find_all('a',
href=re.compile('^(/wiki/)((?!:).)*$'))
return [link for link in links if link not in visited]
def scrape_article(path):
visited.append(path)
html = urlopen('http://en.wikipedia.org{}'.format(path))
time.sleep(5)
bs = BeautifulSoup(html, 'html.parser')
title = bs.find('h1').get_text()
print('Scraping {} in process {}'.format(title, os.getpid()))
links = get_links(bs)
if len(links) > 0:
newArticle = links[random.randint(0, len(links)-1)].attrs['href']
print(newArticle)
scrape_article(newArticle)
processes = []
processes.append(Process(target=scrape_article, args=('/wiki/Kevin_Bacon',)))
processes.append(Process(target=scrape_article, args=('/wiki/Monty_Python',)))
for p in processes:
p.start()
Novamente, estamos atrasando o processo do scraper de modo artificial
incluindo um time.sleep(5), de modo que esse código seja usado como
exemplo, sem impor uma carga absurdamente alta aos servidores da
Wikipédia.
Nesse caso, estamos substituindo o thread_name definido pelo usuário,
passado como argumento, por os.getpid(), que não precisa ser passado
como argumento e pode ser acessado de qualquer lugar.
Uma saída como esta é gerada:
Scraping Kevin Bacon in process 84275
Getting links in 84275
/wiki/Philadelphia
Scraping Monty Python in process 84276
Getting links in 84276
/wiki/BBC
Scraping BBC in process 84276
Getting links in 84276
/wiki/Television_Centre,_Newcastle_upon_Tyne
Scraping Philadelphia in process 84275
Teoricamente, fazer crawling em processos separados é um pouco mais
rápido do que usar threads diferentes por dois motivos principais:
• Os processos não estão sujeitos a travar por causa do GIL, e podem
executar as mesmas linhas de código e modificar o mesmo objeto (na
verdade, instâncias diferentes do mesmo objeto) ao mesmo tempo.
• Os processos podem executar em vários núcleos (cores) de CPU, o que
pode significar vantagens quanto à velocidade se cada um de seus
processos ou threads fizer uso intenso do processador.
No entanto, essas vantagens vêm acompanhadas de uma grande
desvantagem. No programa anterior, todos os URLs encontrados são
armazenados em uma lista visited global. Quando estávamos usando várias
threads, essa lista era compartilhada entre elas; e uma thread, na ausência
de uma condição de concorrência rara, não podia acessar uma página que
já tivesse sido acessada por outra thread. No entanto, cada processo agora
tem a própria versão independente da lista de páginas acessadas, e está
livre para visitar páginas que já tenham sido acessadas por outros
processos.
def get_links(bs):
links = bs.find('div', {'id':'bodyContent'}).find_all('a',
href=re.compile('^(/wiki/)((?!:).)*$'))
return [link.attrs['href'] for link in links]
processes = []
taskQueue = Queue()
urlsQueue = Queue()
processes.append(Process(target=task_delegator, args=(taskQueue, urlsQueue,)))
processes.append(Process(target=scrape_article, args=(taskQueue, urlsQueue,)))
processes.append(Process(target=scrape_article, args=(taskQueue, urlsQueue,)))
for p in processes:
p.start()
Há algumas diferenças estruturais entre esse scraper e aqueles criados
originalmente. Em vez de cada processo ou thread seguir o próprio
percurso aleatório começando pelo ponto de partida que receberam, eles
atuam em conjunto para fazer um rastreamento completo do site. Cada
processo pode extrair qualquer “tarefa” da fila, e não apenas os links que
eles mesmos encontraram.
Rastreamento com multiprocesso – outra abordagem
Todas as abordagens discutidas para rastreamento com várias threads e
com vários processos partem do pressuposto de que você precisa de
alguma espécie de “orientação parental” sobre as threads e processos
filhos. Podemos iniciar ou terminar todos de uma só vez, e podemos enviar
mensagens ou compartilhar memória entre eles.
Mas e se seu scraper for projetado de modo que nenhuma orientação ou
comunicação seja necessária? Talvez ainda não haja muitos motivos para
começar a enlouquecer com import _thread.
Por exemplo, suponha que você queira rastrear dois sites semelhantes em
paralelo. Temos um crawler implementado, capaz de rastrear qualquer um
desses sites, e isso é determinado por uma pequena diferença na
configuração ou talvez por um argumento na linha de comando. Não há
absolutamente motivo algum para que você não possa fazer simplesmente
o seguinte:
$ python my_crawler.py website1
$ python my_crawler.py website2
E voilà, você acabou de iniciar um web crawler com multiprocesso, ao
mesmo tempo em que evitou um overhead na CPU para manter um
processo pai a fim de iniciá-los!
É claro que essa abordagem tem suas desvantagens. Se quiser executar dois
web crawlers no mesmo site dessa maneira, será necessário ter alguma
forma de garantir que eles não comecem acidentalmente a fazer scraping
das mesmas páginas. A solução poderia ser a criação de uma regra de URL
(“o crawler 1 faz scraping das páginas de blog, o crawler 2 faz scraping das
páginas de produto”) ou dividir o site de alguma maneira.
Como alternativa, poderíamos cuidar dessa coordenação com algum tipo
de banco de dados intermediário. Antes de acessar um novo link, o crawler
poderia fazer uma requisição ao banco de dados para perguntar: “Essa
página já foi rastreada?”. O crawler usa o banco de dados como um sistema
de comunicação entre processos. É claro que, sem uma apreciação
cuidadosa, esse método pode resultar em condições de concorrência ou em
atrasos se a conexão com o banco de dados for lenta (provavelmente só
será um problema se a conexão for feita com um banco de dados remoto).
Talvez você perceba também que esse método não é muito escalável. Usar
o módulo Process permite aumentar ou diminuir dinamicamente o número
de processos rastreando o site, ou até mesmo armazenando dados. Iniciá-
los manualmente exige que uma pessoa execute fisicamente o script ou que
haja um script de gerenciamento separado (seja um script bash, um cron
job ou outro meio) para isso.
No entanto, esse é um método que já usei com muito sucesso no passado.
Para projetos pequenos, executados uma só vez, é uma ótima maneira de
obter muitas informações com rapidez, sobretudo se houver vários sites.
CAPÍTULO 17
Fazendo scraping remotamente
No último capítulo, vimos como executar web scrapers com várias threads
e processos, em que a comunicação estava um tanto quanto limitada ou
tinha de ser planejada com cuidado. Este capítulo leva esse conceito à sua
conclusão lógica – executar crawlers não só em processos separados, mas
em máquinas totalmente distintas.
O fato de este capítulo ser um dos últimos do livro, de certo modo, é
apropriado. Até agora, vínhamos executando todas as aplicações Python a
partir da linha de comando, confinados ao seu computador local. É claro
que você pode ter instalado o MySQL em uma tentativa de reproduzir o
ambiente de um servidor na vida real. A situação, porém, não é a mesma.
Como diz o ditado: “Se você ama alguém, conceda-lhe a liberdade”.
Este capítulo descreve vários métodos para executar scripts a partir de
máquinas diferentes, ou apenas de endereços IP distintos em sua própria
máquina. Embora possa se sentir tentado a menosprezar este passo como
não necessário neste momento, você poderia se surpreender ao saber como
é fácil começar a trabalhar com as ferramentas de que já dispõe (por
exemplo, um site pessoal em uma conta de hospedagem paga), e como sua
vida será muito mais simples se você parar de tentar executar seus scrapers
Python em seu notebook.
Portabilidade e extensibilidade
Algumas tarefas são grandes demais para um computador doméstico e uma
conexão com a internet. Ainda que não queira impor uma carga pesada a
um único site, você poderia coletar dados de vários deles e precisar de
muito mais largura de banda e área de armazenagem do que sua
configuração atual é capaz de oferecer.
Além disso, ao se livrar de cargas intensas de processamento, ciclos de
máquina de seu computador doméstico estarão livres para tarefas mais
importantes (alguém aí quer jogar World of Warcraft?) Não será preciso se
preocupar em economizar energia elétrica nem manter uma conexão com a
internet (inicie sua aplicação em um Starbucks, pegue o notebook e saiba
que tudo continuará executando de modo seguro), e os dados coletados
poderão ser acessados de qualquer lugar em que houver uma conexão com
a internet.
Se você tiver uma aplicação que exija muita capacidade de processamento
a ponto de um único computador extragrande não o satisfazer, dê uma
olhada também no processamento distribuído. Isso permite que várias
máquinas trabalhem em paralelo para que seus objetivos sejam atingidos.
Como um exemplo simples, poderíamos ter um computador rastreando
um conjunto de sites e outro rastreando um segundo conjunto, e ambos
armazenariam os dados coletados no mesmo banco de dados.
É claro que, conforme mencionamos nos capítulos anteriores, muitas
pessoas podem imitar o que uma pesquisa do Google faz, mas poucas
serão capazes de replicar a escala com que ela é feita. O processamento
distribuído envolve uma área grande da ciência da computação e está além
do escopo deste livro. No entanto, aprender a iniciar sua aplicação em um
servidor remoto é um primeiro passo necessário, e você se surpreenderá
com o que os computadores são capazes de fazer hoje em dia.
Tor
A rede Onion Router (Roteador Cebola), mais conhecida pelo acrônimo
Tor, é uma rede de servidores voluntários, configurada para encaminhar e
reencaminhar tráfego por várias camadas (daí a referência à cebola) de
diferentes servidores a fim de ocultar sua origem. Os dados são
criptografados antes de entrarem na rede, de modo que, caso algum
servidor em particular seja espionado, a natureza da comunicação não
poderá ser revelada. Além disso, embora as comunicações de entrada e de
saída de qualquer servidor em particular possam ser comprometidas, seria
necessário que alguém conhecesse os detalhes da comunicação de entrada
e de saída de todos os servidores no percurso da comunicação para decifrar
os verdadeiros pontos inicial e final de uma comunicação – uma proeza
quase impossível.
O Tor é comumente usado por quem trabalha com direitos humanos e
denúncias políticas para se comunicar com jornalistas, e recebe boa parte
de seus financiamentos do governo norte-americano. É claro que ele
também é frequentemente utilizado para atividades ilegais, sendo, desse
modo, alvo constante da vigilância do governo (apesar disso, até hoje, a
vigilância teve sucesso apenas parcial).
PySocks
O PySocks é um módulo Python excepcionalmente simples, capaz de
encaminhar tráfego por meio de servidores proxy, e funciona de modo
incrível em conjunto com o Tor. Você pode fazer seu download a partir do
site (https://pypi.org/project/PySocks/1.5.0/) ou usar qualquer um dos
gerenciadores de módulos de terceiros para instalá-lo.
Apesar de não haver muita documentação para esse módulo, é muito
simples usá-lo. O serviço Tor deve estar executando na porta 9150 (a porta
default) enquanto o código a seguir é executado:
import socks
import socket
from urllib.request import urlopen
socks.set_default_proxy(socks.SOCKS5, "localhost", 9150)
socket.socket = socks.socksocket
print(urlopen('http://icanhazip.com').read())
O site http://icanhazip.com exibe apenas o endereço IP do cliente conectado
ao servidor, e pode ser útil para testes. Quando for executado, esse script
deverá exibir um endereço IP que não é o seu.
Se quiser usar o Selenium e o PhantomJS com o Tor, não será necessário
ter o PySocks – basta garantir que o Tor esteja executando e acrescentar os
parâmetros opcionais em service_args, especificando que o Selenium deve
se conectar por meio da porta 9150:
from selenium import webdriver
service_args = [ '--proxy=localhost:9150', '--proxy-type=socks5', ]
driver = webdriver.PhantomJS(executable_path='<path to PhantomJS>',
service_args=service_args)
driver.get('http://icanhazip.com')
print(driver.page_source)
driver.close()
Mais uma vez, esse código deve exibir um endereço IP que não é o seu, mas
aquele que seu cliente Tor em execução está usando no momento.
Hospedagem remota
Apesar de um anonimato completo ter deixado de existir assim que você
usou seu cartão de crédito, hospedar seus web scrapers remotamente pode
melhorar muito a sua velocidade. Isso ocorre tanto porque você pode
comprar tempo em máquinas provavelmente muito mais potentes que a
sua, mas também porque a conexão não precisa mais passar pelas camadas
de uma rede Tor para alcançar seu destino.
Recursos adicionais
Muitos anos atrás, executar “na nuvem” era essencialmente o domínio
daqueles que estivessem dispostos a mergulhar fundo na documentação e
já tivessem alguma experiência com administração de servidores. Hoje em
dia, porém, as ferramentas melhoraram bastante em razão da maior
popularidade e da concorrência entre provedores de computação na
nuvem.
Apesar disso, para construir scrapers e crawlers de grande porte ou mais
complexos, talvez você queira mais orientações sobre como criar uma
plataforma para coletar e armazenar dados.
O livro Google Compute Engine de Marc Cohen, Kathryn Hurley e Paul
Newson (O’Reilly, http://oreil.ly/1FVOw6y) é um recurso simples que
descreve como usar o Google Cloud Computing tanto com Python como
com JavaScript. Ele não só aborda a interface de usuário do Google como
também as ferramentas de linha de comando e de scripting que podem ser
usadas para dar mais flexibilidade à sua aplicação.
Se preferir trabalhar com a Amazon, o livro Python and AWS Cookbook de
Mitch Garnaat (O’Reilly, http://oreil.ly/VSctQP) é um guia conciso, porém
muito útil, que lhe permitirá trabalhar com o Amazon Web Services e
mostrará como ter uma aplicação escalável pronta e executando.
1 Tecnicamente, os endereços IP podem ser forjados em pacotes de saída; essa é uma técnica usada
em ataques distribuídos de negação de serviço (distributed denial-of-service attacks), em que os
invasores não se preocupam em receber pacotes de volta (os quais, se houver, serão enviados para
o endereço errado). Por definição, porém, o web scraping é uma atividade na qual uma resposta do
servidor web é necessária, portanto consideramos que os endereços IP são uma informação que
não pode ser falsificada.
CAPÍTULO 18
Aspectos legais e éticos do web scraping
User-agent: Googlebot
Allow: *
Disallow: /private
Nesse caso, todos os bots são proibidos em qualquer parte do site, exceto o
Googlebot, que tem permissão para qualquer lugar, com exceção do
diretório /private.
O arquivo robots.txt do Twitter tem instruções explícitas para os bots do
Google, Yahoo!, Yandex (uma ferramenta de pesquisa popular na Rússia),
Microsoft e outros bots ou ferramentas de pesquisa não incluídos nas
categorias anteriores. A seção do Google (que parece idêntica às
permissões para todas as demais categorias de bots) tem o seguinte
aspecto:
#Google Search Engine Robot
User-agent: Googlebot
Allow: /?_escaped_fragment_
Allow: /?lang=
Allow: /hashtag/*?src=
Allow: /search?q=%23
Disallow: /search/realtime
Disallow: /search/users
Disallow: /search/*/grid
Disallow: /*?
Disallow: /*/followers
Disallow: /*/following
Observe que o Twitter restringe o acesso às partes de seu site para as quais
há uma API. Como o Twitter tem uma API bem organizada (e uma com a
qual ele pode ganhar dinheiro licenciando), é interesse da empresa não
permitir que haja qualquer “API caseira” que colete informações
rastreando o site de modo independente.
Embora um arquivo informando ao seu crawler quais são as partes que ele
não pode acessar pareça restritivo à primeira vista, talvez seja uma bênção
disfarçada para o desenvolvimento de um web crawler. Se você encontrar
um arquivo robots.txt que não permita o rastreamento de uma parte
específica do site, o webmaster está essencialmente dizendo que aceita
crawlers em todas as demais seções (afinal de contas, se não concordasse
com isso, não teria restringido o acesso quando escreveu o robots.txt, para
começar).
Por exemplo, a seção do arquivo robots.txt da Wikipédia que se aplica aos
web scrapers em geral (em oposição às ferramentas de pesquisa) é
extremamente permissiva. Ela chega a conter um texto legível aos seres
humanos para dar boas-vindas aos bots (somos nós!) e bloqueia o acesso
somente a algumas páginas, como a página de login, a página de pesquisa e
a “Página aleatória” (random article):
#
# Friendly, low-speed bots are welcome viewing article pages, but not
# dynamically generated pages please.
#
# Inktomi's "Slurp" can read a minimum delay between hits; if your bot supports
# such a thing using the 'Crawl-delay' or another instruction, please let us
# know.
#
# There is a special exception for API mobileview to allow dynamic mobile web &
# app views to load section content.
# These views aren't HTTP-cached but use parser cache aggressively and don't
# expose special: pages etc.
#
User-agent: *
Allow: /w/api.php?action=mobileview&
Disallow: /w/
Disallow: /trap/
Disallow: /wiki/Especial:Search
Disallow: /wiki/Especial%3ASearch
Disallow: /wiki/Special:Collection
Disallow: /wiki/Spezial:Sammlung
Disallow: /wiki/Special:Random
Disallow: /wiki/Special%3ARandom
Disallow: /wiki/Special:Search
Disallow: /wiki/Special%3ASearch
Disallow: /wiki/Spesial:Search
Disallow: /wiki/Spesial%3ASearch
Disallow: /wiki/Spezial:Search
Disallow: /wiki/Spezial%3ASearch
Disallow: /wiki/Specjalna:Search
Disallow: /wiki/Specjalna%3ASearch
Disallow: /wiki/Speciaal:Search
Disallow: /wiki/Speciaal%3ASearch
Disallow: /wiki/Speciaal:Random
Disallow: /wiki/Speciaal%3ARandom
Disallow: /wiki/Speciel:Search
Disallow: /wiki/Speciel%3ASearch
Disallow: /wiki/Speciale:Search
Disallow: /wiki/Speciale%3ASearch
Disallow: /wiki/Istimewa:Search
Disallow: /wiki/Istimewa%3ASearch
Disallow: /wiki/Toiminnot:Search
Disallow: /wiki/Toiminnot%3ASearch
A decisão de escrever web crawlers que obedeçam ao arquivo robots.txt
cabe a você, mas recomendo enfaticamente que isso seja feito, sobretudo se
seus crawlers rastreiam a web de modo indiscriminado.
Seguindo em frente
A internet está mudando constantemente. As tecnologias que nos trazem
imagens, vídeos, texto e outros arquivos de dados estão sendo atualizadas e
reinventadas o tempo todo. Para acompanhar esse ritmo, o conjunto de
tecnologias usado para scraping de dados da internet também deve mudar.
Não sabemos o que pode acontecer. Versões futuras deste texto poderão
omitir por completo o JavaScript, considerando-o uma tecnologia obsoleta
e de uso raro e, em vez disso, poderia ter como foco o parsing de
hologramas HTML8. No entanto, o que não mudará é o modo de pensar e
a abordagem geral necessários para coletar dados de qualquer site com
sucesso (ou o que quer que seja usado como “sites” no futuro).
Ao deparar com qualquer projeto de web scraping, sempre faça as
seguintes perguntas:
• Qual é a pergunta que eu quero que seja respondida, ou qual é o
problema que eu quero que seja resolvido?
• Quais dados me ajudarão a fazer isso e onde estão?
• Como o site exibe esses dados? Sou capaz de identificar exatamente
qual parte do código do site contém essas informações?
• Como posso isolar os dados e obtê-los?
• Que tipo de processamento ou análise devem ser feitos para deixar os
dados mais convenientes?
• Como posso tornar esse processo melhor, mais rápido e mais robusto?
Além do mais, é necessário aprender não só a usar as ferramentas
apresentadas neste livro de forma isolada, mas saber como podem atuar em
conjunto para resolver um problema maior. Às vezes, os dados estarão
facilmente disponíveis e bem formatados, permitindo que um scraper
simples dê conta do serviço. Em outras ocasiões, será preciso pôr a cabeça
para funcionar.
No Capítulo 11, por exemplo, combinamos a biblioteca Selenium para
identificar imagens carregadas com Ajax na Amazon, e o Tesseract para
usar OCR a fim de lê-las. No problema do Six Degrees of Wikipedia,
usamos expressões regulares para escrever um crawler que armazenava
informações de links em um banco de dados, e então utilizamos um
algoritmo de resolução de grafos para responder à pergunta: “Qual é o
caminho mais curto com links entre Kevin Bacon e Eric Idle?”.
Raramente haverá um problema sem solução quando se trata de coletar
dados de modo automático na internet. Basta lembrar que a internet é uma
API gigante, com uma interface de usuário um tanto quanto precária.
1 Bryan Walsh, “The Surprisingly Large Energy Footprint of the Digital Economy [UPDATE]” (O
surpreendentemente elevado consumo de energia na economia digital, http://ti.me/2IFOF3F),
TIME.com, 14 de agosto de 2013.
Python para análise de dados
McKinney, Wes
9788575227510
616 páginas