Web Scraping eficiente: Do Headless Browser às APIs ocultas
Quando você decide criar um agregador de dados em tempo real, a primeira ideia que vem à cabeça para coletar informações de grandes plataformas é: "Vou subir um Puppeteer, simular cliques humanos e pronto".
Foi exatamente isso que fiz quando comecei a construir a base de dados do NaveIdeal, uma plataforma criada para minerar, higienizar e auditar ofertas de veículos de múltiplos portais (como OLX, WebMotors e iCarros) em tempo real, calculando a defasagem contra a Tabela FIPE para ranquear as melhores oportunidades do mercado.
No papel, parecia perfeito. Na prática, me fez desistir do projeto no primeiro mês.
Neste post, quero compartilhar as 3 fases de maturidade de Web Scraping pelas quais passei, os erros de consumo de memória que quase derrubaram a infraestrutura e como saí de instâncias pesadas de Chromium para requisições de alta performance consumindo frações míseras de CPU e RAM.
1. O Ponto de Partida e o Pesadelo da RAM (Puppeteer)
No início, para garantir que as páginas carregassem scripts dinâmicos e contornassem desafios visuais, configurei uma fila usando puppeteer-cluster e puppeteer-extra-plugin-stealth.
A lógica de extração era parecida com isto:
async function scrapeComBrowser(page, url) {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60000 });
await page.waitForSelector("#price-box-container", { timeout: 10000 });
const dados = await page.evaluate(() => {
const titulo = document.querySelector("h1")?.innerText || "";
const preco = document.querySelector("#price-box-container")?.innerText || "";
return { titulo, preco };
});
return dados;
}
Onde a conta não fechava:
Consumo de Memória: Cada aba ou instância do Chromium consome facilmente entre 150 MB e 350 MB de RAM. Multiplique isso por uma concorrência mínima de 4 workers em um servidor modesto e você terá a máquina travando ou muito lenta.
Overhead de CPU: O Chromium renderiza CSS (mas temos como configurar para o puppeteer não renderizar isso), calcula layout e processa scripts que você não precisa para nada se o seu objetivo for apenas extrair texto e JSON.
Fragilidade e Timeouts: Quedas de conexão no protocolo do DevTools (protocolTimeout) eram comuns durante varreduras contínuas.
2. A Virada de Chave: HTTP Puro + Cheerio
Percebi que em 80% das páginas que eu precisava varrer, o HTML inicial já vinha com o conteúdo renderizado no servidor (SSR) ou embutido em tags iniciais. Eu não precisava executar JavaScript no cliente, precisava apenas fazer o download do HTML cru e fazer o parse da árvore estática.
Trocamos o Puppeteer por um cliente HTTP otimizado (com suporte a proxies residenciais e headers humanizados) aliado ao Cheerio (uma implementação rápida e leve do núcleo do jQuery voltada para o servidor).
const cheerio = require("cheerio");
const proxyManager = require("./services/proxyManager");//aqui esta configurado os proxys residencias
async function scrapeComCheerio(url) {
const response = await proxyManager.fetch({ url });
const $ = cheerio.load(response.body);
const title = $("h1").first().text().trim();
const precoTxt = $("#price-box-container").text().trim();
const preco = Number(precoTxt.replace(/\D/g, ""));
return { title, preco };
}
O impacto imediato:
Memória RAM: Caiu de centenas de megabytes por worker para menos de 30 MB no processo total.
Velocidade: Uma requisição que as vezes levava até 10 ou 15 segundos no Chromium passou a ser resolvida em 500ms a 1.2s.
3. O Santo Graal: Mineração Direta de APIs e Payloads Ocultos
Se usar Cheerio já reduziu o consumo em 80%, a descoberta seguinte mudou completamente o jogo: muitos portais modernos trafegam dados estruturados por trás das cortinas.
Em vez de fazer o scraping visual de dezenas de elementos (.div > span > a), começamos a inspecionar a aba Network das plataformas. Descobrimos que portais robustos como a WebMotors alimentam suas listagens e telas através de APIs REST internas ou dados serializados.
No extrator do NaveIdeal, em vez de baixar todo o HTML de uma listagem para caçar tags CSS, consumimos diretamente os endpoints de busca com filtros ordenados:
async function extrairViaApiInterna(estado, tipo, pagina = 1) {
const apiUrl = `https://www.site.com.br/api/search/${tipo}?state=${estado}&actualPage=${pagina}&displayPerPage=24`;
const response = await proxyManager.fetch({
url: apiUrl,
headers: {
"Accept": "application/json",
"Origin": "https://www.site.com.br"
}
});
const json = JSON.parse(response.body);
const anuncios = json.SearchResults || [];
return anuncios.map(item => ({
id: item.UniqueId,
marca: item.Specification?.Make?.Value,
modelo: item.Specification?.Model?.Value,
versao: item.Specification?.Version?.Value,
ano: item.Specification?.YearModel,
preco: item.Prices?.Price,
fotos: item.Media?.Photos?.map(p => p.PhotoPath)
}));
}
Por que isso é superior?
Zero quebra de layout: Se a plataforma mudar a cor do botão ou a classe CSS do preço de .ad-price para .sc-price-new, o seu scraper tradicional quebra. A API estruturada tende a ser muito mais estável.
Custo Computacional Quase Zero: Fazer o parse de uma string JSON nativa com JSON.parse() é ordens de grandeza mais rápido do que construir e percorrer nós de DOM.
Onde tudo isso se conecta?
Todo esse processo de extração leve alimenta o pipeline do NaveIdeal.
Depois que esses dados brutos chegam sem peso na infraestrutura, eles entram em uma camada de normalização semântica, cruzam com a base histórica da Tabela FIPE e geram um Score de Oportunidade para filtrar veículos com real margem de desconto versus distorções de mercado.