Arquitetura de Scraping em Larga Escala: Um Guia de Campo

Algumas centenas de páginas é um script. Milhões é um sistema distribuído. Aqui está a arquitetura que te leva até lá — filas, trabalhadores assíncronos, camadas de proxy, tentativas, deduplicação e verificações de qualidade de dados, com código funcional.

Fazer scraping de algumas centenas de páginas é um script. Fazer scraping de milhões é um sistema distribuído. Quando seu alvo passa de "roda durante a noite no meu laptop" para "precisa terminar esta semana sem derreter", a parte difícil deixa de ser o parsing e passa a ser tudo ao redor — como você coloca o trabalho em fila, distribui, evita bloqueios, tenta novamente em caso de falhas, e armazena e verifica o resultado. Isso é scraping em larga escala como uma arquitetura, não um trecho de código, com código funcional em cada etapa.

Escala é concorrência, não um loop maior

Um número torna isso concreto. Pegue uma categoria com 20.000 páginas de listagem, 20 itens cada — 400.000 páginas para buscar. A 2,5 segundos por página, uma execução estritamente sequencial leva cerca de 1.000.000 segundos, aproximadamente 11,5 dias esperando o carregamento das páginas antes de analisar um único campo. Dirija 200 páginas em paralelo e esses 11,5 dias colapsam para cerca de uma hora de tempo de relógio. O tempo é a restrição em escala, e a concorrência é como você o recupera. Todo o resto na arquitetura existe para tornar essa concorrência viável.

Diagrama de estatísticas mostrando 400.000 páginas levando 11,5 dias sequencialmente versus cerca de uma hora com 200 em paralelo, com 10.000 falhas por milhão a 1 por cento
A concorrência transforma uma execução de 11,5 dias em uma hora — mas com um milhão de requisições, mesmo uma taxa de falha de 1% são 10.000 páginas mortas.

A arquitetura em um relance

Um scraper que sobrevive a milhões de páginas é um pequeno sistema distribuído com algumas partes nomeadas, cada uma resolvendo um problema que só aparece em volume:

Primeiro a fila: desacople a descoberta da busca

A decisão estrutural mais importante é colocar uma fila entre "o que buscar" e "fazer a busca". Um produtor enumera URLs; um conjunto de trabalhadores as esgota. Nenhum dos lados sabe quão rápido o outro opera, e você adiciona trabalhadores sem tocar no produtor. Em Python, isso é Celery ou RQ sobre Redis; em Node, BullMQ; em escala maior, RabbitMQ ou Kafka. O padrão em um arquivo:

import asyncio, aiohttp

CONCURRENCY = 50
queue = asyncio.Queue()

async def worker(session):
    while True:
        url = await queue.get()
        try:
            async with session.get(url, timeout=20) as resp:
                await handle(url, await resp.text(), resp.status)
        except Exception as err:
            await on_failure(url, err)
        finally:
            queue.task_done()

async def run(urls):
    for u in urls:
        queue.put_nowait(u)
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.create_task(worker(session)) for _ in range(CONCURRENCY)]
        await queue.join()
        for t in tasks:
            t.cancel()

O ajuste que importa é CONCURRENCY. Muito baixo desperdiça o paralelismo que torna a escala possível; muito alto sobrecarrega tanto o alvo quanto sua própria saída. Você encontra o valor certo observando as taxas de erro subirem — e é exatamente por isso que o monitoramento é uma parte de primeira classe do sistema, não um pensamento tardio.

Diagrama de fluxo de um pipeline de scraping em larga escala: fila, trabalhadores assíncronos, camada de proxy, parsing e verificações de qualidade, armazenamento
Oito partes nomeadas. As camadas de proxy, anti-bot e renderização são as mais difíceis de manter saudáveis — e o lugar natural para investir.

A camada de proxy quebra primeiro

Em baixo volume, você mal percebe as defesas anti-bot; em escala, elas quebram a execução primeiro. Envie algumas centenas de milhares de requisições de um IP e você é limitado por taxa, depois desafiado, depois bloqueado. A solução é a rotação entre muitos endereços. Estruture: IPs de datacenter baratos para alvos e APIs lenientes, IPs residenciais rotativos para alvos comerciais difíceis que esperam tráfego de usuários reais. Mas a rotação sozinha não é suficiente — defesas modernas também leem impressões digitais TLS e a ordem dos cabeçalhos, então o tráfego precisa parecer um navegador, não apenas vir de um IP novo.

Tentativas: falha é o estado constante

Com um milhão de requisições, uma taxa de falha transitória de 1% são 10.000 páginas falhadas. Falha não é um caso extremo nesse volume — é rotina, e o pipeline precisa tratar uma busca falhada como normal em vez de fatal. Tente novamente com recuo exponencial e um limite, depois mova a URL para uma fila de mensagens mortas em vez de bloquear a execução. Leia por que falhou: um tempo limite ou 503 vale a pena tentar novamente, um 404 definitivo não.

import asyncio, random

async def fetch_with_retry(session, url, tries=4):
    for attempt in range(tries):
        try:
            async with session.get(url, timeout=20) as r:
                if r.status == 404:
                    return None                # don't retry a hard 404
                if r.status < 400:
                    return await r.text()
        except Exception:
            pass
        await asyncio.sleep(2 ** attempt + random.random())  # backoff + jitter
    await dead_letter(url)                     # give up after the cap
    return None

Deduplicação: não rastreie a mesma página duas vezes

