🎯 Um navegador em nuvem personalizável e anti-detecção alimentado por Chromium desenvolvido internamente, projetado para rastreadores web e agentes de IA. 👉Experimente agora
De volta ao blog

Construir um Raspador Web Distribuído em Node.js: Filas e Deduplicação

Alex Johnson
Alex Johnson

Senior Web Scraping Engineer

29-Jul-2026

TL;DR:

  • Um rastreador web distribuído precisa de uma fronteira de URL durável, trabalhadores sem estado, chaves de URL canônicas, orçamentos de requisições por host e um caminho de quarentena terminal explícito.
  • Mantenha o Node.js responsável pela descoberta, agendamento, estado e armazenamento. Delegue a renderização em JavaScript e a aquisição de páginas a uma camada de execução gerenciada quando a fonte exigir.
  • Use o hash da URL canônica como ID de trabalho da fila e como chave de idempotência de armazenamento. Isso bloqueia trabalhos duplicados antes de chegarem a um trabalhador.
  • A concorrência global dos trabalhadores e o ritmo por host resolvem problemas diferentes. Escale o primeiro com capacidade; defina o segundo a partir da permissão da fonte e do comportamento observado do servidor.
  • Comece com um pequeno conjunto de fontes autorizadas, meça páginas aceitas em vez de páginas tentadas e escale apenas depois que a profundidade da fila, a demora na frescura e a rejeição de esquema forem visíveis.

Um rastreador de processo único falha de maneiras previsíveis: sua fila de memória desaparece na reinicialização, links duplicados se multiplicam, um host lento ocupa o loop de eventos, e a execução do navegador consome a mesma máquina que deveria estar agendando o trabalho.

Um rastreador web distribuído separa essas responsabilidades. O Node.js controla o plano de controle— a fronteira de URL, estado do trabalho, desduplicação e decisões de armazenamento. Trabalhadores independentes são responsáveis pelo caminho dos dados. Para páginas públicas renderizadas em JavaScript, um serviço gerenciado pode executar a página e retornar Markdown ou HTML sem colocar processos de navegador dentro de cada contêiner de trabalhador.

Defina os requisitos e modos de falha

Antes de escolher uma fila, escreva o contrato de rastreamento:

  • Quais domínios e caminhos são autorizados?
  • Quantas páginas e níveis cada rastreamento pode descobrir?
  • Qual janela de frescura cada fonte precisa?
  • Quais formatos de resposta e campos exigidos definem uma página aceita?
  • Que ritmo de requisições é permitido por host?
  • Para onde vão resultados inválidos, vazios ou inesperados?
  • Por quanto tempo as versões de trabalho e de página devem ser retidas?

A primeira versão também deve ter condições de parada: profundidade máxima, número máximo de páginas aceitas, número máximo de URLs descobertas e um prazo. Esses limites evitam que um arquivo de calendário, navegação facetada ou parâmetro de rastreamento transforme um pequeno rastreamento em uma caminhada em gráfico sem fim.

Modos de falha comuns são arquitetônicos:

Falha Causa raiz Controle
Fronteira perdida Fila na memória Trabalhos duráveis suportados por Redis
Páginas duplicadas URLs brutas usadas como chaves Hash de URL canônica
Um host sobrecarregado Somente concorrência global definida Fila e ritmo por host
Conteúdo vazio armazenado Sucesso no transporte tratado como sucesso de dados Contrato de aceitação de conteúdo
Trabalhadores não conseguem escalar Estado local do navegador Trabalhadores sem estado e execução gerenciada
Trabalhos de veneno ciclo indefinidamente Sem estado terminal Fila de quarentena com liberação manual
Rastreio parece saudável, mas está desatualizado Apenas throughput medido Atraso de frescura e métricas de páginas aceitas

Separe o plano de controle da execução de páginas

O rastreador pode ser desenhado como dois planos:

Plano de controle: sementes → normalização de URL → desduplicação → filas Redis → estado do trabalho → metadados de armazenamento

Plano de execução: trabalhador → aquisição de página → validação de conteúdo → extração de links → registro aceito ou quarentena

Essa fronteira é importante porque o agendamento e a execução do navegador escalam de maneira diferente. As operações da fila são pequenas e com estado. A execução da página é pesada em rede e pode exigir JavaScript, roteamento regional ou uma sessão de navegador isolada.

