Desenvolvimento

24 nov, 2017

Web Scraping com Node.js – Parte 02

Publicidade

Esse artigo é uma continuação do Webscrapping com Node.js que escrevi no início de outubro. Crawlers e web scrapers são peças fundamentais para a construção de mecanismos de busca e já faz algum tempo que estava para escrever sobre eles, a pedido de leitores e amigos.

Isso porque crio tais bots há cerca de 7 anos, desde que comecei o projeto do Busca Acelerada pela primeira vez e de lá para cá não parei, tendo lançado diversos projetos para diversas empresas, como o BuildIn para a Softplan, e até mesmo alguns projetos pessoais, como o SóFamosos.

Mas por que uma parte 2? Não ensinei tudo que tinha de ensinar na parte 1?

O grande problema do Web Scraping

Quase. O web scraper que ensinei a criar na primeira parte deste artigo é bem básico e sofre de alguns males bem comuns, que quem já deve ter feito web scraping antes deve conhecer: HTML dinâmico e execução de scripts.

Claro, existem outros problemas também, como o bloqueio do bot após crawling excessivo, mas considerando que – quase a totalidade dos sites atuais possuem execução de scripts para tornar suas páginas dinâmicas – é quase certo que você, mais cedo ou mais tarde, terá problemas com o seu web scraper enxergando um HTML x, e você no navegador enxergando um HTML y, o que dificultará seu trabalho de capturar os dados corretamente.

Explicando melhor: quando você acessa um site via servidor, usando um script como o web scraper do artigo anterior, você não executa o código front-end da página, não manipula o DOM final, não dispara as chamadas Ajax, etc. Seu robô apenas lê as informações que foram geradas no servidor do site-alvo, o que pode fazer com que seu robô nem tenha nada de útil para ler na página, caso a mesma se construa usando Ajax.

A solução: Headless browsers

Para contornar este problema, usamos um tipo especial de browser, chamado headless (“sem cabeça”).

Os web browsers, como conhecemos, possuem três tarefas principais:

  • Dada uma URL, acessar uma página HTML (funcionalidade principal, o tronco do browser);
  • Carregar o DOM e executar os scripts front-end (os membros);
  • Renderizar visualmente o DOM (a cabeça);

Os primeiros browsers antigos, que eram em modo texto, só faziam o primeiro item. O browser que você está usando para ler este artigo faz os três. Um headless browser faz somente os dois primeiros, ou seja, ele apenas não possui uma interface gráfica, mas carrega o HTML, o DOM e executa todos os scripts de front-end.

Esse tipo de browser é como se fosse um console application rodando em modo silencioso, apenas um processo no seu sistema operacional que age como um browser. No fundo ele é um browser, apenas não tem UI.

Muito útil para testes automatizados de interface, é comum os headless browsers exporem bibliotecas de manipulação do DOM, como JQuery, para permitir que através de scripts o programador consiga usar a página carregada pelo headless browser, como executar cliques em botões, preencher campos, etc. E tudo isso sem estar enxergando uma UI, ou sequer estar usando mouse e teclado.

Não é preciso ser um gênio para entender que podemos usar headless browsers para fazer web scraping, certo? Com esse tipo de tecnologia conseguiremos que nosso bot leia a versão final do HTML, da mesma forma que um usuário “veria” no navegador, após a execução dos scripts de front-end. E não é só isso, você consegue fazer com que seu bot não apenas leia conteúdo, mas que use a página em si, clicando em botões e selecionando opções, por exemplo.

Se DOM, HTML, front-end e outros termos utilizados aqui não forem de seu domínio, dificilmente você conseguirá criar um crawler realmente bom. Em meu livro de Programação Web com Node.js, cubro estes e outros aspectos essenciais da programação web, caso lhe interesse.

Escolhendo um headless browser

Existem diversas opções de headless browsers no mercado. A maioria é open-source e baseada no Webkit, a mesma base na qual o Apple Safari e o Google Chrome foram construídos, o que garante uma grande fidelidade em relação ao uso de web browser comuns para carregar o HTML+JS.

