Web Scraping com NodeJs e PhantomJs

Extraindo dados de uma página web (supermercado) e disponibilizando para consumo.

Um conceito importante antes de começarmos, é entendermos a diferença entre Crawling e Scraping. Basicamente, fazer o Scraping de uma página, é extrair as informações de forma estruturada, para que seja consumida por alguma função do seu aplicativo/site. Já um crawler, tem como função capturar e disponibilizar todos os links de um determinado domínio e algumas informações (é isto que o google faz, por exemplo).

As ferramentas aqui utilizadas cumpriram seu propósito, a informação foi devidamente capturada, porém existiram vários problemas durante o processo, dos quais algumas “soluções” não me agradaram muito.

Primeira tentativa ( axios e cheerio )

Pesquisa daqui, pesquisa dali, vi algumas publicações a respeito e vi alguns exemplos que utilizavam estas duas libs. Como já utilizo axios, tentei seguir esse caminho, o qual acabei descobrindo possuir um limitação crucial.

Qual o problema?

O axios realiza uma chamada http para o endereço desejado e retorna a chamada. Isto nos dá acesso a todo o código HTML, porém como ele não é um browser, o código javascript dentro do documento não é executado , assim sendo, caso alguma informação dentro do site seja disponibilizada por chamadas http (dinamicamente), o conteúdo não estará disponível para ser utilizado dentro pelo cheerio.

E era esse exatamente o meu caso, através do axios eu tinha acesso a todo o body do site, exceto as informações de descrição do produto e valores, os quais a página os solicitava via javascript para alguma API.

E agora batman?

A solução foi utilizar um navegador headless, um browser sem “visualização”, sem interface gráfica. Assim, eu poderia acessar a página, este browser executaria o javascript e me tornaria possível acessar estas informações.

Mais pesquisas e cheguei no PhantomJs, que também possuia uma lib (node-horseman) que permitia o acesso à esse “browser” via NodeJs. Parecia a solução dos meus problemas e de certo modo foi, mas com novas abordagens, vem novos problemas.

PhantomJs e node-horseman

Instalei o phantomjs pelo repositório AUR do Arch Linux, iniciei um novo projeto, instalei o node-horseman e comecei a fazer os testes com as ferramentas.

yaourt  -phantomjs #Instalar Phantomjs
mkdir node-horseman #Criar pasta para o projeto
npm init #Inicar projeto com npm
npm i -- save node-horseman #Instalar node-horseman
touch scrapeAllPages.js #Criar arquivo que conterá o código

Tudo instalado e a primeira surpresa aparece, a lib utiliza JQuery para extrair os dados da página.

Sei que o JQuery ainda é muito utilizado, porém sempre tento fugir dele e construir a soluções com js puro. Posteriormente o phantomjs começou a demonstrar problemas quanto a compatibilidade com o ES6, talvez por isso ela venha por padrão com JQuery como método para extração de dados.

Entendendo o Projeto

Extrair descrição e valores de produtos de uma página de supermercado da região.

Para evitar maiores problemas, vou substituir o endereço da página por apenas URL, então onde virem esta sigla, substituam pelo endereço desejado.

Entendendo a lib node-horseman

A documentação está disponível apenas no github, a ferramenta funciona com base em Promises, e estudei seu funcionamento com base nestes exemplos:

A seleção da informação que desejamos extrair será feitas através de tag’s HTML ou classes CSS, então precisamos pensar em um modo de selecionar o conteúdo desejado estruturando a chamada com essa premissa.

Vamos conferir o site? Preste atenção nos elementos destacados da página abaixo:

Site do qual pretendemos extrair as informações

Para nossa sorte, o site está estruturado de uma forma perfeita para fazermos o scraping da página, deste modo:

Dentro do arquivo scrapeAllPages.js que criamos:

var Horseman = require('node-horseman')
var horseman = new Horseman()
var fs = require('fs')
var finalData = []
function getdata() {
return horseman.evaluate(function(){

var descNode = document.querySelectorAll('.descr a')
var desc = Array.prototype.map.call(descNode, function (t) { return t.textContent })
    var valueNode = document.querySelectorAll('.price a')
var value = Array.prototype.map.call(valueNode, function (t) { return t.textContent })
    var finalData = []
    for (var i=0 ; i < desc.length; i ++) {
var item = {}
item['desc'] = desc[i]
item['value'] = value[i]
finalData.push(item)
}
return finalData
})
}

O que fazemos aqui, é selecionar todos os itens que tenham uma tag a dentro de uma classe .descr.price, posteriormente fazemos um .map() retornando apenas o textContent dos nodes selecionados.

Sim, esta sintaxe não está nada agradável aos meus olhos também, mas lembram que eu falei sobre problemas com compatibilidade com ES6 ?

Aqui tivemos a segunda decepção

Utilizar const e let, simplesmente quebravam o código, retornando um erro que não ajudava no console.log. Também foi este o motivo por ter usado o for, e não um .forEach() ou mesmo ou .map() da vida.

Segue a vida …

Continuando os testes, feito estas alteraões, consegui extrair os dados da primeira página, mas se perceberam, existem 40 páginas com as promoções … A solução é após cada scraping, “clicar” no link (caso exista) e realizar um novo, scraping, e assim sucessivamente. Mas como ? Assim:

No inicio, o código pode parecer meio confuso, porém a lógica é simples:

  1. A função scrape é chamada
  2. Retornamos uma promise
  3. Retornamos o valor da função scrape (coloquei um console.log aqui para acompanhar no terminar se está tudo indo ok).
  4. Verificamos se o link para próxima página existe .lnkPagNext
  5. Se existir, clicamos no link e aguardamos 3s para o conteúdo ser carregado, caso contrário, a função acaba aqui.

Funções OK, é hora de chamar o horseman e fazer a mágica acontecer:

horseman
.on('consoleMessage', function(msg){
console.log(msg)
})
.open('http://www.xxxxx.com.br/super/index')
.then(scrape)
.finally(function() {
fs.writeFile('xxxxxData.txt', JSON.stringify(finalData), (err) => {
if (err) throw err
console.log('The file has been saved!')
horseman.close()
})
})

Aqui utilizo o fs para gravar um arquivo no sistema com as informações extraídas do site.

Então, de dentro da pasta criada rodamos o seguinte comando:

DEBUG=horseman node scrapAllPages.js

O uso do DEBUG=horseman é optativo, mas ajuda a entender o fluxo e a localizar possíveis erros.

E a mágica acontece:

Após todas as operações, um arquivo txt é gerando dentro da pasta com todas as informações desejadas. Dentro do meu github, também temos um exemplo de como utilizar estes dados (arquivo useDataScraped.js), neste caso, apenas os imprimindo na tela as cervejas em promoção (caso existam).

As promoções que foram retiradas da página web

Conclusões

O objetivo inicial foi atingido, sendo que os dados foram extraídos com sucesso da página, porém, durante todo o aprendizado com o phantomJs, foi possível perceber que o projeto esta abandonado, tendo apenas 01 release em 2015, 02 releases em 2016 e nenhum em 2017.

Muitas features do ES6 não funcionam aqui, tornando o processo um tanto quanto lento, pois foi necessário ficar pensando em modos de escrever o mesmo código em ES5.

Após mais pesquisas, encontrei a seguinte ferramenta, que roda no chrome headless e está em constante desenvolvimento:

Sugiro a quem precise desenvolver um WebScraping com node, a tentar outras opções, visto que o phantomJs tem demonstrado certas limitações e não está em desenvolvimento.

Abraços pessoal.