Conhecimento

Schema Drift em Dados Raspados: Detectando Extratores Quebrados Antes dos Seus Usuários

Raspadores raramente travam quando um site muda. Eles continuam rodando e retornam dados sutilmente incorretos. Como contratos de dados e profiling em lote detectam desvios cedo.

Matt Brown

Matt Brown

29 de setembro de 2026 · 9 min de leitura

O pior tipo de falha em scraping não parece uma falha. O job roda, as requisições têm sucesso, as linhas chegam ao data warehouse no horário previsto. Então alguém do financeiro pergunta por que o preço médio triplicou da noite para o dia, ou por que metade do catálogo está de repente fora de estoque, e a investigação leva de volta a um redesign do site feito três semanas antes que ninguém notou.

Isso é drift de schema: os dados continuam fluindo, mas seu formato ou significado mudou silenciosamente. É o modo de falha normal dos dados da web, porque as fontes mudam sem aviso e os extratores continuam produzindo saída. Este guia cobre as duas camadas que detectam isso: contratos no nível de registro que rejeitam valores ruins, e perfis no nível de lote que percebem quando os dados como um todo deixam de se parecer consigo mesmos. Ele também mostra, com um pequeno teste, por que você precisa de ambos.

Principais conclusões

  • O drift costuma ser silencioso. Uma página alterada raramente quebra o extrator; ela faz o extrator retornar algo plausível e errado.
  • Contratos no nível de registro verificam cada registro segundo regras: campos obrigatórios, tipos, intervalos e formatos. Eles pegam as quebras óbvias.
  • Perfis no nível de lote comparam cada execução com uma linha de base: taxas de preenchimento, mix de tipos e medianas. Eles pegam as quebras que os contratos não conseguem ver.
  • No nosso teste com três drifts realistas, os contratos pegaram um. Os outros dois passaram em todas as verificações no nível de registro e só foram detectados pelo perfil de lote.
  • Alerte sobre o drift antes que os dados sejam enviados, coloque o lote em quarentena e registre qual site e qual versão do extrator o produziram.

Como o drift realmente acontece

CausaO que os dados fazem
Redesign muda a marcaçãoUm seletor corresponde a um elemento diferente, ou a nenhum
Formato de preço muda”9.99” vira “€9.99”, “9,99” ou 999 em unidades menores
Um campo passa a depender de JavaScriptO HTML estático deixa de contê-lo, então ele volta vazio
Localização ou variante geográficaUma moeda, idioma ou unidade diferente aparece em algumas páginas
Teste A/BUma fração das páginas usa um novo layout, então uma fração dos registros quebra
Página de bloqueio ou desafioA página carrega, o extrator roda, e retorna nada ou lixo

O ponto em comum é que nenhum desses casos gera uma exceção. O último, páginas que carregam com sucesso mas não são a página que você queria, é abordado em a taxa de falha silenciosa. O resto é o extrator processando fielmente uma página que mudou por baixo dele.

Camada 1: contratos no nível de registro

Um contrato de dados define como é um registro válido: quais campos são obrigatórios, seus tipos, seus intervalos e formatos permitidos. Cada registro é verificado antes de ser aceito, e as violações são contadas e colocadas em quarentena em vez de armazenadas silenciosamente.

import re
import statistics
from collections import Counter

CONTRACT = {
    "name":     {"type": str, "required": True},
    "price":    {"type": float, "required": True, "min": 0.01, "max": 100_000},
    "currency": {"type": str, "required": True, "pattern": r"^[A-Z]{3}$"},
    "in_stock": {"type": bool, "required": False},
}


def violations(record, contract=CONTRACT):
    """Record-level checks: presence, type, range, format."""
    problems = []
    for field, rule in contract.items():
        value = record.get(field)
        if value in (None, ""):
            if rule.get("required"):
                problems.append(f"{field}: missing")
            continue
        if rule["type"] is float and isinstance(value, int) and not isinstance(value, bool):
            value = float(value)
        if not isinstance(value, rule["type"]):
            problems.append(f"{field}: expected {rule['type'].__name__}, got {type(value).__name__}")
            continue
        if "min" in rule and value < rule["min"] or "max" in rule and value > rule["max"]:
            problems.append(f"{field}: {value} out of range")
        if "pattern" in rule and not re.match(rule["pattern"], value):
            problems.append(f"{field}: {value!r} bad format")
    return problems

Um registro como {"name": "x", "price": "€9.99", "currency": "eur"} falha duas vezes: o preço é uma string, e a moeda não é um código de três letras maiúsculas. Contratos são baratos, explícitos e fáceis de raciocinar. Seu limite é que cada registro é julgado isoladamente, e muito drift produz registros que são individualmente válidos.

Camada 2: perfis no nível de lote

Um perfil resume um lote inteiro: para cada campo, com que frequência ele é preenchido, quais tipos aparecem e, para números, a mediana. Comparar o perfil de cada execução com uma linha de base de execuções saudáveis recentes mostra quando os dados como um todo mudam de forma, mesmo que cada registro passe em seu contrato.