Algumas opções disponíveis, são:

Destes, o SlimerJS usa a plataforma Gecko da Mozilla ao invés do Webkit, enquanto o PhantomJS e o HtmlUnit são famosos por se integrarem ao Selenium, uma popular ferramenta de automação de testes. Além disso, o Phantom é o único que eu usei profissionalmente em uma startup que trabalhei que usava este headless browser para tirar screenshots de páginas web.

No entanto, meu intuito ao escrever este artigo era justamente para aprender a usar outro headless browser, que não o Phantom, pois este possui alguns problemas conhecidos, como não-conformidade com ES6, obrigatoriedade de uso do JQuery e nenhuma novidade desde 2016 (o projeto está parado). Esses problemas (que não são o fim do mundo, mas que me motivaram a buscar outra tecnologia) são comentados neste excelente artigo de Matheus Rossi. Então se você realmente quer aprender a usar o PhantomJS, leia o artigo que acabei de citar.

Uma coisa que descobri recentemente, é que desde a versão 59 do navegador Google Chrome que foi disponibilizada a opção de rodar o navegador do Google sem a interface gráfica, funcionando em modo headless. Usar o Chrome em modo headless é o sonho de muito testador por aí, pois representa rodar scripts de testes automatizados no navegador mais popular do mundo, com a maior fidelidade possível em relação ao que o usuário final vai usar.

Certo, headless browser escolhido, como fazer um webscrapper que use-o para carregar o mesmo DOM que um usuário do Chrome veria?

Manipulando o headless browser via Node.js

No mesmo Github do Google Chrome existe um projeto chamado Puppeteer, que em uma tradução livre seria um manipulador de marionetes, ou um mestre dos fantoches. Ele é essencialmente um módulo Node.js que permite manipular tanto o Headless Chrome quanto o Chrome “normal” para fazer tarefas, como:

  • Gerar screenshots e PDFs de páginas web;
  • Fazer crawling em cima de páginas com Ajax e SPAs;
  • Automatizar o uso de UIs web;
  • Fazer Web Scraping com alta fidelidade;

Para que o Puppeteer funcione, você precisa ter o Node.js versão 8+ instalado na sua máquina. Depois que tiver o Node instalado, criaremos um novo projeto Node.js chamado Web Scrapper 2, criar um index.js vazio dentro dele, navegar via terminal até essa pasta e rodar um ‘npm init’ para configurá-lo com as opções default.

Se você nunca programou em Node.js antes, sugiro começar pelos tutoriais para iniciantes do meu blog ou pelo meu livro, que pega o iniciante pela mão e ensina desde JS tradicional, até diversos módulos do Node.

Agora rode o comando abaixo para instalar o Puppeteer no seu projeto, sendo que ele automaticamente irá baixar sua própria versão do Chrome, independente se você já possuir uma (ou seja, a instalação irá demorar um pouco mais que o normal para módulos NPM):

> npm install -S puppeteer

Usá-lo é muito simples. No seu index.js, inclua o seguinte código que representa a estrutura básica do nosso web scraper:

const puppeteer = require('puppeteer')

let scrape = async () => {
  const browser = await puppeteer.launch({headless: true});
  const page = await browser.newPage()
  await page.goto('http://books.toscrape.com/')
  await page.waitFor(1000)
  // Scrape
  browser.close()
  return result
};

scrape().then((value) => {
    console.log(value) // sucesso!
});

Na primeira linha carregamos a dependência do módulo puppeteer. No bloco seguinte, criamos a função assíncrona que fará todo o trabalho de verdade, com a lógica de acesso à página e scraping (comentado).

Neste primeiro momento, essa função scrape apenas executa o Headless Chrome, acessa uma nova página com a URL de um site fake de livros e espera 1 segundo antes de se fechar. Ainda não há a lógica de scrapping aqui.

