🎯 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

Como Construir um Scraper do Etsy com o Scrapeless Scraping Browser: Guia Abrangente 2026 (Node.js)

James Thompson
James Thompson

Scraping and Proxy Management Expert

16-Apr-2026

Principais Pontos:

  • Scrapeless Scraping Browser atua como uma poderosa infraestrutura de navegador AI, limpando a camada anti-bot DataDome do Etsy com impressão digital automatizada, proxies residenciais e resolução de CAPTCHA.
  • Quatro modos de descoberta a partir de um bloco CONFIG — URL do produto, URL da categoria, busca por palavra-chave (com expansão opcional) e URL da loja. Troque as entradas, mesmo pipeline.
  • Oito filtros estruturados (em promoção, frete grátis, personalizável, envia para, preço mínimo/máximo, condição, ordenar por) se combinam com qualquer modo de descoberta que utilize URLs de pesquisa ou de categoria.
  • O esquema de saída cobre mais de 30 campos por produto, incluindo variações, breadcrumbs, dataDeListagem, reviews[].photos e sinais de merchandising únicos (isBestseller, isStarSeller, isFreeShipping, inStock, favoritesCount, sub-notas por avaliação).
  • Um loop de repetição configurável (padrão maxRetries: 10, backoff crescente de 3 s → 47 s) muda para uma nova sessão e IP residencial entre as tentativas, absorvendo automaticamente os 403 transitórios.

O Etsy é um poço de ouro para inteligência de e-commerce: preços de vendedores comparáveis para proprietários de lojas, corpora de treinamento de sentimento para projetos de ML e descoberta de nichos para dropshippers fluem a partir das mesmas páginas de listagem. A API oficial do Etsy tem acesso restrito e um longo ciclo de aprovação, os revendedores de dados de terceiros são caros e um scraper personalizado requer manutenção contínua contra o DataDome e mudanças frequentes no nome das classes CSS na frontend do Etsy.

Este guia percorre um único arquivo TypeScript construído com Scrapeless Scraping Browser que lida com todas as partes difíceis desde o início: o navegador em nuvem anti-detectação, os proxies residenciais, o enriquecimento por produto com avaliações e metadados da loja e a técnica de expansão de múltiplas consultas que revela muitos mais resultados a partir de uma única palavra-chave base do que o teto de pesquisas por palavra chave do Etsy normalmente permite. O mesmo scraper suporta quatro modos de descoberta independentes — alimentá-lo com uma URL de produto, uma URL de categoria, uma pesquisa por palavra-chave ou uma URL de loja — e cada linha de saída carrega o mesmo rico esquema de 30 campos, independentemente de como a listagem foi descoberta.


O Que Você Pode Fazer Com Isso

