Extração de dados

Movendo Dados de API de Web Scraping para Bancos de Dados SQL em Escala

Buscar os dados é a parte fácil. Como levar a saída de uma API de scraping para o SQL com cargas idempotentes, campos tipados, alertas de schema-drift e proveniência intacta.

Chris Collins

Chris Collins

14 de setembro de 2026 · 11 min de leitura

Uma API de web scraping remove a parte mais difícil da coleta: proxies, renderização, novas tentativas e bloqueios. O que ela não remove é tudo o que acontece depois que a resposta chega, e é aí que a maioria dos pipelines realmente falha.

As falhas são silenciosas. Linhas duplicadas de uma nova tentativa que ninguém deduplicou. Uma coluna de preço cheia de strings como "$16.99" que alguém converteu para número em um dashboard. Um redesign do site que transformou um campo em null três semanas atrás. Um bug no extrator que não pode ser corrigido sem pagar para buscar tudo de novo.

Este guia trata do lado do carregamento: levar a saída da API de scraping para um banco de dados SQL de forma confiável, em volume, sem perder a capacidade de explicar ou reproduzir o que foi armazenado. Os exemplos usam PostgreSQL e a Shifter Web Scraping API, e os padrões se aplicam a qualquer banco de dados SQL.

Comece pela resposta que você realmente recebe

Com a Shifter Web Scraping API você escolhe entre dois formatos de resposta.

HTML bruto, onde você faz o parsing do seu lado. Ou JSON estruturado, passando extract_rules que mapeiam seletores CSS para campos:

curl "https://scrape.shifter.io/v1?api_key=YOUR_API_KEY&url=https://shop.example.com/p/42&render_js=1&extract_rules=%7B%22title%22%3A%7B%22selector%22%3A%22h1%22%2C%22output%22%3A%22text%22%7D%2C%22price%22%3A%7B%22selector%22%3A%22.price%22%2C%22output%22%3A%22text%22%7D%7D"

# {"title": "Example Product", "price": "$19.99"}

Duas propriedades dessa saída moldam seu esquema. Um campo cujo seletor não corresponde a nada volta como null em vez de falhar a requisição, então um elemento ausente e um seletor quebrado parecem idênticos na resposta. E as saídas de texto são strings de exibição, então preços, avaliações e datas chegam formatados para humanos, não tipados para um banco de dados. A sintaxe das regras, incluindo extração de listas para páginas de resultados, está na documentação de regras de extração.

Três camadas, não uma tabela

O design que sobrevive ao contato com a produção separa o que você recebeu do que você concluiu.

CamadaConteúdoPor que existe
Landing brutaToda resposta bem-sucedida, como recebida, com metadados de buscaReproduzir a extração sem buscar novamente
Observações tipadasValores parseados, tipados e validados com um status de parsingO que analistas e aplicações consultam
Estado atualO valor mais recente por entidade, derivado das observaçõesLeituras rápidas para produtos e dashboards

A camada bruta é a que as equipes pulam e depois lamentam. Créditos são gastos em requisições bem-sucedidas, então um bug no seu parsing que só pode ser corrigido buscando novamente custa o crawl inteiro duas vezes. Faça o landing da resposta primeiro, faça o parsing depois, e uma correção no parser se torna uma consulta de reprodução.

A tabela de landing

CREATE TABLE scrape_raw (
  job_id            text        PRIMARY KEY,
  source_url        text        NOT NULL,
  market            text        NOT NULL,
  fetched_at        timestamptz NOT NULL,
  http_status       smallint    NOT NULL,
  body              jsonb       NOT NULL,
  body_hash         text        NOT NULL,
  extractor_version text        NOT NULL
);

CREATE INDEX scrape_raw_url_time ON scrape_raw (source_url, fetched_at DESC);

Algumas escolhas deliberadas ali.

job_id é a chave de idempotência, calculada antes da requisição a partir da URL e da janela de agendamento, então uma nova tentativa do mesmo job lógico cai na mesma chave em vez de criar uma segunda linha. extractor_version registra qual conjunto de regras de extração produziu o corpo, o que é o que permite distinguir depois uma mudança no site de uma mudança nas regras. market registra onde a observação foi feita, porque a mesma URL pode retornar conteúdo diferente por país. E a chave de API nunca é armazenada em nenhum lugar nos metadados da requisição que você persiste, já que credenciais em um banco de dados são credenciais em todo backup.

Carregando de forma idempotente

import hashlib
import json
import os

import psycopg
import requests
from psycopg.types.json import Jsonb