No bloco final, executamos a função e quando ela terminar, o valor retornado pela função scrape será impresso no console.

Antes de fazer a versão completa da função scrape, temos de entender o que vamos capturar de informações da página em questão.

Fazendo HTML Scrapping

O site Books to Scrape serve para o único propósito de ajudar desenvolvedores a praticar web scraping. Ele mostra um catálogo de livros, como na imagem abaixo:

Books to Scrape

Acesse ele no seu navegador Chrome e com o clique direito do mouse sobre a área do primeiro livro da listagem, mais especificamente sobre o título dele, selecione a opção Inspecionar/Inspect.

Isso abrirá a aba Elements do Google Developer Tools, onde você poderá entender como construir um seletor que diga ao seu web scraper onde estão os dados que ele deve ler. Neste caso, vamos ler apenas os nomes dos livros. Se você não está acostumado com CSS e/ou seletores (no melhor estilo JQuery), você pode usar o recurso Copy > Selector que aparece no painel elements para gerar o seletor para você, como mostra a imagem abaixo.

Copiando o selector

Nesse caso específico não usarei a sugestão do Chrome, que é bem engessada. Usarei um seletor bem mais simples: “h3 > a”, ou seja, vou pegar o link/âncora de cada H3 do HTML dessa página. Os H3 dessa página são exatamente os títulos dos livros.

Se analisarmos em detalhes este bloco que queremos extrair o título, veremos que o texto interno da âncora nem sempre é o título fiel, pois, por uma questão de espaço, algumas vezes ele está truncado com reticências no final. Sendo assim, o atributo HTML que vamos ler com o scraper é o title da âncora, que esse sim sempre possui o título completo.

Vamos atualizar nosso código de scrapping para pegarmos esta informação:

const puppeteer = require('puppeteer')

let scrape = async () => {
  const browser = await puppeteer.launch({headless: true})
  const page = await browser.newPage()
  await page.goto('http://books.toscrape.com/')
  await page.waitFor(1000)
  
  // Scrape
  const result = await page.evaluate(() => {
    return document.querySelector('h3 > a').title
  })
  
  browser.close()
  return result
};

scrape().then((value) => {
    console.log(value) // sucesso!
})

Analisando apenas o bloco de código de scraping, criamos uma função que executa um código JavaScript sobre a página que carregamos. A execução de scripts é realizada usando a função evaluate. Nesse código JavaScript, estamos usando a função querySelector no documento e o seletor que criamos anteriormente para acessar um componente do DOM da página. Sobre este componente, queremos o seu atributo title.

Se você rodar este código Node agora, verá que ele retornará apenas o nome do primeiro livro, “A Light in the Attic”. Isso porque a função querySelector retorna apenas o primeiro componente que ela encontra com aquele seletor.

O código abaixo resolve isso:

const puppeteer = require('puppeteer')

let scrape = async () => {
  const browser = await puppeteer.launch({headless: true})
  const page = await browser.newPage()
  await page.goto('http://books.toscrape.com/')
  await page.waitFor(1000)
  
  // Scrape
  const result = await page.evaluate(() => {
    const books = []
    document.querySelectorAll('h3 > a').forEach(book => books.push(book.title))
    return books
  });
  
  browser.close()
  return result
};

scrape().then((value) => {
    console.log(value) // sucesso!
})

Aqui, usei a querySelectorAll, sobre a qual apliquei um forEach. Para cada livro encontrado com aquele seletor, eu adiciono o title do livro em um array de livros, retornado ao fim do evaluate.

Com isso, se você executar novamente este script, verá que ele retorna o título de todos os livros da primeira página do site, concluindo este artigo.

Qual seria o próximo passo? Navegar pelas demais páginas, coletando os títulos dos outros livros. Esta é a tarefa principal de um webcrawler ou webspider, to crawl (rastejar). Ao algoritmo de webscrapping, cabe apenas coletar os dados.

Até a próxima!