Scrapeless Crawl suporta coleta de página única, em lote e de site vinculado com formatos que incluem Markdown, HTML, links, metadados e capturas de tela. Quando o caminho de execução precisa de um navegador interativo, o Scrapeless Scraping Browser mantém essa execução fora do contêiner do trabalhador. O aplicativo Node pode permanecer como o sistema de registro para escopo e estado enquanto a Scrapeless cuida da aquisição.

Para uma introdução conceitual à descoberta e extração, leia o que é um rastreador web. O design abaixo começa onde um rastreador de máquina única para: filas compartilhadas, enfileiramento idempotente e múltiplos trabalhadores.

Construa a fronteira de URL respaldada por Redis

BullMQ implementa a execução de trabalhos distribuídos no Redis. Sua documentação oficial cobre filas, trabalhadores, eventos, trabalhos atrasados, limitação de taxa e estado do trabalho.

A fronteira deve armazenar um pequeno payload de trabalho:

Campo Propósito
url URL canônica a ser coletada
host Partição da fila e pesquisa de política
depth Limite de descoberta
crawlId Correlação e cancelamento
parentUrl Origem para descoberta
schemaVersion Contrato downstream

Não coloque HTML de página no Redis. Armazene resultados grandes em um armazenamento de objetos ou banco de dados e mantenha apenas identificadores, estados e metadados compactos na fila.

Use uma fila separada por host aprovado quando domínios diferentes exigirem orçamentos de solicitação diferentes. Trabalhadores atribuídos a docs-example-com podem então usar um limitador, enquanto outro host recebe seu próprio ritmo. Uma fila global é mais simples, mas seu limitador não pode expressar políticas independentes de host.

Torne a adição à fila idempotente

Duas páginas podem ser equivalentes enquanto suas URLs brutas diferem:

  • um fragmento aponta para uma localização dentro do mesmo documento;
  • parâmetros de rastreamento mudam sem alterar o conteúdo;
  • parâmetros de consulta aparecem em uma ordem diferente;
  • portas padrão ou barras finais variam;
  • links relativos resolvem para a mesma página absoluta.

Normalize antes da inserção na fila. O Padrão de URL WHATWG define o modelo de análise implementado pela classe URL do Node.js.

Uma política segura é específica da fonte. Remover cada parâmetro de consulta pode mesclar páginas genuinamente diferentes. Mantenha uma lista de permissão ou lista de bloqueio para parâmetros conhecidos e preserve qualquer parâmetro que altere o recurso.

Após a normalização, hash a URL com SHA-256. Use esse digest hexadecimal como:

  • o ID do trabalho BullMQ;
  • a chave de upsert do banco de dados;
  • o link de origem entre o trabalho e a página armazenada.

BullMQ ignora um novo trabalho quando outro trabalho com o mesmo ID já existe naquela fila. Os registros de trabalho removidos pela retenção não fornecem mais essa proteção, então o armazenamento durável deve manter a mesma chave de idempotência.

Projeto Node.js minimalista com versão fixa

Este projeto é intencionalmente compacto:

Copy
distributed-crawler/
├── package.json
└── src/
    └── crawler.mjs

As versões das dependências foram verificadas em relação aos seus registros de pacotes no momento da redação. O exemplo é um bloqueio de lacuna de pré-requisitos: requer Node.js, Redis, uma SCRAPELESS_API_KEY, e um hostname público autorizado definido em ALLOWED_HOST. É projetado para um host, então o limitador de fila é realmente específico do host.

json Copy
{
  "name": "distributed-crawler-example",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node src/crawler.mjs"
  },
  "dependencies": {
    "@scrapeless-ai/sdk": "1.3.1",
    "bullmq": "5.79.3"
  },
  "engines": {
    "node": ">=22"
  }
}

O trabalhador abaixo tem quatro desfechos finais: aceito, inalterado, rejeitado e quarentenado. Ele não cria um loop de falha automático. Um operador pode inspecionar o registro de quarentena, corrigir sua causa e re-encadear explicitamente a URL canônica.

javascript Copy
import { createHash } from "node:crypto";
import { Queue, Worker } from "bullmq";
import { ScrapingCrawl } from "@scrapeless-ai/sdk";