Os dados do Etsy são um ativo versátil, impulsionando aplicações comerciais de alto impacto, desde pesquisa de produtos até análises avançadas de AI. Aqui estão cinco usos comerciais do mundo real, todos alcançáveis a partir da mesma base de código, muitas vezes com apenas uma mudança de configuração:

  1. Pesquisa de dropshipping e descoberta de produtosmodo de pesquisa por palavra-chave. Execute o scraper sobre "suporte de planta de macramé" com expandStrategy: "keywords" em ["boho", "moderno", "minimalista"], configure maxProducts: 200 e classifique a saída por favoritesCount × rating. Filtre para lojas onde isStarSeller: true e favoritesCount está bem acima da mediana — esses são os seus candidatos a dropshipping. Importe o CSV resultante para o Shopify ou uma lista de fornecedores privada. Esta é a razão mais comum pela qual as pessoas extraem dados do Etsy e a mais rápida para gerar receita.
  2. Monitoramento de preços de concorrentesmodo de URL de produto (URL direta). Mantenha uma lista de URLs de listagens de concorrentes em startUrls e execute o scraper à noite. Armazene cada snapshot JSON com seu timestamp scrapedAt e faça a diferença nos valores de price, originalPrice, discountPercent e inStock entre as execuções. Queda de preço superior a 10%? Alerta no Slack. inStock muda de true para false? Marque como um sinal de suprimento. O histórico de preços completo que você constrói dessa forma é o núcleo de cada painel de inteligência competitiva.
  3. Pesquisa de palavras-chave e tendênciasmodo de URL de categoria com filtros. Aponte categoryUrl para uma categoria específica do Etsy (por exemplo, /c/bags-and-purses/wallets-and-money-clips/wallets), aplique combinações de filtros (filters.onSale: true, filters.condition: "new", filters.orderBy: "date_desc"), colete tags e materials em algumas centenas de listagens, conte a frequência e ordene pela soma de favoritesCount nas listagens que usam cada tag. As tags que aparecem em listagens criadas recentemente, mas NÃO em listagens mais antigas são seus sub-nichos em ascensão.
  4. Agregação de avaliações para ML e pesquisa de mercadomodo de palavra-chave ou categoria. Extraia reviews[] em milhares de listagens em um segmento (velas artesanais, por exemplo, ou joias personalizadas), insira reviews[].text em um classificador de sentimento e use itemQuality / shipping / customerService sub-notas como rótulos de treinamento supervisionado quando estiverem presentes. As fotos por avaliação (reviews[].photos[]) lhe dão um corpus de imagens paralelo caso você precise de dados de treinamento visual.
  5. Benchmarking de desempenho da lojamodo shop-URL. Aponte shopUrl para a página de loja de um concorrente (por exemplo, https://www.etsy.com/shop/TexasValleyLeather), defina maxPagesPerQuery: 5 para paginar todo o catálogo e o scraper enumera cada listagem que a loja vende atualmente. Compare vendedores na mesma categoria por shop.totalSales, shop.openedYear, rating, reviewsCount e isStarSeller.

Por que Scrapeless

Scrapeless Scraping Browser oferece ao seu scraper um navegador em nuvem de nível de produção que elimina as verificações do DataDome do Etsy imediatamente — sem plugins furtivos, sem ajuste de impressão digital, sem scripts de rotação de proxy para manter. Conecte-se através de um ponto final WebSocket usando Puppeteer ou Playwright e deixe a infraestrutura lidar com a camada anti-bot.

De imediato, você obtém:

  • Impressão digital anti-detecção que se sustenta em sessões de longa duração
  • Proxies residenciais em mais de 195 países (alvos de preços separados para EUA, GB, DE)
  • Resolução automática de CAPTCHA quando o Etsy apresenta um
  • Gravação de sessão para depurar regressões de seletor posteriormente
  • Pontos finais WebSocket que suportam frameworks baseados em CDP como Puppeteer e Playwright — sem SDK para aprender
  • Pronto para agentes de IA: Integra-se perfeitamente com ferramentas como o Scrapeless MCP Server para conceder aos seus agentes de IA "olhos e mãos" na web.

A integração é uma alteração de uma linha: aponte puppeteer.connect() para uma URL Scrapeless em vez de um navegador local. O restante do código permanece exatamente o mesmo — CDP padrão, seletores padrão, fluxos de trabalho padrão. Toda a complexidade do DataDome vive no lado do servidor, fora do seu código.

Obtenha sua chave de API no plano gratuito em app.scrapeless.com.


Pré-requisitos & Instalação

Node.js 18 ou mais recente. Uma chave de API Scrapeless (o nível gratuito cobre tudo neste guia). Um pouco de familiaridade com Puppeteer ajuda. Nenhum Chrome local é necessário — o navegador roda na nuvem da Scrapeless.

bash Copy
mkdir etsy-scrapeless-browserless && cd etsy-scrapeless-browserless
npm init -y
npm install puppeteer-core dotenv cheerio
npm install -D tsx typescript @types/node @types/cheerio

puppeteer-core aciona o navegador em nuvem; cheerio analisa o HTML renderizado no lado do servidor uma vez que cada página tenha terminado de carregar. Separar a rolagem do lado do navegador da análise do lado do Node mantém cada extrator tipado e testável em unidade contra os documentos HTML salvos.

.env:

Copy
SCRAPELESS_API_KEY=sua_chave_aqui

Um assistente de conexão para todo o scraper. Crie uma URL WSS com o token, país e TTL, e então entregue-a ao puppeteer.connect.

ts Copy
import "dotenv/config";
import puppeteer, { type Browser, type Page } from "puppeteer-core";
import * as cheerio from "cheerio";

// Assistente — puxe o HTML completo da página e analise-o com o cheerio. O chamador
// é responsável por executar qualquer rolagem do lado do navegador / waitForFunction
// primeiro, para que regiões preguiçosas sejam hidratadas. Depois disso, a análise permanece no Node:
// tipada, sem corpos de avaliação convertidos em strings, sem a armadilha tsx `__name`, fácil de
// testar em unidade contra documentos HTML salvos.
async function parseWithCheerio(page: Page): Promise<cheerio.CheerioAPI> {
  const html = await page.content();
  return cheerio.load(html);
}

type ScraperInput = {
  proxyCountry: string;   // por exemplo, "US", "GB", "DE"
  sessionTTL: number;     // segundos, 60–900 permitidos; 600 é um padrão seguro
};

function connectionURL(sessionName: string, cfg: ScraperInput): string {
  const token = process.env.SCRAPELESS_API_KEY;
  if (!token) throw new Error("SCRAPELESS_API_KEY não está definido em .env");
  // Scrapeless fixa o IP residencial durante a vida útil de uma sessão por
  // padrão, então cada navegação de página dentro de um puppeteer.connect usa o
  // mesmo IP de saída. Abrir uma nova sessão (nova conexão) gera um novo IP,
  // que é o que o loop de repetição depende para contornar um IP sinalizado.
  const qs = new URLSearchParams({
    token,
    proxyCountry: cfg.proxyCountry,
    sessionTTL: String(cfg.sessionTTL),
    sessionName,
    sessionRecording: "true",
    // Deixe a Scrapeless ter a impressão digital completa do desktop — UA, tela, fuso horário
    // e idioma. Não é necessário definir manualmente setViewport / setUserAgent.
    fingerprint: JSON.stringify({ platform: "Windows" }),
  });
  return `wss://browser.scrapeless.com/api/v2/browser?${qs.toString()}`;
}

async function openBrowser(sessionName: string, cfg: ScraperInput): Promise<Browser> {
  return puppeteer.connect({
    browserWSEndpoint: connectionURL(sessionName, cfg),
    defaultViewport: null,
  });
}

Essa é a área de superfície específica do Scrapeless — uma URL WSS e um puppeteer.connect. Uma coisa que vale a pena saber antes de escalar isso: uma única sessão puppeteer.connect está vinculada a um único IP residencial durante sua vida útil (verificado ao acessar api.ipify.org três vezes seguidas na mesma instância do navegador — o mesmo IP a cada vez). Abrir uma nova sessão gera um novo IP. Esta é a base sobre a qual o loop de nova tentativa na Etapa 8 se constrói — se uma solicitação no IP desta sessão for bloqueada, fechamos a sessão, abrimos uma nova, obtemos um novo IP e tentamos novamente.

O navegador de scraping Scrapeless possui a impressão digital do navegador na camada de conexão — UA, tamanho da tela, fuso horário e idioma são gerenciados pelo parâmetro de consulta fingerprint: { platform: "Windows" } na URL WSS. Não são necessárias chamadas manuais de setViewport ou setUserAgent. O loop de nova tentativa na Etapa 8 absorve bloqueios transitórios.

A única configuração do lado do navegador é um stub de compatibilidade de uma linha tsx:

ts Copy
async function prepPage(page: Page): Promise<void> {
  // Stub da função auxiliar __name injetada pelo tsx para que os corpos das funções page.evaluate
  // não travem com "__name is not defined" dentro do contexto do navegador.
  await page.evaluateOnNewDocument(
    "(function(){ globalThis.__name = function(f){ return f; }; })()",
  );
}

Aquecimento da sessão

Antes de navegar para uma página de pesquisa ou loja, o scraper carrega a página inicial do Etsy uma vez para estabelecer uma sessão de navegador válida. Sem esta etapa, os endpoints /search e /shop retornam 403 em uma sessão fria:

ts Copy
const ETSY_COUNTRY_PATHS: Record<string, string> = {
  US: "", DE: "de/", GB: "uk/", FR: "fr/", IT: "it/", ES: "es/",
  NL: "nl/", CA: "ca/", AU: "au/", JP: "jp/", IN: "in/",
};

async function warmUpSession(page: Page, proxyCountry: string): Promise<void> {
  const path = ETSY_COUNTRY_PATHS[proxyCountry] ?? "";
  try {
    await page.goto(`https://www.etsy.com/${path}`, {
      waitUntil: "domcontentloaded",
      timeout: 30000,
    });
  } catch {
    // Timeout ou erro de rede é aceitável — os cookies já estão configurados.
  }
  await dismissEtsyConsent(page);
  await delay(1500);
}

O caminho específico do país é importante: um proxy DE acessando etsy.com/de/ retorna 200 e configura os cookies de sessão regionais corretos, enquanto etsy.com/ com um proxy DE retorna 403 e a sessão permanece bloqueada. Verificado nos EUA (64 listagens), DE (60 listagens) e GB (61 listagens) — todos os três retornam resultados de pesquisa na primeira tentativa quando o aquecimento corresponde ao país do proxy. O scraper chama warmUpSession uma vez por sessão de navegador antes da primeira chamada collectSearchResults.


Etapa 2 — Quatro Modos de Descoberta

O scraper aceita quatro maneiras independentes de encontrar listagens, todas no mesmo bloco de CONFIG. Escolha a que corresponde à pergunta anterior e defina exatamente um dos startUrls, shopUrl, categoryUrl ou searchQuery. Se mais de um for definido, a precedência é shopUrlcategoryUrlsearchQuerystartUrls.

Modo URL do Produto (URL direta) — listagens conhecidas, re-coletas noturnas, instantâneas de concorrentes:

ts Copy
const CONFIG: ScraperInput = {
  startUrls: [
    "https://www.etsy.com/listing/547491922/leather-walletwalletman-leather",
    "https://www.etsy.com/listing/1022283131/personalized-slim-wallet-fathers-day",
  ],
  maxProducts: 2,
  // ...outros padrões
};

Modo URL de Categoria — raspagens de categorias inteiras com filtros estruturados:

ts Copy
const CONFIG: ScraperInput = {
  categoryUrl: "https://www.etsy.com/c/bags-and-purses/wallets-and-money-clips/wallets",
  filters: {
    onSale: true,
    freeShipping: true,
    minPrice: 20,
    maxPrice: 60,
    orderBy: "most_relevant",
  },
  maxPagesPerQuery: 2,
  maxProducts: 20,
};

Modo de Busca por Palavra-Chave — descoberta de nichos, pesquisa de tendências, coletas de volume de listagens:

ts Copy
const CONFIG: ScraperInput = {
  searchQuery: "leather wallet",
  expandStrategy: "keywords",                   // "none" | "keywords" | "prices"
  expandKeywords: ["mens", "womens", "vintage"], // adicionados à base quando a expansão = palavras-chave
  maxProducts: 20,
};

Modo URL da Loja — enumere cada listagem em uma loja específica para benchmarking/análise de concorrentes:

ts Copy
const CONFIG: ScraperInput = {
  shopUrl: "https://www.etsy.com/shop/TexasValleyLeather",
  maxPagesPerQuery: 5,
  maxProducts: 40,
};

Todos os quatro modos alimentam o mesmo pipeline de enriquecimento por listagem nas Etapas 4–6 e emitem o mesmo esquema de 30 campos na Etapa 8.

Filtros Estruturados

Oito chaves de filtro opcionais se combinam com searchQuery ou categoryUrl. Defina as que se aplicam, deixe as outras de fora:

Chave Valores Efeito
onSale true Apenas listagens atualmente marcadas como em promoção
freeShipping true Apenas listagens com envio gratuito para o país do proxy
customizable true Apenas listagens personalizáveis
shipsTo Código ISO ex. "US" Deve ser enviado para esse país
minPrice / maxPrice número Faixa de preço (filtro nativo do Etsy)
condition "new" | "vintage" Filtro de condição do Etsy
orderBy "mais_relevante" | "data_desc" | "preço_asc" | "preço_desc" | "maiores_avaliações" Ordenação dos resultados

Controle de Paginação

Defina maxPagesPerQuery: N para iterar explicitamente ?page=1..N em cada URL de descoberta. Sem isso, o scraper rola a página inicial para o alvo e para assim que maxProducts listagens únicas forem coletadas. Use a paginação explícita quando você quiser varridas amplas previsíveis (por exemplo, "raspe as primeiras 5 páginas desta categoria, mesmo que sejam mais de 200 listagens").


Passo 3 — Expansão de Multi-Consulta (a solução alternativa do "limite de resultado" do Etsy)

A interface do consumidor do Etsy limita a paginação bem antes da maioria dos nichos serem esgotados, e a limitação de taxa por IP entra rapidamente em ação sob alto volume de solicitações — qualquer palavra-chave única só revela um segmento de classificação. Para esgotar um nicho, divida a consulta base ao longo de um eixo (palavras-chave ou faixas de preço) e elimine as duplicatas pelos listingId.

Para "carteira de couro", uma expansão de palavras-chave fica assim:

ts Copy
function searchUrlForQuery(query: string, page = 1, priceMin?: number, priceMax?: number) {
  const params = new URLSearchParams({ q: query });
  if (page > 1) params.set("page", String(page));
  if (priceMin !== undefined) params.set("min", String(priceMin));
  if (priceMax !== undefined) params.set("max", String(priceMax));
  return `https://www.etsy.com/search?${params.toString()}`;
}

type ExpandStrategy = "palavras-chave" | "preços" | "nenhum";

function multiQueryExpand(
  base: string,
  cfg: { expandStrategy: ExpandStrategy; expandKeywords: string[]; priceBuckets: [number, number][] }
) {
  if (cfg.expandStrategy === "palavras-chave") {
    const queries = [base, ...cfg.expandKeywords.map((k) => `${k} ${base}`)];
    return queries.map((q) => searchUrlForQuery(q));
  }
  if (cfg.expandStrategy === "preços") {
    return cfg.priceBuckets.map(([min, max]) => searchUrlForQuery(base, 1, min, max));
  }
  return [searchUrlForQuery(base)];
}

["masculinos", "femininos", "vintage"] contra "carteira de couro" produz quatro pesquisas. Execute-as, colete URLs de listagem, elimine duplicatas pelo ID numérico enterrado na URL (/listing/1051861316/...). Defina maxProducts alto o suficiente (algumas dezenas a algumas centenas) para realmente abranger todas as variantes — se o alvo for pequeno, o scraper irá interromper após a primeira consulta que tiver resultados, pulando todo o trabalho de eliminação de duplicatas.

A estrutura de faixas de preço funciona da mesma maneira — diferentes faixas revelam diferentes segmentos de classificação porque o "melhor resultado" do Etsy é influenciado pelo preço em relação aos outros no conjunto de resultados.


Passo 4 — Coletar URLs de Listagens de Cada Pesquisa

Role a barra lateral de resultados o suficiente para acionar cartões carregados de forma preguiçosa, depois pegue todos os links a[href*="/listing/"] dentro de um div.listing-link (com [data-listing-id] como fallback quando o Etsy realiza testes A/B no nome da classe).

ts Copy
type SearchHit = { listingId: string | null; url: string; title: string | null; rank: number };
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function collectSearchResults(page: Page, searchUrl: string, target: number, pageTimeoutMs = 60000): Promise<SearchHit[]> {
  await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 60000 });
  await dismissEtsyConsent(page);
  await delay(2000);

  // Role algumas vezes para acionar cartões carregados de forma preguiçosa. Cada passagem leva um
  // instantâneo cheerio do DOM atual para contar cartões — assim que tivermos
  // o suficiente, paramos de rolar.
  for (let i = 0; i < 6; i++) {
    const $peek = await parseWithCheerio(page);
    if ($peek("[data-listing-id], div.listing-link").length >= target) break;
    await page.evaluate(() => window.scrollBy(0, 1200));
    await delay(900);
  }

  // Analise o DOM estabelecido com cheerio — sem `page.evaluate` round-trip,
  // sem corpos de função stringificados, apenas navegação direta por seletores.
  const $ = await parseWithCheerio(page);
  let cards = $("div.listing-link");
  if (cards.length === 0) cards = $("[data-listing-id]");
  const hits: SearchHit[] = [];
  const seen = new Set<string>();
  cards.each((_, card) => {
    const link = $(card).find('a[href*="/listing/"]').first();
    if (!link.length) return;
    const href = link.attr("href") || "";
    const absolute = href.startsWith("http") ? href : `https://www.etsy.com${href.startsWith("/") ? "" : "/"}${href}`;
    const url = absolute.split("?")[0];
    if (!url || seen.has(url)) return;
    seen.add(url);
    const titleEl = $(card).find("h3").first();
    const title = titleEl.length ? titleEl.text().trim() : (link.attr("title") || null);
    const idMatch = url.match(/\/listing\/(\d+)/);
    const listingId = idMatch ? idMatch[1] : null;
    hits.push({ listingId, url, title, rank: hits.length + 1 });
  });
  return hits;
}