API = "https://scrape.shifter.io/v1"
RULES = {
    "title": {"selector": "h1", "output": "text"},
    "price": {"selector": ".price", "output": "text"},
}
EXTRACTOR_VERSION = "product-v3"

INSERT_RAW = """
INSERT INTO scrape_raw
  (job_id, source_url, market, fetched_at, http_status, body, body_hash, extractor_version)
VALUES (%s, %s, %s, now(), %s, %s, %s, %s)
ON CONFLICT (job_id) DO NOTHING
"""


def job_id(url: str, market: str, window: str) -> str:
    return hashlib.sha256(f"{url}|{market}|{window}".encode()).hexdigest()


def fetch(url: str, market: str) -> requests.Response:
    params = {
        "api_key": os.environ["SHIFTER_API_KEY"],
        "url": url,
        "render_js": 1,
        "country": market,
        "extract_rules": json.dumps(RULES),
    }
    return requests.get(API, params=params, timeout=90)


def land(conn: psycopg.Connection, url: str, market: str, window: str) -> None:
    resp = fetch(url, market)
    if resp.status_code != 200:
        raise RuntimeError(f"{resp.status_code} for {url}")
    body = resp.text
    with conn.cursor() as cur:
        cur.execute(
            INSERT_RAW,
            (
                job_id(url, market, window),
                url,
                market,
                resp.status_code,
                Jsonb(json.loads(body)),
                hashlib.sha256(body.encode()).hexdigest(),
                EXTRACTOR_VERSION,
            ),
        )

ON CONFLICT (job_id) DO NOTHING é o que torna o carregamento seguro para tentar novamente. Seja a falha na rede, no seu worker ou no banco de dados, executar o job novamente não pode produzir uma duplicata. Para MySQL, o equivalente é uma chave única com INSERT IGNORE ou ON DUPLICATE KEY UPDATE.

Vazão: desacople a busca do carregamento

O lado da busca tem um teto rígido definido pelo limite de concorrência do seu plano, e requisições acima dele retornam 429. O banco de dados tem seu próprio teto, e geralmente é o que as equipes atingem primeiro ao abrir uma conexão e uma transação por página coletada.

Coloque uma fila entre eles. Workers de busca, dimensionados conforme seu limite de concorrência, escrevem respostas na fila. Um pequeno número de workers de carregamento a esvazia em lotes. Para volumes constantes, o executemany do psycopg em lotes de algumas centenas de linhas é suficiente. Para grandes cargas retroativas, faça COPY do lote para uma tabela de staging não logada e mescle em uma única instrução:

INSERT INTO scrape_raw
SELECT * FROM scrape_raw_staging
ON CONFLICT (job_id) DO NOTHING;

Isso transforma milhares de idas e voltas em uma só, e mantém intacta a garantia de idempotência.

Para renderizações longas, a API pode entregar de forma assíncrona: passe webhook=<URL> e a resposta é postada para seu endpoint quando estiver pronta. Torne esse receptor idempotente no job_id também, porque qualquer entrega HTTP pode acabar sendo repetida por um dos lados.

Mapeie os erros da API para o comportamento do pipeline

Nem todos os códigos de status são candidatos a nova tentativa, e um carregador que os trata de forma uniforme ou satura uma configuração quebrada ou desiste de falhas transitórias. A tabela completa está em erros e limites.

StatusComportamento do pipeline
408, 422, 500Tentar novamente com backoff exponencial
429Recuar e reduzir a concorrência de workers
400, 401, 403Erro de configuração: enviar para uma fila de mensagens mortas e alertar, nunca tentar novamente
509Créditos esgotados: parar o estágio de busca e alertar, tentar novamente não ajuda

Requisições falhas e respostas 4xx ou 5xx do destino não são cobradas, e a API já tenta novamente falhas transitórias até três vezes antes de retornar, então suas próprias tentativas custam tempo em vez de créditos. Elas ainda custam tempo, e é por isso que o backoff importa.

Tipando as observações

É aqui que strings de exibição se tornam dados, e onde a maioria dos erros silenciosos é introduzida.

CREATE TABLE price_observation (
  source_url    text          NOT NULL,
  market        text          NOT NULL,
  observed_at   timestamptz   NOT NULL,
  price_amount  numeric(12,2),
  currency      char(3),
  raw_price     text,
  parse_status  text          NOT NULL,
  job_id        text          NOT NULL REFERENCES scrape_raw (job_id),
  PRIMARY KEY (source_url, market, observed_at)
);

Três regras mantêm isso honesto.

Mantenha a string bruta junto ao valor parseado. raw_price é o que permite auditar um número suspeito sem buscar novamente.