A descoberta em escala produz duplicatas constantemente — o mesmo produto acessível por três caminhos, parâmetros de rastreamento que fazem uma página parecer dez. Normalize URLs antes de entrarem na fila, depois mantenha um conjunto de vistos (um conjunto Redis, ou um filtro de Bloom quando o conjunto atinge centenas de milhões):

from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode

seen = set()

def normalize(url):
    s = urlsplit(url.lower())
    q = [(k, v) for k, v in parse_qsl(s.query) if not k.startswith("utm_")]
    return urlunsplit((s.scheme, s.netloc, s.path.rstrip("/"), urlencode(sorted(q)), ""))

def enqueue(url):
    key = normalize(url)
    if key not in seen:
        seen.add(key)
        queue.put_nowait(key)

Armazenamento e qualidade de dados

Dois hábitos específicos de escala: escreva em lotes para que o armazenamento não seja seu gargalo, e separe bruto de analisado para que você possa reanalisar sem rastrear novamente quando os seletores mudarem. Depois adicione a metade do monitoramento que a maioria das equipes ignora — verificações de qualidade de dados. Uma execução pode relatar 100% de sucesso HTTP e ainda produzir lixo se o layout mudou e seus seletores agora não correspondem a nada. Assegure-se de que os campos obrigatórios não estão vazios e os valores são sensatos:

def validate(row):
    assert row.get("title"), "empty title — selector may have drifted"
    price = row.get("price")
    assert isinstance(price, (int, float)) and 0 < price < 1_000_000, "bad price"
    return row

# fail loud on page 5,000, not silently after 5,000,000 empty rows

Descarregue proxies, anti-bot e renderização para uma API

Renderize apenas quando necessário

Um navegador sem cabeça é a operação mais cara no pipeline — CPU, memória e segundos por página, que dominam tudo em um milhão de páginas. Muitos sites ainda enviam dados no HTML inicial ou em um endpoint JSON; uma busca simples mais um analisador é uma ordem de magnitude mais barata. Tente o caminho barato primeiro, confirme que os campos estão presentes, e escale para renderização apenas para as páginas que precisam. Nossa análise de custo de renderização versus HTTP coloca números reais nessa diferença.

Construir versus comprar as camadas difíceis

Tudo acima é construível, então a pergunta honesta é quais partes merecem seu tempo de engenharia. O modelo de dados, a lógica de parsing, as verificações de qualidade e o esquema de armazenamento são específicos para o seu projeto — só você pode construí-los bem. O pool de proxies, o manuseio anti-bot, a frota de renderização sem cabeça e a fila de tentativas e entregas são infraestrutura genérica que é cara de construir e um trabalho árduo de manter saudável à medida que os alvos evoluem. É aí que uma Scraper API gerenciada se posiciona: alugue as partes que são as mesmas para todos. Nossa análise de construir versus comprar trabalha o imposto de manutenção em detalhe, e executar scrapers como software de produção cobre como manter tudo observável.

Perguntas frequentes

Como você faz scraping de milhões de páginas?

Com concorrência, não um loop maior. Coloque uma fila entre a descoberta de URLs e a busca, esgote-a com um conjunto de trabalhadores assíncronos ou distribuídos, rotacione IPs para evitar bloqueios, tente novamente falhas transitórias com recuo, deduplique URLs e escreva em lotes para o armazenamento. Uma execução sequencial de um milhão de páginas leva dias; o mesmo trabalho através de um conjunto de trabalhadores concorrentes termina em horas.

Qual é a melhor arquitetura para scraping em larga escala?

Um pipeline de fila e trabalhadores: um produtor enumera URLs em uma fila (Redis, RabbitMQ ou Kafka), trabalhadores buscam simultaneamente através de uma camada de proxy rotativa, renderizam apenas as páginas que precisam de JavaScript, tentam novamente falhas em uma fila de mensagens mortas, deduplicam com um conjunto de vistos e armazenam dados brutos e analisados separadamente. Envolva tudo em monitoramento com verificações de qualidade de dados para que desvios apareçam cedo.

Quantas requisições você pode executar em paralelo?

Depende do alvo e da sua saída, não de um número fixo. Scraping é limitado por IO, então uma máquina modesta pode manter muitas centenas de requisições em andamento. Comece com uma concorrência de cerca de 50, observe a taxa de erro e aumente até que as falhas subam — esse é o seu limite. Além dos limites de uma máquina, adicione trabalhadores distribuídos em vez de forçar um único nó.

Como você lida com falhas em escala?

Assuma a falha — com um milhão de requisições, mesmo uma taxa de erro de 1% são 10.000 páginas mortas. Tente novamente erros transitórios (tempos limite, 503s) com recuo exponencial e jitter, limite as tentativas e mova falhas persistentes para uma fila de mensagens mortas em vez de bloquear a execução. Não tente novamente 404s definitivos. Randomizar a ordem de coleta também espalha falhas para que você não falhe nas mesmas páginas em toda execução.

Escala é principalmente as partes que não são divertidas de construir: rotação de IPs, anti-bot, renderização sem cabeça, filas e tentativas. Possua as peças específicas para seus dados — o modelo, os parsers, as verificações de qualidade — e alugue a infraestrutura genérica que é o mesmo trabalho árduo para todos. Acerte a fila e a concorrência primeiro; todo o resto é fazer essa concorrência sobreviver ao contato com um milhão de páginas reais.

Alimente a camada de busca com IPs residenciais rotativos