A rolagem permanece em page.evaluate porque é uma ação DOM ao vivo (acionando o carregamento preguiçoso do Etsy), mas cada peça de análise passa pelo cheerio em uma captura de page.content(). Esse é o mesmo padrão usado por todos os seis extratores de enriquecimento nos Passos 6–7.
A chamada dismissEtsyConsent está lá para sessões não dos EUA, onde o Etsy serve um portal "Cookies e Privacidade" antes que a página seja renderizada. A função procura por qualquer botão rotulado como "Aceitar tudo" / "Rejeitar tudo" / equivalente em algumas línguas e clica nele.

Ao contrário do Google Maps, as URLs do Etsy /listing/<id>/ renderizam o painel completo mesmo na navegação direta, então nenhuma etapa de clique é necessária — o scrapper chama page.goto(listingUrl) diretamente. No entanto, o DataDome retorna HTTP 403 em uma fração significativa de IPs proxy novos para essa subárvore, então o wrapper de navegação verifica o status da resposta, falha rapidamente em 403/429 e lança um erro se o h1 nunca aparecer — cada uma dessas condições aciona o loop de nova tentativa externa para abrir uma nova sessão em um novo IP residencial.

ts Copy
const resp = await page.goto(hit.url, { waitUntil: "domcontentloaded", timeout: 60000 });
const status = resp?.status() ?? 0;
if (status === 403 || status === 429) {
  throw new Error(`bloqueado: HTTP ${status} em ${hit.url}`);
}
await dismissEtsyConsent(page);
try {
  await page.waitForSelector("h1", { timeout: 15000 });
} catch {
  // Sem h1 após 15 s quase sempre significa uma página de desafio ou redirecionamento do DataDome.
  // Lançar erro para que o loop de nova tentativa abra uma nova sessão (= novo IP residencial).
  throw new Error(`sem h1 em ${hit.url} — provavelmente página de desafio de bot`);
}
await delay(1500);

