Voltar as noticias
Construindo um Scraper de Vagas Eficiente e Barato
Casos de UsoMediaEN

Construindo um Scraper de Vagas Eficiente e Barato

Dev.to - MCP·14 de abril de 2026

Eu precisava de milhares de anúncios de emprego no esquema OJP v0.2. Não apenas alguns para uma demonstração — um volume suficiente para que o custo por anúncio tivesse que desaparecer como um item de linha.

As opções existentes não funcionaram para mim. Scrapers comerciais cobram por anúncio em números que assumem que você é um fornecedor de ATS repassando o custo para os clientes. Os de código aberto querem que você escreva um adaptador personalizado para cada página de carreira, o que é seu próprio modo de falha lento. Nenhum se encaixa no que uma camada de protocolo realmente precisa: barato, fresco e estruturado da mesma forma em todos os quadros.

Então eu construí o meu próprio em uma única sessão. Uma execução de produção: 887 anúncios em 4 quadros.

Métrica Valor
Custo $0.39 / 1.000 anúncios
Taxa de processamento 3.9s / emprego
Taxa de sucesso 77% bruto, 95%+ após nova tentativa

A maioria das partes "difíceis" não eram o LLM. A chamada do LLM é a parte barata. Tudo ao redor é onde o custo e a taxa de sucesso realmente vivem.

A arquitetura

scrape-jobs.json (fila + status)
        │
        ▼
┌─────────────────────────┐
│ Navegador Playwright     │  ← contexto stealth, um por trabalhador
└─────────┬───────────────┘
          │
   buscar + remover HTML → ~6K tokens de texto limpo
          │
          ▼
┌─────────────────────────┐
│ Gemini Flash-Lite       │  ← ~$0.0004/chamada
│ (extração OJP v0.2)    │
└─────────┬───────────────┘
          │
   sanitizar + validar (JSON Schema)
          │
          ▼
     results.json

Fila BFS. Páginas de listagem descobrem URLs de anúncios individuais, adicionam-nas como pendentes, e o loop roda até ficar vazio. O status vive no próprio arquivo de entrada, então as execuções retomam limpas após uma falha.

Cinco camadas de fallback, cada uma entra em ação quando a anterior falha: raspagem de link DOM → detecção heurística de contêiner de emprego → navegador visual (captura de tela + modelo de visão escolhe seletores clicáveis) → sanitizador de esquema → nova tentativa completa de visão em falhas de extração.

As cinco coisas que mudaram os números

1. Hashing de conteúdo para que novas tentativas sejam gratuitas

Cada página buscada recebe um SHA-256 de seu texto removido. Se o hash corresponder à última raspagem, pule a chamada do LLM completamente — sem tokens, sem custo.

Em uma nova raspagem semanal, 95% dos URLs pulam o hash. Apenas edições reais de emprego são re-extraídas. Isso é o que torna tudo viável como um pipeline recorrente em vez de uma única tentativa.

2. Playwright stealth para vencer a detecção de bots

User-agent + viewport + fuso horário realistas, --disable-blink-features=AutomationControlled, e um script de inicialização que oculta a flag navigator.webdriver que a maioria dos bots esquece. Passa pelas camadas comuns de detecção de bots em 4/5 quadros.

Um ATS no meu conjunto de testes ainda bloqueia com um desafio completo de CAPTCHA. Esse está na lista.

3. Paralelismo por trabalhador particionado por domínio

Particione URLs pendentes por domínio de quadro para que os trabalhadores não se sobreponham. Se um domínio dominar a fila (digamos, 400 URLs em um quadro contra 20 em outro), divida o grande em fragmentos e intercale os URLs para que os primeiros itens se espalhem entre todos os trabalhadores. Uma instância do Chromium por thread, sem estado compartilhado para depurar às 2 da manhã.

Isso importa mais do que a contagem bruta de trabalhadores. O paralelismo ingênuo em round-robin em um fluxo misto de fila luta contra si mesmo — você acaba com cada trabalhador segurando uma conexão ao mesmo quadro.

4. Um sanitizador que absorve a deriva do esquema do LLM

Gemini Flash-Lite é barato, mas felizmente retornará "manager" para um enum de senioridade que só aceita "lead", ou "other" para um código de idioma que deve ser ISO 639-1. A reestruturação: pare de tentar engendrar o modelo para uma conformidade perfeita com o esquema. Assuma que ele irá derivar. Capture a deriva de forma determinística antes da validação.