const redis = {
  host: process.env.REDIS_HOST ?? "127.0.0.1",
  port: Number(process.env.REDIS_PORT ?? 6379)
};
const allowedHost = process.env.ALLOWED_HOST ?? "example.com";
const queueName = `crawl-${hostKey(allowedHost)}`;
const frontier = new Queue(queueName, { connection: redis });
const quarantine = new Queue(`${queueName}-quarantine`, { connection: redis });
const crawl = new ScrapingCrawl({
  apiKey: process.env.SCRAPELESS_API_KEY
});

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

function hostKey(host) {
  return host.toLowerCase().replaceAll(".", "-");
}

function canonicalize(input) {
  const url = new URL(input);
  url.hash = "";
  url.hostname = url.hostname.toLowerCase();
  for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
    url.searchParams.delete(key);
  }
  url.searchParams.sort();
  return url.href;
}

async function enqueue(url, crawlId, depth = 0, parentUrl = null) {
  const canonicalUrl = canonicalize(url);
  const parsed = new URL(canonicalUrl);
  if (parsed.hostname !== allowedHost) {
    throw new Error(`Host outside crawl scope: ${parsed.hostname}`);
  }

  const key = sha256(canonicalUrl);
  await frontier.add(
    "collect-page",
    {
      url: canonicalUrl,
      host: parsed.hostname,
      depth,
      crawlId,
      parentUrl,
      schemaVersion: "crawl-page-v1"
    },
    {
      jobId: key,
      removeOnComplete: 1000,
      removeOnFail: false
    }
  );
  return key;
}