Então, acione o carregamento preguiçoso rolando a página inteira em partes. Descrição, materiais e a seção de envio são todos carregados ao rolar.

Etapa 6 — Extrair os Campos de Visão Geral

O extractor tem uma estrutura em duas fases que se repete em todos os extractors abaixo: lado do navegador (rolar + waitForFunction para hidratar regiões preguiçosas) → lado do Node (puxar o HTML da página uma vez via page.content() e então analisar com cheerio). Essa divisão nos dá o comportamento do DOM ao vivo quando precisamos e a análise server-side tipada e testável quando não precisamos.

ts Copy
async function extractOverview(page: Page): Promise<Partial<EtsyProduct>> {
  // Lado do navegador: rolar em partes para que descrição / materiais / envio
  // sejam carregados de forma preguiçosa, então aguardar que a caixa de compra
  // se hidrate além do placeholder "Carregando".
  await page.evaluate(`(function() {
    var step = 500, total = document.body.scrollHeight, current = 0;
    var iv = setInterval(function() {
      current += step;
      window.scrollTo(0, current);
      if (current >= total) clearInterval(iv);
    }, 200);
  })()`);
  await delay(3500);

  try {
    await page.waitForFunction(
      // Aguardar até que qualquer contêiner de preço plausível contenha um valor numérico
      // (não o placeholder "Carregando" que o Etsy exibe brevemente). Lançar uma rede
      // mais ampla do que apenas o contêiner da caixa de compra melhora a taxa de acerto em
      // listagens lentas onde o preço é renderizado primeiro em .currency-value.
      `(function(){
        var sels = [
          "[data-selector='price-only'] span.currency-value",
          "div[data-buy-box-region='price'] span.currency-value",
          "p[class*='price'] span.currency-value",
          "span.currency-value"
        ];
        for (var i = 0; i < sels.length; i++) {
          var el = document.querySelector(sels[i]);
          if (el && /^\\s*\\$?\\d/.test((el.textContent || '').trim())) return true;
        }
        return false;
      })()`,
      { timeout: 15000 },
    );
  } catch { /* o extractor ainda tenta o melhor abaixo */ }

  // Lado do Node: puxar o HTML renderizado uma vez e analisar com cheerio.
  const $ = await parseWithCheerio(page);

  const title = $("h1").first().text().trim() || null;

  // Preço — cascata em três etapas. (1) preferir o subárvore explícita `[data-selector='price-only']`
  // que o Etsy marca como o preço atual; (2) recorrer ao primeiro
  // `span.currency-value` cujos ancestrais NÃO são contêineres de preço riscado / original; 
  // (3) último recurso, regex no texto do corpo.
  const isOriginalPriceWrapper = (el: any) =>
    $(el).closest("[class*='strikethrough'], [class*='original'], s, .wt-text-strikethrough").length > 0;
  let price: string | null = null;
  const priceOnly = $("[data-selector='price-only'] span.currency-value").first();
  if (priceOnly.length && /^\d/.test(priceOnly.text().trim())) {
    price = priceOnly.text().trim();
  }
  if (!price) {
    $("span.currency-value").each((_, el) => {
      if (price) return false;
      if (isOriginalPriceWrapper(el)) return;
      const t = $(el).text().trim();
      if (t && /^\d/.test(t)) price = t;
    });
  }
  if (!price) {
    const bodyPriceMatch = $("body").text().match(/(?:Agora\s+)?Preço:?\s*([$£€]?[\d.,]+)/i);
    if (bodyPriceMatch) price = bodyPriceMatch[1].trim();
  }

  // Avaliação — qualquer aria-label mencionando "estrela", "avaliação" ou "de".
  let rating: number | null = null;
  $("[aria-label*='estrela' i], [aria-label*='avaliação' i]").each((_, n) => {
    if (rating !== null) return false;
    const a = $(n).attr("aria-label") || "";
    const rm = a.match(/(\d+(?:\.\d+)?)\s*(?:de|estrela|avaliação)/i);
    if (rm) rating = parseFloat(rm[1]);
  });
```pt
// Distintivos de status — regex o corpo da página.
const bodyText = $("body").text();
const isBestseller = /Bestseller/i.test(bodyText);
const isFreeShipping = /frete grátis/i.test(bodyText);
const isStarSeller = /Vendedor Estrela/i.test(bodyText);
const inStock = !/esgotado|vendido/i.test(bodyText);

// Lateral da loja.
const shopLink = $("a[href*='/shop/']").first();
const shopName = shopLink.text().trim() || null;
const shopUrl = (shopLink.attr("href") || "").split("?")[0] || null;

return { title, _price_raw: price, rating, isBestseller, isFreeShipping, isStarSeller, inStock,
  shop: { name: shopName, url: shopUrl } } as Partial<EtsyProduct>;
}

O prefixo `_price_raw` é uma convenção: o `enrichProduct` a seguir passa pelo `extractNumber` e depois exclui o campo de string bruta antes que o JSON final seja emitido. O trecho está abreviado — o `extractOverview` completo em `index.ts` também puxa `currency`, `discountPercent`, `reviewsCount`, `favoritesCount`, `description`, `materials`, `itemDetails`, `shippingFrom`, `processingTime`, `tags`, `listedDate` e o restante dos campos da loja. Mesmo padrão cheerio-primeiro ao longo de todo o código, apenas mais seletores.

Um pequeno helper `extractNumber` tolerante a moedas transforma `"$24.99"`, `"24,99 €"` ou `"1,234"` em um número limpo — o Etsy serve preços no formato local dependendo do país do proxy e você não quer que seus campos numéricos sejam strings.

## Passo 7 — Avaliações, Imagens, Lateral da Loja, Variações, Navegação, Pesquisas Relacionadas

**Avaliações.** Os cartões de avaliação do Etsy estão em `div[data-review-region]` (com `div[class*='review-card']` e `div[class*='review-item']` como alternativas para revisões do DOM). Role até a região das avaliações e, em seguida, mapeie cada cartão para autor / classificação / texto / data mais as três sub-classificações.

```ts
async function extractReviews(page: Page, max: number): Promise<EtsyReview[]> {
  // Lado do navegador: role até a região de avaliações para que os cartões de avaliação preguiçosos sejam renderizados.
  for (let i = 0; i < 8; i++) {
    const found = await page.evaluate(
      `!!document.querySelector('[data-reviews-section], div#reviews, div[class*="reviews"]')`,
    );
    if (found) break;
    await page.evaluate(() => window.scrollBy(0, 700));
    await delay(700);
  }
  await delay(1500);

  // Lado do Node: analise a página agora hidratada com cheerio.
  const $ = await parseWithCheerio(page);
  const out: EtsyReview[] = [];

  $(
    "div[data-review-region], div[class*='review-card'], div[class*='review-item'], li[class*='review']",
  ).each((_, card) => {
    if (out.length >= max) return false;
    const $card = $(card);
    const cardText = $card.text();
    // Pule contêineres agregados que contém muitas avaliações de uma vez.
    if (cardText.length > 6000) return;

    const author = $card.find("a[href*='/people/'], strong, p[class*='name']").first().text().trim() || null;

    // Avaliação: atributo `data-rating` primeiro, depois aria-label.
    let rating: number | null = null;
    const $starEl = $card.find("[aria-label*='estrela' i], [data-rating]").first();
    if ($starEl.length) {
      const dr = $starEl.attr("data-rating");
      if (dr) rating = parseFloat(dr);
      else {
        const rm = ($starEl.attr("aria-label") || "").match(/(\d+(?:\.\d+)?)/);
        if (rm) rating = parseFloat(rm[1]);
      }
    }

    // Texto: seletores dedicados, depois o parágrafo mais longo no cartão.
    let text: string | null =
      $card.find("p[class*='review-text'], div[class*='review-text'], div[id*='review-content']")
        .first().text().trim() || null;
    if (!text) {
      let longest = "";
      $card.find("p").each((_, p) => {
        const pt = $(p).text().trim();
        if (pt.length > longest.length && pt.length > 20) longest = pt;
      });
      text = longest || null;
    }

    // Data: elemento dedicado primeiro, depois uma expressão regular de nome de mês ou numérica.
    let date: string | null =
      $card.find("span[class*='date'], time, p[class*='date']").first().text().trim() || null;
    if (!date) {
      const dm = cardText.match(/(\w{3,9}\s+\d{1,2},?\s+\d{4}|\d{1,2}\/\d{1,2}\/\d{2,4})/);
      if (dm) date = dm[1];
    }

    // Sub-classificações — linhas rotuladas dentro do cartão. Combine rótulo, analise o número adjacente.
    const subRating = (label: string): number | null => {
      let val: number | null = null;
      $card.find("span, div, li").each((_, row) => {
        if (val !== null) return false;
        const t = $(row).text().toLowerCase();
        if (t.includes(label) && t.length < 60) {
          const sm = t.match(/(\d+(?:\.\d+)?)/);
          if (sm) val = parseFloat(sm[1]);
        }
      });
      return val;
    };

    // Fotos enviadas pelo revisor — mesmo upgrade `il_fullxfull` que as imagens de listagem.
    const photos: string[] = [];
    const photoSeen = new Set<string>();
    $card.find("img[src*='etsystatic'], img[data-src*='etsystatic']").each((_, img) => {
      if (photos.length >= 6) return false;
      let psrc = $(img).attr("src") || $(img).attr("data-src") || "";
      if (!psrc) return;
pt Copy
psrc = psrc.replace(/il_\d+xN/, "il_fullxfull").replace(/_\d+x\d+\./, "_1024x1024.");
if (!photoSeen.has(psrc)) { photoSeen.add(psrc); photos.push(psrc); }
});

out.push({
  author, rating, text, date,
  itemQuality: subRating("qualidade do item"),
  shipping: subRating("envio"),
  customerService: subRating("atendimento ao cliente"),
  photos,
});
});

return out;
}

As quatro alternativas de seletores de cartões cobrem as revisões A/B em andamento do Etsy — [data-review-region] é o seletor atual em alta; [class*='review-card'], [class*='review-item'] e li[class*='review'] são variantes mais antigas e mais novas que ainda aparecem dependendo da conta e da listagem. A proteção de comprimento cardText no topo ignora elementos wrapper que acidentalmente coincidem e retornariam um lote inteiro de avaliações concatenadas como uma só.

Imagens. O Etsy serve miniaturas por padrão. Atualize-as para resolução total substituindo o sufixo de tamanho na URL: il_75x75il_fullxfull, ou _300x300.jpg_1024x1024.jpg. Mesma imagem, resolução muito mais alta, sem solicitações extras.

ts Copy
async function extractImages(page: Page, max: number): Promise<string[]> {
  const $ = await parseWithCheerio(page);
  const urls: string[] = [];
  const seen = new Set<string>();
  $("img[src*='etsystatic'], img[data-src*='etsystatic']").each((_, img) => {
    if (urls.length >= max) return false;
    let src = $(img).attr("src") || $(img).attr("data-src") || "";
    if (!src) return;
    // Atualize o sufixo de tamanho da miniatura para resolução total onde possível.
    src = src.replace(/il_\d+xN/, "il_fullxfull").replace(/_\d+x\d+\./, "_1024x1024.");
    if (!seen.has(src)) { seen.add(src); urls.push(src); }
  });
  return urls;
}

O padrão img[data-src*='etsystatic'] no seletor é importante — o Etsy carrega miniaturas de galeria de forma preguiçosa por trás de data-src e não preenche src até que entrem na área de visualização.

Variações, trilhas de navegação, pesquisas relacionadas. Três extratores adicionais são executados após avaliações e imagens, cada um encapsulado em seu próprio try/catch, de modo que um seletor perdido se degrade a um array vazio, em vez de quebrar a linha:

  • extractVariations(page) — coleta as opções de tamanho / cor / personalização que o vendedor expõe, como uma lista {name, options[]}[]. Preenche a partir de elementos <select> dentro de árvores [data-selector*='variation'].
  • extractBreadcrumbs(page) — captura a trilha de categorias (por exemplo, ["Página inicial", "Bolsas e Carteiras", "Carteiras e Clips de Dinheiro", "Carteiras"]) de tags âncora que carregam ref=breadcrumb_listing em seu href. O Etsy não envolve esses em um <nav aria-label="breadcrumb"> — são links simples com um parâmetro ref.
  • extractRelatedSearches(page) — o link "Explorar buscas relacionadas" que o Etsy renderiza na parte inferior das páginas de listagem. O extrator rola novamente para o pé da página e aguarda a seção de tags carregadas de forma preguiçosa antes de ler o texto do link. O Etsy testa A/B chips apenas de imagem (sem texto) vs chips rotulados com texto, então espere que este campo se preencha em cerca de metade das listagens.

Data de listagem e totais em nível de loja são capturados dentro de extractOverview, juntamente com os campos básicos. listedDate analisa a string "Listada em Seg DD, AAAA" que o Etsy exibe perto dos detalhes do item — note que isso reflete a data de relistagem/renovação automática mais recente, não a data de criação original. shop.reviewsCountShop é preenchido apenas quando o Etsy desambiguiza explicitamente o número como nível de loja (muitos layouts de listagem não o renderizam — nulo é a resposta honesta nesse caso).

Fotos carregadas pelo revisor estão dentro de cada cartão de avaliação. extractReviews agora captura até 6 fotos por avaliação através da mesma atualização il_fullxfull usada para imagens de listagem, dando ao código a jusante um corpo de imagens paralelo para análise visual ou verificação de avaliação.

Passo 8 — Enriquecimento Resiliente por Produto & Tratamento de Erros

Raspar uma listagem é simples. Raspar cem em sequência é onde falhas transitórias começam a aparecer — o Etsy ocasionalmente serve um cache desatualizado, o h1 não é preenchido, uma única solicitação de proxy expira. Três camadas defensivas lidam com isso em grande escala:

Navegador novo por produto. Após as hits de busca serem coletadas, abre-se uma nova sessão do Navegador de Raspe Sem Estresse para cada enriquecimento. O estado não vaza entre os produtos e um erro em nível de sessão não contamina o restante da execução. Cada nova sessão roda um novo IP residencial, então quando o DataDome retorna um 403 em um IP, a próxima tentativa ocorre em um diferente.

Até cfg.maxRetries tentativas de reiteração (padrão 10) com um backoff crescente. Em uma execução limpa, a maioria dos produtos tem sucesso na tentativa 1; em uma execução com IP ruim, pode levar de 3 a 6 tentativas antes que a sessão chegue a um IP residencial limpo. Um alto orçamento de tentativas é a diferença entre uma taxa de acerto de 50% e 100%.
Taxonomia de erros categorizados. categorizeError(err) mapeia cada falha bruta (HTTP 403/404/429, ERR_SSL_*, ERR_TUNNEL_*, h1-faltando, tempo de navegação esgotado, handshake WSS) para um dos oito valores ScrapeErrorKind com uma flag retryable: boolean. Erros recuperáveis alimentam o loop de retentativas; os não recuperáveis (por exemplo, HTTP 404 em um anúncio desatualizado) interrompem imediatamente. Quando todas as tentativas se esgotam, o produto é enviado com error: { kind, message, attempts } populado para que o código subsequente possa saber exatamente por que uma linha retornou vazia.

ts Copy
// Dentro do loop principal, uma vez para cada resultado de pesquisa h (indexado por i):
const MAX_ATTEMPTS = cfg.maxRetries;   // padrão 10
let p: EtsyProduct | null = null;
let lastError: ScrapeErrorInfo | null = null;
let attemptsUsed = 0;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
  attemptsUsed = attempt;
  let eb;
  try {
    eb = await openBrowser(`etsy-enrich-${i}-${attempt}-${Date.now()}`, cfg);
  } catch (e: any) {
    lastError = categorizeError(new Error(`falha ao abrir o navegador: ${e?.message ?? e}`));
    log(`    tentativa ${attempt}/${MAX_ATTEMPTS} — ${lastError.kind}: ${lastError.message.slice(0, 120)}`);
    if (!lastError.retryable) break;
    if (attempt < MAX_ATTEMPTS) await delay(Math.max(cfg.retryInitialBackoffMs, 3000));
    continue;
  }
  try {
    p = await enrichProduct(eb, h, cfg);
    if (p.title) { lastError = null; break; }
    lastError = categorizeError(new Error(`sem h1 em ${h.url} — título era nulo`));
  } catch (e: any) {
    lastError = categorizeError(e);
    log(`    tentativa ${attempt}/${MAX_ATTEMPTS} — ${lastError.kind}: ${lastError.message.slice(0, 120)}`);
    if (!lastError.retryable) break;   // não recuperável: 404, etc. Falha rápida.
  } finally {
    await eb.close().catch(() => {});
  }
  if (attempt < MAX_ATTEMPTS) {
    // Retorno crescente configurável. Os padrões (3000, 1500, 500) resultam em
    // 5s, 8s, 12s, 17s, 23s, 30s, 38s, 47s, 57s entre tentativas.
    const backoff = cfg.retryInitialBackoffMs
      + attempt * (cfg.retryBackoffLinearMs + attempt * cfg.retryBackoffQuadraticMs);
    await delay(backoff);
  }
}
if (!p || !p.title) {
  p = emptyProduct(h.url);
  p.rank = h.rank;
  if (lastError) {
    p.error = { kind: lastError.kind, message: lastError.message.slice(0, 200), attempts: attemptsUsed };
  }
}
products.push(p);
// Pausa entre produtos (configurável) — quebra as navegações de anúncios consecutivas
// para que o padrão da sessão não pareça como um bot para o DataDome.
if (i < hits.length - 1) await delay(cfg.interProductDelayMs);