Faça o parsing por mercado, não de forma global. "1.299,00" e "1,299.00" são o mesmo preço sob convenções diferentes, e um símbolo de moeda não é uma moeda: $ é dólar americano, canadense e australiano dependendo da loja. Resolva o código ISO a partir do símbolo e do mercado juntos.

Registre por que um valor é nulo. Um parse_status de missing, unparseable ou ok separa “a página não tinha preço” de “nosso parser falhou”, algo que o null da API não consegue informar por si só.

Para o estado atual, derive em vez de manter. No PostgreSQL:

CREATE VIEW price_current AS
SELECT DISTINCT ON (source_url, market) *
FROM price_observation
WHERE parse_status = 'ok'
ORDER BY source_url, market, observed_at DESC;

Uma view derivada não pode ficar dessincronizada do histórico que resume.

Detecte a deriva de esquema antes que seus usuários o façam

Sites mudam sua marcação, e um seletor alterado não gera um erro. Ele retorna null, a requisição é bem-sucedida, um crédito é gasto, e a linha chega parecendo válida.

A defesa é um monitor de taxa de nulos por campo, por fonte, por versão do extrator. Calcule a proporção de status de parsing missing para cada campo em uma janela móvel e alerte quando ela se afastar bruscamente de sua linha de base. Um campo de preço que passa de 2% de ausência para 60% de ausência da noite para o dia é um redesign, e detectar isso no mesmo dia é a diferença entre um seletor corrigido e três semanas de histórico inutilizável.

Quando você corrigir, aumente o extractor_version e reproduza as linhas brutas afetadas pelo novo parser. Esse é o retorno de fazer o landing das respostas brutas.

Retenção e particionamento

As tabelas de landing bruta crescem mais rápido e são as menos lidas. Particione-as por data de busca, mantenha-as por tempo suficiente para cobrir sua janela realista de reprodução, e descarte partições antigas em vez de excluir linhas. As tabelas de observação são o registro histórico e geralmente merecem uma retenção mais longa, particionadas da mesma forma.

O que monitorar

MétricaO que ela detecta
Buscas bem-sucedidas versus linhas carregadasPerdas do carregador entre a API e o banco de dados
Conflitos de job_id duplicadoTempestades de novas tentativas ou sobreposição de agendamento
Taxa de nulos por campo e versão do extratorMudanças de marcação e seletores quebrados
Profundidade da fila e atraso de carregamentoUm carregador ficando para trás em relação ao estágio de busca
Créditos consumidos versus linhas parseadas como okDinheiro gasto em respostas que você não pôde usar

Essa última métrica é a visão de custo que importa: créditos por linha utilizável, não créditos por requisição. O uso e a taxa de erro da própria API ficam visíveis no painel em Web Scraping API.

Perguntas frequentes

Devo armazenar HTML ou JSON extraído na camada bruta?

O JSON extraído é muito menor e geralmente suficiente. Armazene HTML apenas para fontes onde você espera mudar a lógica de extração com frequência, e dê a essa tabela uma retenção curta.

JSONB é bom o suficiente para consultar diretamente?

Para exploração, sim. Para qualquer coisa da qual um produto ou dashboard dependa, promova os campos para colunas tipadas, onde o banco de dados pode impor tipos e usar índices comuns.

Como evito pagar por páginas que não mudaram?

Toda requisição bem-sucedida custa um crédito, então a economia precisa vir de buscar menos, não de escrever menos. Use sinais baratos, como uma página de listagem ou sitemap, para decidir quais páginas de detalhe precisam ser buscadas.

Isso funciona com a API da Amazon também?

Sim. Suas respostas já são JSON estruturado, então as regras de extração ficam de fora, mas os preços ainda chegam como strings de exibição e os padrões de landing, tipagem e deriva se aplicam sem alterações.

Conclusão

Uma API de scraping resolve a coleta. O design do seu banco de dados decide se o que você coletou permanece confiável. Faça o landing de toda resposta bem-sucedida com uma chave de idempotência e uma versão de extrator, tipe os valores por mercado mantendo a string bruta, registre por que um valor é nulo, derive o estado atual em vez de mantê-lo, e observe as taxas de nulos por campo para que mudanças de marcação apareçam no mesmo dia.

Para a Amazon especificamente, as opções são comparadas em as melhores APIs de web scraping para monitoramento da Amazon, e uma versão do mesmo pipeline para o setor imobiliário está em como empresas do setor imobiliário usam APIs de web scraping. O produto está na página Web Scraping API, com planos na página de preços.

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