const worker = new Worker(
  queueName,
  async (job) => {
    try {
      const result = await crawl.scrapeUrl(job.data.url, {
        formats: ["markdown", "links"],
        onlyMainContent: true,
        timeout: 15000
      });
      const markdown = result.markdown ?? result.data?.markdown;
      if (typeof markdown !== "string" || markdown.length < 200) {
        return { state: "rejeitado", reason: "content-contract", url: job.data.url };
      }

      const record = {
        key: job.id,
        url: job.data.url,
        crawlId: job.data.crawlId,
        depth: job.data.depth,
```plaintext
contentHash: sha256(markdown),
        markdown
      };

      console.log(JSON.stringify({ state: "accepted", ...record }));
      return { state: "accepted", key: record.key, contentHash: record.contentHash };
    } catch (error) {
      await quarantine.add("inspect-page", {
        ...job.data,
        sourceJobId: job.id,
        reason: error instanceof Error ? error.message : "unknown"
      });
      return { state: "quarantined", key: job.id };
    }
  },
  {
    connection: redis,
    concurrency: 4,
    limiter: { max: 2, duration: 1000 }
  }
);

worker.on("completed", (job, result) => {
  console.log(JSON.stringify({ event: "completed", jobId: job.id, result }));
});

worker.on("failed", (job, error) => {
  console.error(JSON.stringify({
    event: "worker-failed",
    jobId: job?.id,
    message: error.message
  }));
});

await enqueue(`https://${allowedHost}/`, "demo-crawl");

O exemplo imprime os registros aceitos para tornar o contrato de dados visível. Substitua console.log por um upsert de banco de dados indexado por record.key. Compare contentHash com o valor armazenado antes de escrever uma nova versão da página ou reconstruir um índice.

Entenda a máquina de estados da tarefa

Um crawler precisa de um modelo de estado que os operadores possam explicar:

Estado Significado Próxima ação
queued URL canônica está aguardando O trabalhador o reivindica
active Um trabalhador possui o contrato Adquirir e validar
accepted Conteúdo passou no contrato Armazenar, indexar, descobrir links
unchanged Hash do conteúdo corresponde à versão armazenada Atualizar metadados de frescor
rejected Resposta concluída mas conteúdo é inválido Revisar validador ou fonte
quarantined Execução não pôde produzir uma decisão Inspecionar e liberar manualmente
cancelled Escopo ou prazo de rastreamento terminou Manter metadados de auditoria

Mantenha failed como um evento de infraestrutura reportado pela fila, não como o único estado de negócio. Um trabalho que retorna uma shell de aplicação vazia é tecnicamente concluído, mas deve ser rejected. Uma página fora do escopo deve ser cancelled antes da aquisição. Estas distinções tornam os painéis acionáveis.

Controle de concorrência por host

A concorrência do trabalhador responde: “Quantos trabalhos este processo pode lidar?” Um orçamento de solicitações de host responde: “Quanto tráfego pode esta origem receber?” Eles devem ser configurados de forma independente.

Para um domínio autorizado:

  1. Leia as regras de robôs e limites contratuais.
  2. Defina um ritmo conservador por host.
  3. Execute vários trabalhadores apenas quando a fila e o orçamento do host permitirem.
  4. Acompanhe o status da resposta, aceitação de conteúdo e latência do servidor.
  5. Reduza o orçamento do host quando a fonte mostrar estresse ou o acordo mudar.

Os trabalhadores BullMQ podem compartilhar uma fila entre processos e máquinas. O limitador de fila coordena a fila selecionada, que é o motivo pelo qual o exemplo usa uma fila específica para o host. Para muitos domínios, gere filas a partir de um registro aprovado e limite o número de objetos de trabalhadores ativos.

O Protocolo de Exclusão de Robôs é padronizado pela RFC 9309. As regras de robôs não são uma concessão de permissão, e não substituem os termos do site, deveres de privacidade ou leis aplicáveis.

Delegar a aquisição de páginas sem perder o controle

O plano de controle Node.js deve decidir o que pode ser coletado. A camada de execução deve decidir como obter a representação da página permitida.

O Scrapeless Crawl quickstart documenta o status de rastreamento assíncrono e resultados em nível de página. Para páginas que exigem interação mais ampla ou execução de JavaScript, use as opções de navegador do Crawl enquanto a fila mantém o escopo, identidade do trabalho e decisões de armazenamento.

Valide o conteúdo retornado em vez de assumir que o executor tomou a decisão empresarial. Exija texto esperado, campos, idioma, URL e conteúdo mínimo. Armazene a rota de aquisição na proveniência para que uma auditoria posterior possa explicar como cada página entrou no conjunto de dados.

A descoberta de links pertence após a aceitação do conteúdo. Analise apenas páginas que atenderam ao contrato de conteúdo, então aplique esses filtros antes de enfileirar:

  • hostname com permissão;
  • prefixos de caminho permitidos;
  • esquemas HTTP suportados;
  • profundidade máxima e contagem de páginas;
  • canonicalização e consulta de ID do trabalho;
  • exclusões de tipo de arquivo;
  • política de parâmetro de consulta específica da fonte.

Não deixe que redirecionamentos expandam silenciosamente o conjunto de hosts permitidos. Registre a URL final, compare-a com o escopo e rejeite resultados entre domínios, a menos que o registro da fonte os autorize explicitamente.

Para sitemaps, trate cada URL como uma entrada descoberta em vez de uma saída confiável. Canonicalize, filtre e deduplica através do mesmo caminho de fronteira que um link HTML.

Armazene versões de página e proveniência de rastreamento

Um modelo de armazenamento útil possui três registros:

Copy
1. **Rastejar:** escopo, URLs semente, prazo, versão da política e estado geral.
2. **Página:** chave canônica, URL de origem, hash aceito mais recente, horário de coleta e versão do esquema.
3. **Versão da página:** hash de conteúdo, localização do payload, metadados e proveniência da aquisição.

A fila não é o banco de dados de longo prazo. As políticas de limpeza de jobs podem remover entradas concluídas, enquanto a tabela de páginas deve manter chaves idempotentes e histórico de versões de acordo com a política de retenção.

Se uma página não foi alterada, atualize o timestamp de frescor sem duplicar o payload. Se ela mudar, escreva uma nova versão imutável da página, aponte o registro da página para ela e notifique a indexação a jusante através de um evento separado.

## Observar o rastreamento

O comprimento da fila sozinho pode ser enganoso. Um rastreador pode esvaziar sua fronteira enquanto rejeita cada página. Acompanhe:

| Sinal | Pergunta respondida |
|---|---|
| Páginas aceitas por minuto | Dados úteis estão chegando? |
| Tempo de atraso entre descoberta e aceitação | Quão obsoleto está o pipeline? |
| Taxa de enfileiramento duplicado | A canonicidade é eficaz? |
| Taxa de rejeição por motivo | O markup de origem ou a validação mudaram? |
| Idade da quarentena | A dívida operacional está se acumulando? |
| Ritmo de solicitações do host | A política está sendo seguida? |
| Taxa de mudança de conteúdo | O cronograma de atualização é apropriado? |
| Percentil de idade da fila | A capacidade dos trabalhadores é suficiente? |

OpenTelemetry descreve rastros, métricas, logs e bagagens em seu <a href="https://opentelemetry.io/docs/concepts/signals/" rel="nofollow"><strong>guia de sinais de telemetria</strong></a>. Use um ID de rastreamento em todos os produtores, eventos da fila, chamadas de aquisição, gravações de armazenamento e eventos de índice a jusante.

Alerta sobre dados aceitos obsoletos e entradas de quarentena antigas, não apenas em caso de falhas de processo. Um processo pode estar saudável enquanto o conjunto de dados para silenciosamente de mudar.

## Lista de verificação de implantação

- Execute o Redis com persistência, autenticação, controles de rede e backups apropriados para a carga de trabalho.
- Mantenha os trabalhadores sem estado e implante a mesma imagem em todas as instâncias.
- Armazene chaves de API em um gerenciador de segredos ou injeção no ambiente, nunca em payloads de jobs.
- Defina a retenção da fila separadamente da retenção de páginas.
- Limite a profundidade da coleta, páginas, tempo e escopo do host.
- Use uma fonte de política de host compartilhada por produtores e trabalhadores.
- Drene os trabalhadores durante a implantação para que arrendamentos ativos não sejam abandonados.
- Teste cancelamento, liberação de quarentena, idempotência de armazenamento e mudanças de política de origem.
- Registre versões do Node.js, BullMQ, do SDK Scrapeless e do esquema da página.

Comece com um host aprovado e um limite de páginas pequeno. Adicione domínios somente após os painéis mostrarem que os dados aceitos, frescor e política de solicitações permanecem dentro de seus objetivos.

## Conclusão: escale a fronteira, não a incerteza

Um rastreador da web distribuído se torna confiável quando cada URL tem uma identidade canônica, cada host tem um orçamento explícito e cada página termina em um estado significativo. Node.js e BullMQ podem dominar a fronteira e a coordenação dos trabalhadores; Scrapeless pode gerenciar a execução da página onde a renderização gerenciada e o manuseio de rede são necessários.

Crie uma prova de conceito limitada com um domínio público, autorizado, verifique a [página de preços do Scrapeless](https://www.scrapeless.com/pt/pricing?utm_source=website&utm_medium=blog&utm_campaign=crawl&utm_term=distributed-web-crawler-nodejs), depois [crie uma conta Scrapeless](https://app.scrapeless.com/passport/login?utm_source=website&utm_medium=blog&utm_campaign=crawl&utm_term=distributed-web-crawler-nodejs). Direcione a aquisição através do Crawl e meça as páginas aceitas e o frescor antes de aumentar o número de trabalhadores.

## Perguntas Frequentes

### O que torna um rastreador da web distribuído?

Sua fronteira de URL e estado de trabalho são compartilhados entre múltiplos processos de trabalhadores ou máquinas. Os trabalhadores podem reivindicar trabalhos independentes, escrever resultados em armazenamento compartilhado e escalar sem depender da memória de um único processo.

### Por que usar BullMQ para um rastreador Node.js?

BullMQ fornece filas suportadas pelo Redis, trabalhadores distribuídos, controles de concorrência, eventos e identificadores de trabalhos. O rastreador ainda precisa de sua própria política de URL, contrato de conteúdo, armazenamento durável e observabilidade.

### Como a deduplicação de URL funciona entre trabalhadores?

Normalize a URL antes de enfileirar, hash a forma canônica e use o digest como o ID do trabalho da fila e chave de armazenamento. O Redis coordena a criação de jobs, enquanto o banco de dados preserva a idempotência após a retenção da fila remover trabalhos antigos.

### Cada trabalhador deve lançar seu próprio navegador?

Nem sempre. Navegadores locais aumentam o tamanho do contêiner, uso de memória e trabalho operacional. Uma camada de aquisição gerenciada pode retornar conteúdo renderizado enquanto os trabalhadores permanecem focados em agendamento, validação, descoberta e armazenamento.

### Como devem ser tratados os trabalhos de páginas com falha?

Separe os resultados de negócios de eventos de infraestrutura. Conteúdo inválido pode ser rejeitado; erros de execução não resolvidos podem entrar em uma fila de quarentena para inspeção e liberação explícita. Evite um loop automático ilimitado.

Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.

Artigos mais populares

Catálogo