Alguns detalhes que importam em grande escala: o nome da sessão inclui o índice do produto i e Date.now() para que sessões novas não colidam entre produtos; openBrowser é envolto em sua própria estrutura de try/catch para que um handshake WSS com falha não pule a retentativa; eb.close() é ignorado com .catch(() => {}) porque a sessão já está morta no momento em que você a está fechando; o aumento do backoff ocorre devagar o suficiente para que produtos fáceis terminem rapidamente, mas os difíceis tenham a janela de dezenas de segundos que o DataDome impõe para IPs sinalizados; e a pausa entre produtos reduz mensuravelmente a chance de bloqueios correlacionados.

Os oito tipos de erros

Cada produto falhado carrega um objeto error: { kind, message, attempts }. O campo kind informa ao código subsequente como reagir sem precisar analisar a mensagem em formato livre:

kind Gatilho Recuperável
blocked HTTP 403 ou 429 — DataDome ou limite de taxa ✅ sim
not-found HTTP 404 — anúncio excluído ou que nunca existiu ❌ não (falha rápida)
tls ERR_SSL_* / ERR_CERT_* — problema transitório do proxy ✅ sim
network ERR_TUNNEL / ERR_CONNECTION_* / ERR_ABORTED ✅ sim
no-h1 Página carregada mas <h1> nunca apareceu — página de desafio suave ✅ sim
timeout Tempo de navegação excedeu pageTimeoutMs ✅ sim
open-browser Falha no handshake WSS para Scrapeless ✅ sim
unknown Qualquer outra coisa ✅ sim (padrão)

