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.
| Camada | Conteúdo | Por que existe |
|---|---|---|
| Landing bruta | Toda resposta bem-sucedida, como recebida, com metadados de busca | Reproduzir a extração sem buscar novamente |
| Observações tipadas | Valores parseados, tipados e validados com um status de parsing | O que analistas e aplicações consultam |
| Estado atual | O valor mais recente por entidade, derivado das observações | Leituras 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.
| Status | Comportamento do pipeline |
|---|---|
408, 422, 500 | Tentar novamente com backoff exponencial |
429 | Recuar e reduzir a concorrência de workers |
400, 401, 403 | Erro de configuração: enviar para uma fila de mensagens mortas e alertar, nunca tentar novamente |
509 | Cré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étrica | O que ela detecta |
|---|---|
| Buscas bem-sucedidas versus linhas carregadas | Perdas do carregador entre a API e o banco de dados |
Conflitos de job_id duplicado | Tempestades de novas tentativas ou sobreposição de agendamento |
| Taxa de nulos por campo e versão do extrator | Mudanças de marcação e seletores quebrados |
| Profundidade da fila e atraso de carregamento | Um carregador ficando para trás em relação ao estágio de busca |
Créditos consumidos versus linhas parseadas como ok | Dinheiro 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.