Web Scraping com um navegador sem cabeça: um tutorial de marionetista
Publicados: 2022-03-11Neste artigo, veremos como é fácil executar web scraping (automação da web) com o método não tradicional de usar um navegador headless .
O que é um navegador sem cabeça e por que é necessário?
Nos últimos anos, a web evoluiu de sites simplistas construídos com HTML e CSS simples. Agora, existem aplicativos da Web muito mais interativos com belas interfaces de usuário, que geralmente são criadas com estruturas como Angular ou React. Em outras palavras, hoje em dia o JavaScript governa a web, incluindo quase tudo com que você interage nos sites.
Para nossos propósitos, JavaScript é uma linguagem do lado do cliente. O servidor retorna arquivos ou scripts JavaScript injetados em uma resposta HTML e o navegador o processa. Agora, isso é um problema se estivermos fazendo algum tipo de web scraping ou automação da web porque, na maioria das vezes, o conteúdo que gostaríamos de ver ou scrape é realmente renderizado pelo código JavaScript e não é acessível a partir da resposta HTML bruta que o servidor entrega.
Como mencionamos acima, os navegadores sabem como processar o JavaScript e renderizar belas páginas da web. Agora, e se pudéssemos aproveitar essa funcionalidade para nossas necessidades de raspagem e tivéssemos uma maneira de controlar os navegadores programaticamente? É exatamente aí que entra a automação do navegador headless!
Sem cabeça? Com licença? Sim, isso significa apenas que não há interface gráfica do usuário (GUI). Em vez de interagir com elementos visuais da maneira que você faria normalmente - por exemplo, com um mouse ou dispositivo de toque - você automatiza os casos de uso com uma interface de linha de comando (CLI).
Chrome sem cabeça e marionetista
Existem muitas ferramentas de web scraping que podem ser usadas para navegação sem cabeça, como Zombie.js ou Firefox sem cabeça usando Selenium. Mas hoje vamos explorar o Chrome sem cabeça via Puppeteer, pois é um player relativamente mais novo, lançado no início de 2018. Nota do editor: Vale mencionar o Remote Browser da Intoli, outro player novo, mas que terá que ser assunto para outro artigo.
O que exatamente é Marionetista? É uma biblioteca Node.js que fornece uma API de alto nível para controlar o Chrome ou Chromium sem periféricos ou para interagir com o protocolo DevTools. É mantido pela equipe do Chrome DevTools e por uma incrível comunidade de código aberto.
Chega de conversa - vamos pular para o código e explorar o mundo de como automatizar a raspagem da web usando a navegação sem cabeça do Puppeteer!
Preparando o Ambiente
Em primeiro lugar, você precisará ter o Node.js 8+ instalado em sua máquina. Você pode instalá-lo aqui, ou se você é amante da CLI como eu e gosta de trabalhar no Ubuntu, siga estes comandos:
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs
Você também precisará de alguns pacotes que podem ou não estar disponíveis em seu sistema. Apenas por segurança, tente instalá-los:
sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
Configurar o Headless Chrome e o Puppeteer
Eu recomendaria instalar o Puppeteer com npm
, pois ele também incluirá a versão estável e atualizada do Chromium que é garantida para funcionar com a biblioteca.
Execute este comando no diretório raiz do seu projeto:
npm i puppeteer --save
Observação: isso pode demorar um pouco, pois o Puppeteer precisará baixar e instalar o Chromium em segundo plano.
Ok, agora que estamos prontos e configurados, vamos começar a diversão!
Usando a API Puppeteer para Web Scraping automatizado
Vamos começar nosso tutorial Puppeteer com um exemplo básico. Vamos escrever um script que fará com que nosso navegador headless faça uma captura de tela de um site de nossa escolha.
Crie um novo arquivo no diretório do projeto chamado screenshot.js
e abra-o em seu editor de código favorito.
Primeiro, vamos importar a biblioteca Puppeteer em seu script:
const puppeteer = require('puppeteer');
Em seguida, vamos pegar a URL dos argumentos da linha de comando:
const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }
Agora, precisamos ter em mente que o Puppeteer é uma biblioteca baseada em promessas: ela executa chamadas assíncronas para a instância do Chrome sem comando sob o capô. Vamos manter o código limpo usando async/await. Para isso, precisamos primeiro definir uma função async
e colocar todo o código do Puppeteer nela:
async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();
Ao todo, o código final fica assim:
const puppeteer = require('puppeteer'); const url = process.argv[2]; if (!url) { throw "Please provide URL as a first argument"; } async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();
Você pode executá-lo executando o seguinte comando no diretório raiz do seu projeto:
node screenshot.js https://github.com
Espere um segundo e bum! Nosso navegador headless acabou de criar um arquivo chamado screenshot.png
e você pode ver a página inicial do GitHub renderizada nele. Ótimo, temos um raspador da Web do Chrome em funcionamento!
Vamos parar por um minuto e explorar o que acontece em nossa função run()
acima.
Primeiro, iniciamos uma nova instância do navegador headless, depois abrimos uma nova página (guia) e navegamos até a URL fornecida no argumento da linha de comando. Por fim, usamos o método interno do Puppeteer para fazer uma captura de tela, e só precisamos fornecer o caminho onde ela deve ser salva. Também precisamos ter certeza de fechar o navegador headless depois que terminarmos nossa automação.
Agora que cobrimos o básico, vamos passar para algo um pouco mais complexo.
Um segundo exemplo de raspagem de marionetista
Para a próxima parte do nosso tutorial Puppeteer, digamos que queremos extrair os artigos mais recentes do Hacker News.
Crie um novo arquivo chamado ycombinator-scraper.js
e cole o seguinte snippet de código:
const puppeteer = require('puppeteer'); function run () { return new Promise(async (resolve, reject) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let urls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }) browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run().then(console.log).catch(console.error);
Ok, há um pouco mais acontecendo aqui em comparação com o exemplo anterior.
A primeira coisa que você pode notar é que a função run()
agora retorna uma promessa, então o prefixo async
foi movido para a definição da função de promessa.
Também envolvemos todo o nosso código em um bloco try-catch para que possamos lidar com quaisquer erros que façam com que nossa promessa seja rejeitada.
E, finalmente, estamos usando o método interno do Puppeteer chamadovalu evaluate()
. Esse método nos permite executar código JavaScript personalizado como se o estivéssemos executando no console do DevTools. Qualquer coisa retornada dessa função é resolvida pela promessa. Este método é muito útil quando se trata de extrair informações ou realizar ações personalizadas.
O código passado para o método evaluate()
é JavaScript bastante básico que cria uma matriz de objetos, cada um com campos de url
e text
que representam os URLs da história que vemos em https://news.ycombinator.com/.
A saída do script se parece com isso (mas com 30 entradas, originalmente):
[ { url: 'https://www.nature.com/articles/d41586-018-05469-3', text: 'Bias detectives: the researchers striving to make algorithms fair' }, { url: 'https://mino-games.workable.com/jobs/415887', text: 'Mino Games Is Hiring Programmers in Montreal' }, { url: 'http://srobb.net/pf.html', text: 'A Beginner\'s Guide to Firewalling with pf' }, // ... { url: 'https://tools.ietf.org/html/rfc8439', text: 'ChaCha20 and Poly1305 for IETF Protocols' } ]
Muito legal, eu diria!

Ok, vamos seguir em frente. Tivemos apenas 30 itens devolvidos, enquanto há muitos mais disponíveis - eles estão apenas em outras páginas. Precisamos clicar no botão “Mais” para carregar a próxima página de resultados.
Vamos modificar um pouco nosso script para adicionar um suporte para paginação:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);
Vamos rever o que fizemos aqui:
- Adicionamos um único argumento chamado
pagesToScrape
à nossa funçãorun()
principal. Usaremos isso para limitar quantas páginas nosso script irá raspar. - Há mais uma nova variável chamada
currentPage
que representa o número da página de resultados que estamos vendo atualmente. É definido como1
inicialmente. Também envolvemos nossaevaluate()
em um loopwhile
, para que ela continue em execução enquantocurrentPage
for menor ou igual apagesToScrape
. - Adicionamos o bloco para mover para uma nova página e aguardar o carregamento da página antes de reiniciar o loop
while
.
Você notará que usamos o método page.click()
para que o navegador headless clique no botão “Mais”. Também usamos o método waitForSelector()
para garantir que nossa lógica seja pausada até que o conteúdo da página seja carregado.
Ambos são métodos de API Puppeteer de alto nível prontos para uso imediato.
Um dos problemas que você provavelmente encontrará durante a raspagem com o Puppeteer é aguardar o carregamento de uma página. O Hacker News tem uma estrutura relativamente simples e foi bastante fácil esperar pela conclusão do carregamento da página. Para casos de uso mais complexos, o Puppeteer oferece uma ampla variedade de funcionalidades integradas, que você pode explorar na documentação da API no GitHub.
Isso tudo é muito legal, mas nosso tutorial Puppeteer ainda não cobriu a otimização. Vamos ver como podemos fazer o Puppeteer correr mais rápido.
Otimizando nosso script de marionetista
A ideia geral é não deixar o navegador headless fazer nenhum trabalho extra. Isso pode incluir carregar imagens, aplicar regras CSS, disparar solicitações XHR, etc.
Assim como com outras ferramentas, a otimização do Puppeteer depende do caso de uso exato, portanto, lembre-se de que algumas dessas ideias podem não ser adequadas para o seu projeto. Por exemplo, se tivéssemos evitado carregar imagens em nosso primeiro exemplo, nossa captura de tela poderia não ter a aparência que queríamos.
De qualquer forma, essas otimizações podem ser realizadas armazenando em cache os ativos na primeira solicitação ou cancelando as solicitações HTTP imediatamente à medida que são iniciadas pelo site.
Vamos ver como o cache funciona primeiro.
Você deve estar ciente de que, ao iniciar uma nova instância de navegador sem periféricos, o Puppeteer cria um diretório temporário para seu perfil. Ele é removido quando o navegador é fechado e não está disponível para uso quando você inicia uma nova instância - portanto, todas as imagens, CSS, cookies e outros objetos armazenados não estarão mais acessíveis.
Podemos forçar o Puppeteer a usar um caminho personalizado para armazenar dados como cookies e cache, que serão reutilizados toda vez que o executarmos novamente - até que expirem ou sejam excluídos manualmente.
const browser = await puppeteer.launch({ userDataDir: './data', });
Isso deve nos dar um bom aumento no desempenho, pois muitos CSS e imagens serão armazenados em cache no diretório de dados na primeira solicitação, e o Chrome não precisará baixá-los repetidamente.
No entanto, esses ativos ainda serão usados ao renderizar a página. Em nossas necessidades de raspagem de artigos de notícias do Y Combinator, não precisamos nos preocupar com nenhum visual, incluindo as imagens. Nós nos preocupamos apenas com a saída HTML simples, então vamos tentar bloquear todas as solicitações.
Felizmente, o Puppeteer é muito legal de se trabalhar, neste caso, porque vem com suporte para ganchos personalizados. Podemos fornecer um interceptor em cada solicitação e cancelar as que realmente não precisamos.
O interceptor pode ser definido da seguinte maneira:
await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });
Como você pode ver, temos controle total sobre as solicitações iniciadas. Podemos escrever uma lógica personalizada para permitir ou abortar solicitações específicas com base em seu resourceType
. Também temos acesso a muitos outros dados, como request.url
, para que possamos bloquear apenas URLs específicos, se quisermos.
No exemplo acima, apenas permitimos que solicitações com o tipo de recurso "document"
passem pelo nosso filtro, o que significa que bloquearemos todas as imagens, CSS e tudo mais além da resposta HTML original.
Segue nosso código final:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } }); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { await page.waitForSelector('a.storylink'); let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.waitForSelector('a.morelink'), await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);
Fique Seguro com Limites de Taxa
Os navegadores headless são ferramentas muito poderosas. Eles são capazes de realizar quase qualquer tipo de tarefa de automação na web, e o Puppeteer torna isso ainda mais fácil. Apesar de todas as possibilidades, devemos cumprir os termos de serviço de um site para garantir que não abusemos do sistema.
Como esse aspecto é mais relacionado à arquitetura, não abordarei isso em profundidade neste tutorial do Puppeteer. Dito isso, a maneira mais básica de desacelerar um script Puppeteer é adicionar um comando sleep a ele:
js await page.waitFor(5000);
Essa instrução forçará seu script a dormir por cinco segundos (5000 ms). Você pode colocar isso em qualquer lugar antes browser.close()
.
Assim como limitar o uso de serviços de terceiros, existem muitas outras maneiras mais robustas de controlar o uso do Puppeteer. Um exemplo seria construir um sistema de filas com um número limitado de trabalhadores. Toda vez que você quiser usar o Puppeteer, você colocaria uma nova tarefa na fila, mas haveria apenas um número limitado de trabalhadores capazes de trabalhar nas tarefas nela. Essa é uma prática bastante comum ao lidar com limites de taxa de API de terceiros e também pode ser aplicada à extração de dados da Web do Puppeteer.
O lugar do marionetista na Web em movimento rápido
Neste tutorial do Puppeteer, demonstrei sua funcionalidade básica como uma ferramenta de web-scraping. No entanto, tem casos de uso muito mais amplos, incluindo teste de navegador sem cabeça, geração de PDF e monitoramento de desempenho, entre muitos outros.
As tecnologias da Web estão avançando rapidamente. Alguns sites são tão dependentes da renderização de JavaScript que se tornou quase impossível executar solicitações HTTP simples para raspá-los ou realizar algum tipo de automação. Felizmente, os navegadores headless estão se tornando cada vez mais acessíveis para lidar com todas as nossas necessidades de automação, graças a projetos como o Puppeteer e as equipes incríveis por trás deles!