Cada parâmetro é ajustável

Todos os valores de retentativa e ritmo estão em ScraperInput — nada é codificado. Ajuste-os quando precisar de uma taxa de processamento previsível em um plano mais rigoroso ou retentativas mais agressivas em um alvo mais difícil:

campo CONFIG Padrão Função
maxRetries 10 Tentativas totais por produto antes de desistir
retryInitialBackoffMs 3000 Base da fórmula de retorno crescente
retryBackoffLinearMs 1500 Termo linear
retryBackoffQuadraticMs 500 Termo quadrático
interProductDelayMs 3000 Pausa entre enriquecimentos consecutivos de produtos
pageTimeoutMs 60000 Timeout de page.goto
h1TimeoutMs 15000 Timeout de waitForSelector("h1")
postLoadDelayMs 1500 Atraso após a aparição do h1, antes da extração

O que você recebe de volta

Um objeto JSON plano por produto. Largo por propósito, para que o mesmo scraper possa alimentar cada caso de uso subsequente sem uma segunda passagem.

Resultado real da primeira pesquisa de um "carteira de couro" executada neste exato modelo:

json Copy
{
  "listingId": "547491922",
json Copy
{
  "title": "Carteira de Couro•Carteira•Carteira Masculina de Couro•Carteira Minimalista•Carteira Personalizada•Aniversário de Couro•Carteira Slim de Couro•Carteira Masculina",
  "url": "https://www.etsy.com/listing/547491922/leather-walletwalletman-leather",
  "rank": 1,
  "price": 5.52,
  "originalPrice": 68.99,
  "currency": "R$",
  "discountPercent": 92,
  "inStock": false,
  "rating": 4.9,
  "reviewsCount": 929,
  "favoritesCount": 850,
  "isBestseller": false,
  "isFreeShipping": false,
  "isStarSeller": true,
  "tags": ["Presentes para Damas de Honra", "Presentes para Padrinhos", "Presentes de Casamento", "Presentes de Noivado"],
  "materials": [],
  "shop": {
    "name": "TexasValleyLeather",
    "url": "https://www.etsy.com/shop/TexasValleyLeather",
    "location": null,
    "totalSales": null,
    "openedYear": null,
    "reviewsCountShop": null
  },
  "images": [
    "https://i.etsystatic.com/15980284/r/il/2456a5/3164786673/il_fullxfull.3164786673_roeh.jpg",
    "... 4 mais URLs"
  ],
  "variations": [
    { "name": "Personalização", "options": ["Sim, adicionar gravação", "Não, obrigado"] },
    { "name": "Opção de Cor", "options": ["Castanho", "Preto", "Bege"] }
  ],
  "breadcrumbs": ["Página Inicial", "Bolsas & Carteiras", "Carteiras & Clips de Dinheiro", "Carteiras"],
  "relatedSearches": ["Carteiras para Homens de Couro", "Carteira Masculina Elegante", "Carteira Bifold Slim Personalizada de Couro"],
  "listedDate": "15 de Abr, 2026",
  "priceBucket": null,
  "reviews": [
    {
      "author": "Liz",
      "rating": 0,
      "text": "Conforme descrito e enviado rapidamente. Obrigada!",
      "date": "12 de Abr, 2026",
      "itemQuality": null,
      "shipping": null,
      "customerService": null,
      "photos": []
    },
    "... 9 mais avaliações"
  ],
  "error": null,
  "scrapedAt": "2026-04-16T17:09:48.919Z"
}

O mesmo assistente de conexão, a taxonomia de repetição e o padrão de sessão por alvo se estendem por todo o catálogo mais amplo do Scrapeless: combine este guia com o Scrapeless MCP Server para conectar dados do Etsy diretamente à superfície da ferramenta de um agente de IA, ou com o resumo dos melhores agentes de IA para obter contexto sobre como esse pipeline se encaixa em fluxos de automação mais amplos.

Defina proxyCountry para corresponder ao marketplace para o qual você deseja preços, mantenha sessionRecording: "true" para que qualquer linha nula possa ser reproduzida de ponta a ponta, trate campos ausentes (materials, shop.location, reviews[].itemQuality) como anuláveis em vez de erros de dados ausentes, e deixe o retrocesso crescente absorver os 403s transitórios. Este é o manual completo.


Pronto para Construir Seu Pipeline de Dados Alimentado por IA?

Junte-se à nossa comunidade para reivindicar um plano gratuito e se conectar com desenvolvedores que estão construindo pipelines de inteligência do Etsy: Discord · Telegram.

Inscreva-se em app.scrapeless.com para um runtime gratuito do Scraping Browser — até 100 horas de execução no navegador durante o teste gratuito — e adapte os padrões acima às categorias, lojas e palavras-chave do Etsy que seu pipeline precisa.


FAQ

Fazer scraping de dados publicamente disponíveis para monitoramento de preços e pesquisa é geralmente legal, desde que você respeite os Termos de Uso do Etsy e evite fazer scraping de dados pessoais do usuário. Usar o Scrapeless garante que sua atividade de scraping respeite os recursos do servidor por meio de um ritmo gerenciado.

Como o Scrapeless lida com a proteção DataDome do Etsy?

Diferente de proxies padrão, o Scrapeless gerencia toda a impressão digital do navegador e o handshake TLS. Isso torna seu scraper indistinguível de um usuário real, permitindo que você contorne a sofisticada detecção de bots do DataDome sem configuração manual de furtividade.

Q1: É necessário um proxy para fazer scraping no Etsy?

Sim. Sem um proxy residencial, o DataDome rapidamente sinaliza o tráfego de datacenters — a combinação de impressão digital e reputação de IP geralmente pontua para a cesta de rejeição, e as solicitações de navegação direta para páginas /listing/ retornam HTTP 403 com uma página de desafio em JavaScript. O Scraping Browser do Scrapeless vem com proxies residenciais integrados — cada sessão passa por um IP residencial diferente no país escolhido, verificado em testes por sessões frescas consecutivas retornando IPs de saída distintos (api.ipify.org probe).

Q2: Como vejo o que o scraper fez em uma execução passada?

Cada sessão neste modelo configura sessionRecording: "true" na URL WSS, então o Scrapeless salva uma reprodução completa no estilo de vídeo de cada página que o navegador em nuvem tocou — posição de rolagem, estado do DOM e atividade de rede. Encontre reproduções em app.scrapeless.comScraping BrowserSessions, e combine pelo valor sessionName que o scraper registra por tentativa (por exemplo, etsy-enrich-3-2-1713198231047).

Se o painel mostrar "Reprodução Indisponível — Por favor, ative 'Gravação Web' para visualizar as gravações da sessão", ative o controle Gravação Web na página de configurações da sua conta Scrapeless. É gratuito em todos os planos; está apenas desativado por padrão. Uma vez ativado, todas as futuras sessões gravam automaticamente — sessões passadas que foram realizadas enquanto a gravação estava desligada não podem ser recuperadas retroativamente.

As reproduções são a forma mais rápida de depurar por que uma linha retornou com title: null. Abra a sessão, desloque a linha do tempo até o momento em que page.goto foi acionado, e você verá se o servidor retornou um anúncio real, um desafio do DataDome ou um redirecionamento de URL desatualizado.

Q3: Por que as avaliações às vezes carregam através de endpoints internos em vez da página?

Listagens mais novas do Etsy carregam alguns lotes de avaliações através de solicitações POST internas após a página
categoryUrl: "https://www.etsy.com/c/bags-and-purses/wallets-and-money-clips/wallets",
filtros: {
emPromoção: true, // → &is_on_sale=1
freteGrátis: true, // → &free_shipping=1
personalizável: true, // → &is_personalizable=1
enviaPara: "US", // → &ships_to=US (código de país ISO)
preçoMin: 20, // → &min=20
preçoMax: 60, // → &max=60
condição: "vintage", // "novo" | "vintage" (→ &explicit=vintage)
ordenarPor: "preço_asc", // "mais_relevante" | "data_desc" | "preço_asc" | "preço_desc" | "maiores_avaliações"
},
// ...
};

