O futuro do webscraping? Como fazer scrapers que não quebram quando a página muda utilizando IA e agentes (1/3)
Por muito e muito tempo, o webscraping (e automações baseadas em browser ou DOM extraction no geral) era inerentemente fadado a nunca ser perfeito. Você poderia ter o fingerprint perfeito, o browser perfeito, o workflow perfeito, lifecycle perfeito, proxy, absolutamente tudo, e mesmo assim, mais hora ou menos hora, você sofreria com uma dor irreparável: uma atualização de página.
Um pequeno seletor que mudou, uma árvore DOM com algumas alterações, uma mudança de texto num botão ou mesmo uma repaginada completa da página que você estava mirando. Qualquer uma dessas coisas poderia, e eventualmente iria, quebrar seu scraper. Era basicamente um trabalho infinito, imprevisível e ingrato manter uma extração ou automação recorrente sobre uma página que você não controlava. Até agora.
A IA foi um game changer em muitas áreas, e embora eu veja muita gente ensinando jeitos de ligar o Claude ou o Cursor a navegadores, posso contar nos dedos as vezes que vi alguém falando de integrar IA ao fluxo de extração de dados, webscraping e fluxos de automações em browser específicas, e a maioria das vezes que ouvi isso foi dentro da minha empresa atual, em conversas com meus liderados. Foi pensando nisso que resolvi começar a escrever uma série de artigos sobre o que considero ser o futuro do webscraping e das automações baseadas em navegador, que fico muito feliz de compartilhar com vocês a seguir.
Alguns disclaimers...
Antes de iniciar, gostaria de esclarecer algumas coisas que julgo necessárias para a boa interpretação desses artigos:
- Durante a escrita desse artigo, utilizarei como motor de automação o pain.headless, meu SaaS de browsers in cloud que atualmente se encontra em versão beta. Não estou utilizando ele por jabá ou por autopromoção, e sim porque quero testar os limites de meus browsers e estou extremamente familiarizado e encantado pela simplicidade do SDK. Isso não torna os exemplos/códigos aqui irreproduzíveis in natura, já que estou oferecendo 5 mil créditos de teste para qualquer um que queira fazer parte do BETA testing, sem compromisso algum. Além disso (embora eu goste de pensar que todo dev saiba disso), qualquer um dos exemplos que você verá aqui são perfeitamente adaptáveis para outras tecnologias/linguagens, como Selenium, Puppeteer ou Playwright, bastando apenas a força de vontade e interesse em fazê-lo.
- NÃO SOU UM ESPECIALISTA OU GURU DE IA. Embora goste de pensar que sei usar as ferramentas que nos proporciona, saiba que não sou um especialista em IA, modelos ou orquestração, sei apenas um pouco mais que o básico para um profissional de TI, e tenho muita força de vontade para fazer as coisas funcionarem. Caso você identifique coisas que eu poderia ter feito diferente, otimizações ou recomendações que tenha a fazer, sinta-se completamente livre para dizer e garanto que verei e testarei todas de mente aberta, como alguém que realmente quer aprender.
- Não passarei pelo código completo para não estender (mais) o tamanho desse texto. Aqui mostrarei apenas os trechos/arquivos principais, com o código-fonte completo disponível aqui. Como são 3 artigos diferentes, cada um estará em uma branch diferente para que, conforme as publicações e leituras de vocês, seja possível aos interessados dissecar exatamente o que foi feito.
Com isso esclarecido, abre esse claude vadia, e bora trabalhar.
Nosso alvo
Você pode usar o que faremos aqui para basicamente qualquer coisa (com as suas devidas adaptações, claro). Como o intuito aqui é mostrar como fazer a IA nos auxiliar na extração de dados e não focar em anti-bot ou alvos complexos, iremos ser felizes no simples. Nosso scraper irá entrar em três e-commerces de hardware brasileiros: KaBuM!, Pichau e TerabyteShop. Lá dentro, ele irá pesquisar por uma lista de termos informados, entrar nos três primeiros resultados e coletar alguns dados básicos do produto; mas, lembrando novamente, após entender o conceito base aqui, o céu é literalmente o limite.
Fazendo nosso setup
Bom, vamos começar de forma simples, criando nosso diretório base e adicionando as dependências necessárias para trabalharmos:
# Main deps
yarn add cheerio dotenv openai pain-headless
# & dev dependencies
yarn add -D @types/node typescript tsx
Após isso, será criado um arquivo package.json na nossa pasta, onde adicionaremos o seguinte script para rodar nosso projeto:
{
"scripts": {
"dev": "tsx src/index.ts"
}
// resto do arquivo...
}
Agora que nossas dependências estão prontas, precisamos também de um lugar para manter nossas configurações e chaves, então criaremos um .env.example com o seguinte conteúdo:
PHLESS_API_KEY=your_pain_headless_api_key
KEEP_FOOTER=0
KEEP_NAVBAR=1
KEEP_IMAGE_SOURCES=0
KEEP_SVG=0
NUM_RESULTS_PER_PROVIDER=3
OPENROUTER_API_KEY=your_openrouter_api_key
Após isso, copie esse arquivo como um .env e preencha com suas chaves (mais pra frente veremos melhor o que essas variáveis KEEP_ fazem, não se preocupe) e lembre-se de nunca commitar, revelar ou deixar publicamente acessível o seu arquivo .env com as suas chaves. Durante esse artigo, eu utilizarei o OpenRouter como provedor de IA, pela sua versatilidade e compatibilidade, além da possibilidade de poder testar diversos modelos sem alterar código.
Com o terreno bem adubado, deveremos ter a seguinte árvore de arquivos no nosso projeto:
|- node_modules/
|- src/
|---- helpers/
|------ agentic.ts
|------ code-parser.ts
|------ files.ts
|------ scraper.ts
|---- services/
|------ open-router.ts
|------ pain-headless.ts
|---- index.ts
|---- types.ts
|- .gitignore
|- .env
|- .env.example
|- package.json
|- yarn.lock
Agora, com tudo pronto, podemos partir para os primeiros blocos de código.
Facilitando um pouco a vida
Antes de abordar como ficará nosso core, vamos criar os tipos que usaremos (e se você não está tipando seus códigos hoje em dia, bom, recomendo que volte duas casas e repense) e alguns helpers que facilitarão a vida e melhorarão a leitura do nosso código.
// src/helpers/files.ts
import { writeFile, mkdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
// Usaremos essa function para escrever os dados extraidos pelo nosso scraper
export const outputJson = async (filename: string, data: unknown): Promise<void> => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const path = join(__dirname, '..', '..', 'output');
await mkdir(path, { recursive: true });
await writeFile(`${path}/${filename.replace('.json', '')}.json`, JSON.stringify(data, null, 4), 'utf8');
};
// src/types.ts
export type Provider = 'kabum' | 'pichau' | 'terabyte';
export interface ProductData {
title: string;
thumbnail: string;
description: string;
price: number | string;
link: string;
rate: string | number;
provider: Provider;
}
Após criados os helpers e os tipos que precisamos, podemos criar nossos services para consumir serviços externos:
// src/services/pain-headless.ts
import { StealthBrowser } from "pain-headless"
// Apenas um shortcut para evitar repetir codigo, como instanciaremos muitos browsers
export const getBrowser = (): StealthBrowser => new StealthBrowser({
apiKey: process.env?.PHLESS_API_KEY || '',
proxy: {
type: 'premium',
country: 'br',
strategy: 'sticky',
},
waitElements: {
until: 'visible',
timeout: 15000,
}
});
Apenas para caráter de explicação, utilizaremos proxy para evitar blocks por IPs (pensando que em cenário real provavelmente teríamos algumas dezenas de produtos para olhar, um IP de servidor pode não ser muito bem visto, mesmo em sites com anti-bot fraco). A estratégia sticky do pain.headless mantém o mesmo IP entre os goTo que rodaremos, o que simularia um tráfego humano de alguém fazendo algumas pesquisas. Caso fôssemos escalar para centenas de produtos, o recomendado seria fazer a rotação do browser após algumas dezenas de produtos.
E, por último, configuraremos um simples helper para poder chamar o OpenRouter e rodar nossos prompts no Gemma 4 26B (em meus poucos testes, ganhou do QWEN Flash, tanto em custo quanto em eficiência, produzindo seletores mais corretos e menos alucinações).
// src/services/open-router.ts
import OpenAI from 'openai';
export const runPrompt = async (prompt: string, model: string = 'google/gemma-4-26b-a4b-it'): Promise<string> => {
const logs: string[] = []; // Como trabalharemos com mais de um browser rodando ao mesmo tempo, fica mais facil acumular os logs e exibi-los juntos
const ai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', // Necessario para o SDK consumir o OpenRouter ao inves da API oficial da OpenAI
apiKey: process.env.OPENROUTER_API_KEY,
});
logs.push(`[${model}]: sending prompt...`);
const response = await ai.chat.completions.create({
model,
messages: [{ role: 'user', content: prompt }],
});
if (response.usage) {
const inputTokens = response.usage.prompt_tokens ?? 0;
const outputTokens = response.usage.completion_tokens ?? 0;
const totalTokens = response.usage.total_tokens ?? (inputTokens + outputTokens);
logs.push(`[${model}]: used ${totalTokens} tokens.`);
}
logs.push(`[${model}]: response: ${response.choices[0]?.message?.content}`);
console.log(logs.join('\n'));
return response.choices[0]?.message?.content || '';
}
OBS: Devido ao limite de chars aqui do TabNews, vou criar uma thread nesse post para disponibilizar o artigo completo.