O que o sanitizador realmente faz:

  • Mapeia sinônimos de enum para valores canônicos (manager → lead, intermediate → mid, graduate → junior, chief → c_level)
  • Normaliza nomes de idiomas para ISO 639-1 (english → en, deutsch → de)
  • Muda campos deslocados para a aninhagem correta (LLMs adoram colocar skills na raiz em vez de sob must_have)
  • Remove nulos porque o esquema tem additionalProperties: false

Levou o sucesso de 77% → 90%+ com a mesma entrada sem mudar o prompt ou o modelo.

5. Nova tentativa de visão para os últimos 10%

Quando a extração de texto falha — uma SPA renderizada não retorna nada parseável, ou Gemini Lite retornou JSON inválido mesmo após a sanitização — re-execute com uma captura de tela de página inteira através da visão Gemini Flash.

Recuperou 4/20 novas tentativas a $0.0037 por anúncio recuperado. Quadros onde a remoção de texto retornou 0 caracteres se tornam viáveis porque o Playwright ainda renderiza a página. A visão vê o que o usuário vê.

Uma nuance que vale a pena notar: Flash-Lite tem um ancoramento de visão mais fraco, então o caminho de nova tentativa usa especificamente gemini-flash-latest mesmo que a extração primária use Lite.

O modelo de custo validado

Camada Por chamada Por 1K anúncios
Busca (Playwright) grátis ~3s de computação
Remoção de HTML grátis local
Extração (texto Flash-Lite) $0.0004 $0.39
Descoberta de navegação visual $0.0006 $0.003 por quadro difícil
Nova tentativa de visão $0.0007 $0.015 por 20 novas tentativas

De ponta a ponta em escala, incluindo novas tentativas: ~$0.42 / 1.000. Isso é ~$4 por 10K, ~$40 por 100K.

Cada execução escreve um stats-{timestamp}.json congelado com custo de extração e visão rastreados separadamente, para que eu possa diferenciar regressões entre execuções. Um stats.json cumulativo é mesclado no final de cada execução — escritor único, sem condições de corrida entre trabalhadores paralelos.

Armadilhas que paguei

  • A versão da imagem base do Playwright deve corresponder exatamente à versão do pip playwright, ou o Chromium headless falha com Executable doesn't exist
  • Leitura/modificação/gravação de estatísticas compartilhadas de trabalhadores paralelos cria condições de corrida — use arquivos por execução e mescle uma vez no final
  • Quadros com prefixos de localidade (/en_US/jobs/..., /de_DE/jobs/...) criam URLs duplicados que inflacionam extrações em 10× a menos que você normalize durante a descoberta de links
  • A ancoragem de visão do Gemini Flash Lite não é boa o suficiente para nova tentativa — codifique o modelo maior para capturas de tela
  • A visão baseada em captura de tela em quadros pesados de SPA funciona mesmo quando a remoção de texto retorna 0 caracteres, porque o Playwright ainda renderiza a página

Sobre o que isso realmente se trata

O alvo de custo não era o ponto para mim. Um centavo por anúncio é uma manchete útil, mas é um efeito colateral.

O ponto é que uma vez que a extração se torna tão barata e tão estruturada, o passo de raspagem deixa de ser um obstáculo. Qualquer um que queira dados de emprego em escala pode obtê-los. O que permanece valioso é o esquema em que você extrai — OJP no meu caso — e a camada de protocolo que torna essas extrações interoperáveis entre cada agente e cada quadro.

Estou construindo o ADNX porque a próxima geração de contratação não opera em ferramentas de recrutamento. Ela opera em transações de agente para agente contra protocolos de domínio. Ingestão de empregos a $0.

Contexto Triplo Up

Empresas brasileiras podem se beneficiar de scrapers de dados eficientes para otimizar processos de recrutamento. A implementação de soluções de scraping estruturadas pode reduzir custos e melhorar a qualidade das informações coletadas. Isso é especialmente relevante em um mercado competitivo onde a agilidade na obtenção de dados é crucial.

Noticias relacionadas

Gostou do conteudo?

Receba toda semana as principais novidades sobre WebMCP.