Copy
Duas ressalvas que vale a pena saber: `filtros.preçoMin` / `filtros.preçoMax` em uma URL `/search?q=...` é sensível ao DataDome (URLs de busca filtradas recebem erro 403 mais agressivamente do que as não filtradas), então `estratégiaDeExpansão: "preços"` agora executa uma única busca ampla e categoriza os resultados no lado do cliente via `preçoCategoria` — mesma intenção do usuário, sem bloqueio de URL filtrada. Na `categoryUrl`, o filtro de preço funciona normalmente.

### Q6: Posso ajustar tentativas, tempos limite e intervalos?

Sim. Cada valor de tentativa e intervalo é um campo CONFIG em `ScraperInput`:

| Campo | Padrão | Função |
|---|---|---|
| `maxRetries` | `10` | Tentativas totais por produto antes de desistir |
| `retryInitialBackoffMs` | `3000` | Base da fórmula de aumento de espera |
| `retryBackoffLinearMs` | `1500` | Termo linear |
| `retryBackoffQuadraticMs` | `500` | Termo quadrático (resulta em progressão de 5 s → 57 s) |
| `interProductDelayMs` | `3000` | Pausa entre enriquecimentos consecutivos de produtos |
| `pageTimeoutMs` | `60000` | Timeout de `page.goto` |
| `h1TimeoutMs` | `15000` | Timeout de `waitForSelector("h1")` |
| `postLoadDelayMs` | `1500` | Atraso após a aparição do `h1`, antes da extração |