def profile(records, fields=CONTRACT):
    """Batch-level shape: how often each field is filled, its types, and numeric medians."""
    n = max(1, len(records))
    out = {}
    for field in fields:
        values = [r.get(field) for r in records]
        present = [v for v in values if v not in (None, "")]
        numbers = [float(v) for v in present if isinstance(v, (int, float)) and not isinstance(v, bool)]
        out[field] = {
            "fill_rate": len(present) / n,
            "types": Counter(type(v).__name__ for v in present),
            "median": statistics.median(numbers) if numbers else None,
            "distinct": len(set(map(str, present))),
        }
    return out


def drift(baseline, current, fill_drop=0.1, median_ratio=3.0):
    """Compare two batch profiles and describe what changed shape."""
    alerts = []
    for field, base in baseline.items():
        cur = current[field]
        if base["fill_rate"] - cur["fill_rate"] > fill_drop:
            alerts.append(f"{field}: filled {base['fill_rate']:.0%} -> {cur['fill_rate']:.0%}")
        if set(cur["types"]) - set(base["types"]):
            alerts.append(f"{field}: new types {sorted(set(cur['types']) - set(base['types']))}")
        if base["median"] and cur["median"]:
            ratio = cur["median"] / base["median"]
            if ratio > median_ratio or ratio < 1 / median_ratio:
                alerts.append(f"{field}: median {base['median']:g} -> {cur['median']:g}")
    return alerts

Os limiares são pontos de partida. Uma queda de 10 pontos na taxa de preenchimento ou uma mudança de três vezes em uma mediana raramente é normalidade; ajuste ambos por campo depois de ter algumas semanas de histórico.

Por que você precisa de ambos: um pequeno teste

Geramos uma linha de base saudável de 1.000 registros de produtos, depois simulamos três quebras realistas e rodamos as duas camadas em cada uma.

DriftO que aconteceuContrato no nível de registroPerfil de lote
Preço formatadoUm redesign colocou o símbolo da moeda dentro do preço em 30% das páginasDetectado: 300 registros rejeitadosDetectado: novo tipo str no preço
Unidades menoresPreços começaram a chegar como 6305 em vez de 63.05Não detectado: todos os registros passaramDetectado: mediana de 63.05 para 6305, e um novo tipo int
Seletor de estoque quebradoO campo in-stock voltou vazio em 80% das páginasNão detectado: o campo é opcionalDetectado: preenchimento de 100% para 20%

O contrato pegou o único drift que produziu valores obviamente malformados. Os outros dois produziram registros que eram individualmente válidos, um número positivo dentro do intervalo e um campo opcional deixado vazio, e só a visão em lote mostrou que algo havia mudado. O caso das unidades menores não é hipotético: um endpoint de produto real que examinamos na semana passada retornou 10000 para um sapato de $100.00, como descrito em pare de fazer parsing de HTML.

O que fazer quando o drift dispara

  1. Coloque o lote em quarentena. Não envie dados de um lote com alertas de drift até que alguém tenha analisado. Um conjunto de dados atrasado é melhor do que um errado.
  2. Localize o escopo. Divida o alerta por site, tipo de página e versão do extrator. O drift geralmente começa em um site após uma mudança.
  3. Compare uma amostra. Observe um punhado de registros afetados ao lado das páginas de onde vieram. A causa geralmente é óbvia em poucos minutos.
  4. Corrija e faça o backfill. Atualize o extrator, depois reextraia o período afetado a partir das páginas armazenadas, se você as mantiver, ou recolete se não mantiver.
  5. Atualize a linha de base deliberadamente. Quando uma mudança é legítima, como um site genuinamente mudando de moeda, redefina a linha de base de propósito, nunca automaticamente.

Torne o drift menos provável desde o início

Algumas fontes sofrem menos drift do que outras. Dados estruturados incorporados para mecanismos de busca mudam com muito menos frequência do que o layout da página, e é por isso que extraí-los primeiro reduz a frequência com que os contratos disparam. Observar as próprias páginas também ajuda: uma mudança de template detectada por detecção de mudanças em escala é um aviso antecipado de que a extração pode estar prestes a quebrar, e uma taxa crescente de violações de contrato em um site é um insumo forte para uma pontuação de saúde do alvo. Coletar de forma consistente de um único mercado por job também elimina toda uma classe de drift causada por variantes geográficas e de moeda.

A conclusão

Os dados da web sofrem drift porque a web muda sem pedir permissão. O perigo não é que os extratores quebrem, mas que eles continuem funcionando em páginas que já não são o que eram quando foram construídos, e produzam dados que parecem corretos quando analisados registro por registro.

Verifique cada registro contra um contrato, e verifique cada lote contra seu próprio histórico recente. No nosso teste, o contrato sozinho pegou um drift em três; o perfil de lote pegou os três. Juntos, eles transformam “o painel parecia estranho por três semanas” em um alerta na manhã em que o problema começou.

Fontes e referências

  • Teste conduzido pela Shifter em 29 de setembro de 2026 com o código acima, em 1.000 registros de produtos gerados e três drifts simulados.

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $ 0,75/GB.

Começar