Planos mais rigorosos do Scrapeless se beneficiam de menor `interProductDelayMs` + menor `maxRetries`; alvos de anti-bot mais difíceis se beneficiam de valores mais altos em ambos.

### Q7: Que categorias de falha posso esperar?

Cada produto que esgota as tentativas carrega um campo estruturado `erro: { tipo, mensagem, tentativas }`. Oito tipos categorizados:

- `bloqueado` — HTTP 403/429 do DataDome ou limitação de taxa (repetível)
- `não-encontrado` — HTTP 404, listagem antiga ou excluída (não-repetível — falha rápida)
- `tls` — `ERR_SSL_*` / `ERR_CERT_*` problema de proxy TLS (repetível)
- `rede` — `ERR_TUNNEL` / `ERR_CONNECTION_*` / `ERR_ABORTED` (repetível)
- `sem-h1` — página carregada mas `<h1>` nunca apareceu, provavelmente uma página de desafio DD leve (repetível)
- `timeout` — timeout de navegação excedido (repetível)
- `abrir-navegador` — falha na troca de WSS com o Scrapeless (repetível)
- `desconhecido` — qualquer outra coisa (repetível por padrão)

O código de downstream pode tratar `tipo: "não-encontrado"` como "descartar esta URL, nunca reencadear" e `tipo: "bloqueado"` como "tente esta novamente na próxima hora quando a janela de reputação IP do DataDome for redefinida".

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