वापस ब्लॉग पर

कैसे Scrapeless Scraping ब्राउज़र के साथ एक Etsy स्क्रैपर बनाएं: व्यापक गाइड 2026 (Node.js)

James Thompson
James Thompson

Scraping and Proxy Management Expert

16-Apr-2026

मुख्य बातें:

  • Scrapeless Scraping Browser एक शक्तिशाली एआई ब्राउजर इन्फ्रास्ट्रक्चर के रूप में कार्य करता है, जो Etsy के DataDome एंटी-बॉट लेयर को स्वचालित फिंगरप्रिंटिंग, आवासीय प्रॉक्सी और CAPTCHA हल करने के साथ साफ करता है।
  • एक CONFIG ब्लॉक से चार डिस्कवरी मोड — उत्पाद URL, श्रेणी URL, कीवर्ड खोज (वैकल्पिक विस्तार के साथ) और दुकान URL। इनपुट्स का आदान-प्रदान करें, वही पाइपलाइन।
  • आठ संरचित फ़िल्टर (बिक्री पर, फ्री-शिपिंग, अनुकूलन योग्य, भेजने के लिए, न्यूनतम/अधिकतम कीमत, स्थिति, क्रम द्वारा) किसी भी डिस्कवरी मोड के साथ मिश्रित होते हैं जो खोज या श्रेणी URLs का उपयोग करते हैं।
  • आउटपुट स्कीमा प्रति उत्पाद 30+ फ़ील्ड को कवर करता है जिसमें variations, breadcrumbs, listedDate, reviews[].photos और अद्वितीय विपणन संकेत (isBestseller, isStarSeller, isFreeShipping, inStock, favoritesCount, प्रति-रिव्यू उप-स्कोर) शामिल हैं।
  • एक कॉन्फ़िगर करने योग्य पुनः प्रयास लूप (डिफ़ॉल्ट maxRetries: 10, बढ़ता बैकऑफ़ 3 सेकंड → 47 सेकंड) प्रयासों के बीच ताजा सत्र और आवासीय IP की ओर घूमता है, स्वचालित रूप से अस्थायी 403s को अवशोषित करता है।

परिचय: एंटी-डिटेक्शन क्लाउड ब्राउज़र के साथ स्क्रैपिंग Etsy

Etsy ई-कॉमर्स खुफिया के लिए एक खजाना है: दुकान मालिकों के लिए तुलनात्मक विक्रेता मूल्य, एमएल परियोजनाओं के लिए भावना-प्रशिक्षण कॉर्पस और ड्रॉपशिपर्स के लिए निचे खोज सभी उसी सूचीकरण पृष्ठों से प्रवाहित होते हैं। आधिकारिक Etsy API का सीमित पहुंच और लंबा स्वीकृति चक्र है, तीसरे पक्ष के डेटा पुनर्विक्रेता महंगे होते हैं और एक कस्टम स्क्रैपर को DataDome के खिलाफ चल रहे रखरखाव की आवश्यकता होती है और Etsy के फ्रंटएंड पर लगातार CSS वर्ग नामों में परिवर्तन होता है।

यह गाइड एक एकल टाइपस्क्रिप्ट फ़ाइल के माध्यम से चलती है जो Scrapeless Scraping Browser पर आधारित है जो सभी कठिन हिस्सों को पहले ही संभाल लेती है: एंटी-डिटेक्शन क्लाउड ब्राउज़र, आवासीय प्रॉक्सी, समीक्षाओं और दुकान मेटाडेटा के साथ प्रति-उत्पाद समृद्धि और वह बहु-पूंछ विस्तार तकनीक जो एक एकल बेस कीवर्ड से अपेक्षाकृत अधिक परिणामों का खुलासा करती है जितना कि Etsy के प्रति-खोज की छत सामान्यतः अनुमति देती है। वही स्क्रैपर चार स्वतंत्र डिस्कवरी मोड का समर्थन करता है — इसे उत्पाद URL, श्रेणी URL, कीवर्ड खोज या दुकान URL दें — और हर आउटपुट पंक्ति सूचीकरण को खोजने के तरीके की परवाह किए बिना वही समृद्ध 30-फील्ड स्कीमा ले आती है।


आप इसके साथ क्या कर सकते हैं

Etsy डेटा एक बहुपरकारी संपत्ति है, जो उत्पाद अनुसंधान से लेकर उन्नत एआई विश्लेषण तक उच्च-प्रभाव वाले व्यावसायिक अनुप्रयोगों को चलाता है। यहाँ पांच वास्तविक दुनिया के व्यावसायिक उपयोग हैं, सभी उसी कोडबेस से प्राप्य, अक्सर केवल कॉन्फ़िगरेशन में बदलाव के साथ:

  1. ड्रॉपशिपिंग अनुसंधान और उत्पाद खोजनाकीवर्ड-खोज मोड। स्क्रैपर को "macramé plant hanger" पर expandStrategy: "keywords" के साथ ["boho", "modern", "minimalist"] पर चलाएं, maxProducts: 200 सेट करें और आउटपुट को favoritesCount × rating के अनुसार क्रम दें। उन दुकानों में फ़िल्टर करें जहाँ isStarSeller: true है और favoritesCount औसत से काफी ऊपर है — ये आपके ड्रॉपशिपिंग उम्मीदवार हैं। परिणामी CSV को Shopify या एक निजी आपूर्तिकर्ता सूची में छोड़ दें। यही सबसे सामान्य कारण है कि लोग Etsy को स्क्रैप करते हैं और इसे राजस्व में बदलने का सबसे तेज़ तरीका है।
  2. प्रतिस्पर्धी मूल्य निगरानीउत्पाद-URL (प्रत्यक्ष-URL) मोड। प्रतिस्पर्धी सूची URL की एक सूची को startUrls में रखें और स्क्रैपर को रात में चलाएँ। प्रत्येक JSON स्नैपशॉट को इसकी scrapedAt टाइमस्टैम्प के साथ संग्रहीत करें और कीमत, originalPrice, discountPercent और चलन में स्टॉक के बीच अंतर करें। क्या मूल्य में 10% से अधिक गिरावट है? स्लैक अलार्म। inStock true से false में बदलता है? आपूर्ति संकेत के रूप में झंडा। आप इस तरह से जो पूर्ण मूल्य इतिहास बनाते हैं वह हर प्रतिस्पर्धा-खुफिया डैशबोर्ड का मुख्य आधार है।
  3. कीवर्ड और प्रवृत्ति अनुसंधानफ़िल्टर के साथ श्रेणी-URL मोड। categoryUrl को एक विशिष्ट Etsy श्रेणी की ओर इंगित करें (जैसे /c/bags-and-purses/wallets-and-money-clips/wallets), फ़िल्टर संयोजनों का उपयोग करें (filters.onSale: true, filters.condition: "new", filters.orderBy: "date_desc"), कुछ सौ लिस्टिंग में tags और materials को खींचें, उनकी आवृत्ति की गणना करें और प्रत्येक टैग का उपयोग करने वाली लिस्टिंग पर favoritesCount के योग के अनुसार क्रमबद्ध करें। हाल ही में बनाई गई सूचियों में दिखाई देने वाले टैग लेकिन पुराने में नहीं आपके बढ़ते उप-निचे हैं।
  4. एमएल और बाजार अनुसंधान के लिए समीक्षा संग्रहणकीवर्ड या श्रेणी मोड। एक वर्टिकल (हैंडमेड मोमबत्तियाँ, कहें, या व्यक्तिगत आभूषण) में हजारों लिस्टिंग में reviews[] को स्क्रैप करें, reviews[].text को एक भावना वर्गीकरणकर्ता में फीड करें और उनके उपस्थित होने पर सुपरवाइज्ड ट्रेनिंग लेबल के रूप में itemQuality / shipping / customerService उप-रेटिंग का उपयोग करें। प्रति-रिव्यू फ़ोटो (reviews[].photos[]) आपको एक समान छवि कॉर्पस प्रदान करते हैं यदि आपको दृश्य प्रशिक्षण डेटा की आवश्यकता है।
  5. दुकान प्रदर्शन बेंचमार्किंगदुकान-यूआरएल मोड। बिंदु shopUrl को प्रतिस्पर्धी की दुकान पृष्ठ पर सेट करें (जैसे https://www.etsy.com/shop/TexasValleyLeather), maxPagesPerQuery: 5 सेट करें ताकि उनके पूरे कैटलॉग को पृष्ठांकित किया जा सके और स्क्रैपर उन सभी सूचीकरणों को संख्याबद्ध करे जो वह दुकान वर्तमान में बेचती है। समान श्रेणी में विक्रेताओं की तुलना करें shop.totalSales, shop.openedYear, rating, reviewsCount और isStarSeller द्वारा।

क्यों स्क्रैपलेस

स्क्रैपलेस स्क्रैपिंग ब्राउज़र आपके स्क्रैपर को एक उत्पादन-ग्रेड क्लाउड ब्राउज़र देता है जो ईटीसी के डेटा डोम चेक को बिना किसी कोडिंग के साफ करता है — कोई स्टेल्थ प्लगइन्स, कोई फिंगरप्रिंट ट्यूनिंग, कोई प्रॉक्सी रोटेशन स्क्रिप्ट्स नहीं जो बनाए रखने की आवश्यकता है। Puppeteer या Playwright का उपयोग करके WebSocket एंडपॉइंट के माध्यम से कनेक्ट करें और अवसंरचना एंटी-बॉट लेयर को संभालने दे।

बॉक्स से बाहर आपको मिलता है:

  • एंटी-डिटेक्शन फिंगरप्रिंटिंग जो लंबे समय तक चलने वाले सत्रों में टिकाऊ रहती है
  • स्थायी प्रॉक्सी
    195+ देशों में (अलग से US, GB, DE मूल्य निर्धारण)
  • स्वचालित CAPTCHA समाधान जब ईटीसी एक प्रदान करता है
  • सत्र रिकॉर्डिंग पिछले मामले में चयनकर्ता गिरावट की समस्या निवारण के लिए
  • WebSocket एंडपॉइंट्स जो Puppeteer और Playwright जैसे CDP-आधारित ढांचों का समर्थन करते हैं — कोई SDK सीखने की आवश्यकता नहीं
  • एआई एजेंट के लिए तैयार: स्क्रैपलेस MCP सर्वर जैसे टूल के साथ समस्या निवारण और कनेक्शन को सुचारू रूप से एकीकृत करता है।

संयोग एक-लाइन का परिवर्तन है: puppeteer.connect() को एक स्क्रैपलेस यूआरएल पर इंगीत करें न कि स्थानीय ब्राउज़र पर। बाकी कोड बिल्कुल वही रहता है — मानक CDP, मानक चयनकर्ता, मानक कार्यप्रवाह। सभी डेटा डोम जटिलता सर्वर साइड पर, आपके कोडबेस से बाहर रहती है।

app.scrapeless.com पर निःशुल्क योजना में अपना API कुंजी प्राप्त करें।


पूर्वापेक्षाएँ और स्थापना

Node.js 18 या नया। एक स्क्रैपलेस API कुंजी (निःशुल्क स्तर इस गाइड में सब कुछ कवर करता है)। कुछ Puppeteer की परिचितता सहायक होती है। स्थानीय Chrome की आवश्यकता नहीं है — ब्राउज़र स्क्रैपलेस के क्लाउड में चलता है।

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 क्लाउड ब्राउज़र को चालित करता है; cheerio प्रदर्शित HTML को प्रत्येक पृष्ठ के लोड होने के बाद सर्वर-साइड पर पार्स करता है। ब्राउज़र-साइड स्क्रोलिंग और Node-साइड पार्सिंग को अलग करना हर एक्सट्रैक्टर को टाइप और सहेजे गए HTML फिक्स्चर के खिलाफ यूनिट-टेस्ट करने योग्य बनाए रखता है।

.env:

Copy
SCRAPELESS_API_KEY=your_key_here

कदम 1 — स्क्रैपिंग ब्राउज़र से कनेक्ट करें

पूरे स्क्रैपर के लिए एक कनेक्शन सहायक। टोकन, देश और TTL के साथ एक WSS यूआरएल बनाएं, फिर इसे puppeteer.connect को सौंपें।

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

// सहायक — पृष्ठ का पूरा HTML खींचें और इसे cheerio के साथ पार्स करें। कॉलर
// किसी भी ब्राउज़र-साइड स्क्रोलिंग / waitForFunction को पहले चलाने के लिए जिम्मेदार है
// ताकि आलसी क्षेत्र हाइड्रेटेड हो सकें। इसके बाद, पार्सिंग Node में रहती है:
// टाइप किया हुआ, कोई स्ट्रिंगिफाइड मूल्यांकन बॉडी नहीं, कोई `__name` tsx गड्ढा नहीं, बचत HTML फिक्स्चर के खिलाफ
// यूनिट-टेस्ट करना आसान है।
async function parseWithCheerio(page: Page): Promise<cheerio.CheerioAPI> {
  const html = await page.content();
  return cheerio.load(html);
}

type ScraperInput = {
  proxyCountry: string;   // जैसे "US", "GB", "DE"
  sessionTTL: number;     // सेकंड, 60–900 की अनुमति; 600 एक सुरक्षित डिफ़ॉल्ट है
};

function connectionURL(sessionName: string, cfg: ScraperInput): string {
  const token = process.env.SCRAPELESS_API_KEY;
  if (!token) throw new Error("SCRAPELESS_API_KEY is not set in .env");
  // स्क्रैपलेस डिफॉल्ट द्वारा सत्र के जीवनकाल के लिए स्थायी आईपी पिन करता है,
  // इसलिए एक puppeteer.connect के भीतर प्रत्येक पृष्ठ नेविगेशन उसी 
  // आउटबाउंड आईपी का उपयोग करता है। एक नए सत्र (नया कनेक्शन) को खोलने से एक नया आईपी मिलता है,
  // जिस पर पुनः प्रयास चक्र को एक फ्लैग किए गए आईपी के चारों ओर रूट करने के लिए भरोसा होता है।
  const qs = new URLSearchParams({
    token,
    proxyCountry: cfg.proxyCountry,
    sessionTTL: String(cfg.sessionTTL),
    sessionName,
    sessionRecording: "true",
    // स्क्रैपलेस को पूर्ण डेस्कटॉप फिंगरप्रिंट का स्वामित्व दें — UA, स्क्रीन, टाइमज़ोन
    // और भाषा। कोई मैनुअल 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,
  });
}

यह पूरी Scrapeless-विशिष्ट सतह क्षेत्र है - एक WSS URL और एक puppeteer.connect। इसे स्केल करने से पहले जानने के लिए एक बात यह है: एक puppeteer.connect सत्र अपने जीवनकाल के लिए एक ही निवासी IP से बंधा होता है (एक ही ब्राउज़र हैंडल पर api.ipify.org को लगातार तीन बार हिट करके सत्यापित किया गया - हर बार वही IP)। एक ताजा सत्र खोलने से एक नया IP मिलता है। यह वही आधार है जिस पर चरण 8 में_retry लूप बनता है - यदि इस सत्र के IP पर एक अनुरोध अवरुद्ध हो जाता है, तो हम सत्र को बंद करते हैं, एक नया खोलते हैं, एक नया IP प्राप्त करते हैं और फिर से प्रयास करते हैं।

Scrapeless Scraping Browser कनेक्शन परत पर ब्राउज़र फिंगरप्रिंट का अधिकार रखता है - UA, स्क्रीन आकार, टाइमज़ोन और भाषा सभी fingerprint: { platform: "Windows" } WSS URL पर क्वेरी पैरामीटर द्वारा संभाला जाता है। मैनुअल setViewport या setUserAgent कॉल्स की आवश्यकता नहीं है। चरण 8 में_retry लूप ऊपर किस्मत ब्लॉक्स को अवशोषित करता है।

ब्राउज़र-पक्ष की सेटअप एक-लाइन tsx संगतता स्टब है:

ts Copy
async function prepPage(page: Page): Promise<void> {
  // पृष्ठ.evaluate फ़ंक्शन निकायों के साथ "__name is not defined" के अंदर क्रैश न हों इसके लिए tsx-इनजेक्टेड __name हेल्पर को स्टब करें।
  await page.evaluateOnNewDocument(
    "(function(){ globalThis.__name = function(f){ return f; }; })()",
  );
}

सत्र वार्म-अप

खोज या दुकान पृष्ठ पर जाने से पहले, स्क्रैपर पहले एक बार Etsy की होमपेज को लोड करता है ताकि एक वैध ब्राउज़र सत्र स्थापित किया जा सके। इस कदम के बिना, /search और /shop एंडपॉइंट्स एक ठंडे सत्र पर 403 लौटाते हैं:

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 {
    // टाइमआउट या नेटवर्क त्रुटि ठीक है — तब तक कुकीज़ सेट की जाती हैं।
  }
  await dismissEtsyConsent(page);
  await delay(1500);
}

देश-विशिष्ट पथ महत्वपूर्ण है: एक DE प्रॉक्सी etsy.com/de/ को हिट करने पर 200 लौटाता है और सही क्षेत्रीय सत्र कुकीज़ सेट करता है, जबकि DE प्रॉक्सी के साथ etsy.com/ 403 लौटाता है और सत्र बंद रहता है। US (64 लिस्टिंग), DE (60 लिस्टिंग) और GB (61 लिस्टिंग) में सत्यापित — तीनों पहले प्रयास में खोज परिणाम लौटाते हैं जब वार्मअप प्रॉक्सी देश से मेल खाता है। स्क्रैपर पहले collectSearchResults कॉल से पहले प्रत्येक ब्राउज़र सत्र में एक बार warmUpSession को कॉल करता है।


चरण 2 — चार खोज मोड

स्क्रैपर लिस्टिंग खोजने के लिए चार स्वतंत्र तरीके स्वीकार करता है, ये सभी एक ही CONFIG ब्लॉक में होते हैं। उस एक को चुनें जो अपस्ट्रीम प्रश्न से मेल खाता है और startUrls, shopUrl, categoryUrl, या searchQuery में से एक ही सेट करें। यदि एक से अधिक सेट हैं, तो प्राथमिकता है shopUrlcategoryUrlsearchQuerystartUrls

उत्पाद URL मोड (प्रत्यक्ष-URL) — ज्ञात लिस्टिंग, रात की दोबारा-स्क्रैपिंग, प्रतिस्पर्धी स्नैपशॉट:

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,
  // ...अन्य डिफ़ॉल्ट
};

श्रेणी URL मोड — संरचित फ़िल्टर के साथ संपूर्ण श्रेणी क्रॉल:

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,
};

कीवर्ड खोज मोड — विशेष खोज, प्रवृत्ति अनुसंधान, मात्रा लिस्टिंग पुल:

ts Copy
const CONFIG: ScraperInput = {
  searchQuery: "leather wallet",
  expandStrategy: "keywords",                   // "none" | "keywords" | "prices"
  expandKeywords: ["mens", "womens", "vintage"], // जब विस्तार = कीवर्ड हो तब बेस में जोड़ा जाता है
  maxProducts: 20,
};

दुकान URL मोड — बेंचमार्किंग / प्रतिस्पर्धी विश्लेषण के लिए एक विशिष्ट दुकान में प्रत्येक लिस्टिंग की गणना करें:

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

चारों मोड चरण 4–6 में एक ही प्रति-सूची संपत्ति पाइपलाइन को खिलाते हैं और चरण 8 में एक समान 30-क्षेत्र स्कीमा जारी करते हैं।

संरचित फ़िल्टर

आठ वैकल्पिक फ़िल्टर कुंजी searchQuery या categoryUrl के साथ संयोजित होती हैं। जिनमें से भी लागू हों, उन्हें सेट करें, बाकी को छोड़ दें:

कुंजी मान प्रभाव
onSale true केवल वर्तमान में बिक्री पर सूचीबद्ध वस्तुएं
freeShipping true केवल प्रॉक्सी देश में मुफ्त शिपिंग करने वाली सूची
customizable true केवल व्यक्तिगत रूप से अनुकूलन योग्य लिस्टिंग
shipsTo ISO कोड जैसे "US" उस देश में भेजना आवश्यक है
minPrice / maxPrice संख्या मूल्य सीमा (Etsy का स्वदेशी फ़िल्टर)
condition "new" | "vintage" Etsy स्थिति फ़िल्टर
orderBy "सबसे प्रासंगिक" | "तारीख_desc" | "कीमत_कम" | "कीमत_ज्यादा" | "उच्चतम समीक्षाएँ" परिणाम क्रमबद्धता

पेजिनेशन नियंत्रण

maxPagesPerQuery: N सेट करें ताकि प्रत्येक डिस्कवरी URL पर ?page=1..N को स्पष्ट रूप से कार्यान्वित किया जा सके। इसके बिना, स्क्रैपर लक्षित प्रारंभिक पृष्ठ को स्क्रॉल करता है और जब maxProducts अद्वितीय लिस्टिंग एकत्र हो जाती हैं, तो रुक जाता है। जब आप पूर्वानुमानित बड़े स्क्रैप करना चाहते हैं (जैसे "इस श्रेणी के पहले 5 पृष्ठों को स्क्रैप करें, भले ही वह 200+ लिस्टिंग हो"), तब स्पष्ट पेजिनेशन का उपयोग करें।


चरण 3 — मल्टी-क्वेरी विस्तार (Etsy "परिणाम सीमा" समस्या का हल)

Etsy का उपभोक्ता UI पेजिनेशन को अधिकांश niches खत्म होने से पहले ही सीमित करता है, और प्रति-IP दर सीमा उच्च अनुरोध मात्रा के तहत जल्दी लागू होती है — कोई भी एकल कीवर्ड केवल एक रैंकिंग स्लाइस को ही सतह पर लाता है। एक निच को समाप्त करने के लिए, आधार क्वेरी को एक धुरी (कीवर्ड या कीमत बकेट) के साथ विभाजित करें और परिणामों को listingId द्वारा डेडुप करें।

"लेदर वॉलेट" के लिए, एक कीवर्ड विस्तार इस प्रकार दिखता है:

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 = "keywords" | "prices" | "none";

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

["पुरुषों", "महिलाओं", "विंटेज"] के खिलाफ "लेदर वॉलेट" चार खोजें उत्पन्न करता है। उन्हें चलाएँ, लिस्टिंग URLs एकत्र करें, URL में निहित संख्या में ID द्वारा डेडुप करें (/listing/1051861316/...)। maxProducts को पर्याप्त ऊँचा सेट करें (अधिकांशतः कुछ दर्जन से लेकर कुछ सौ तक) ताकि वास्तव में सभी वेरिएंट्स में फैल सके — यदि लक्ष्य छोटा है तो स्क्रैपर पहले क्वेरी के बाद जो परिणाम है, उसके बाद सीधे रुक जाएगा, डेडुप बनाने के काम को पूरी तरह से छोड़ देगा।

कीमत बकेटिंग उसी तरह काम करती है — विभिन्न बकेट विभिन्न रैंकिंग स्लाइस को सतह पर लाते हैं क्योंकि Etsy का "सर्वश्रेष्ठ मेल" कीमत से प्रभावित होता है जो परिणाम सेट में अन्य के सापेक्ष है।


चरण 4 — प्रत्येक खोज से लिस्टिंग URLs एकत्र करें

परिणाम साइडबार को इतना स्क्रॉल करें कि लेज़ी-लोडेड कार्ड्स को ट्रिगर किया जा सके, फिर हर a[href*="/listing/"] लिंक को div.listing-link के अंदर पकड़ें (जब Etsy A/B-टेस्ट क्लास नाम करता है तो [data-listing-id] के रूप में बैकअप पर)।

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);

  // Lazy-loaded कार्ड्स को ट्रिगर करने के लिए कुछ बार स्क्रॉल करें। प्रत्येक पास
  // वर्तमान DOM का एक cheerio स्नैपशॉट लेता है — जैसे ही हमारे पास पर्याप्त हो जाता है,
  // हम स्क्रॉल करना बंद कर देते हैं।
  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);
  }

  // Cheeriod के साथ स्थिर DOM का विश्लेषण करें — कोई `page.evaluate` राउंड-ट्रिप नहीं,
  // कोई स्ट्रिंगिफाइड फ़ंक्शन बॉडीज़ नहीं, बस सीधे सेलेक्टर ट्रैवर्सल।
  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;
}

स्क्रॉलिंग page.evaluate पर रहती है क्योंकि यह एक सक्रिय DOM क्रिया है (Etsy की लेज़ी-लोड को ट्रिगर करना), लेकिन हर टुकड़ा पार्सिंग Cheeriod पर page.content() स्नैपशॉट के माध्यम से चलता है। यही पैटर्न सभी छह समृद्धि निष्कर्षणकर्ताओं द्वारा चरण 6-7 में उपयोग किया जाता है।
dismissEtsyConsent कॉल उन गैर-अमेरिकी सत्रों के लिए है जहाँ Etsy एक "कुकीज़ और गोपनीयता" गेट दिखाता है इससे पहले कि पृष्ठ प्रस्तुत हो। यह फ़ंक्शन किसी भी बटन की तलाश करता है जिस पर "सभी स्वीकार करें" / "सभी अस्वीकार करें" / कुछ भाषाओं में समकक्ष लिखा होता है और उसे क्लिक करता है।

चरण 5 — प्रत्येक लिस्टिंग पर नेविगेट करें

Google Maps के विपरीत, Etsy के /listing/<id>/ यूआरएल सीधे नेविगेशन पर भी पूरा पैनल प्रदर्शित करते हैं, इसलिए किसी क्लिक-थ्रू चरण की आवश्यकता नहीं है — स्क्रैपर सीधे page.goto(listingUrl) को कॉल करता है। हालांकि, DataDome इस उपपर्ण पर ताज़ा प्रॉक्सी आईपी के महत्वपूर्ण हिस्से पर HTTP 403 लौटाता है, इसलिए नेविगेशन चेहरा प्रतिक्रिया की स्थिति की जांच करता है, 403/429 पर तेजी से विफल होता है और फेंकता है यदि h1 कभी नहीं दिखाई देता है — इनमें से प्रत्येक स्थिति बाहरी पुनः प्रयास लूप को नया आवासीय आईपी पर ताजा सत्र खोलने के लिए ट्रिगर करती है।

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(`blocked: HTTP ${status} on ${hit.url}`);
}
await dismissEtsyConsent(page);
try {
  await page.waitForSelector("h1", { timeout: 15000 });
} catch {
  // 15 सेकंड के बाद कोई h1 लगभग हमेशा DataDome चुनौती या पुनःनिर्देशन पृष्ठ का अर्थ है।
  // फेंकें ताकि पुनः प्रयास लूप एक नए सत्र को खोले (= नया आवासीय आईपी)।
  throw new Error(`no h1 on ${hit.url} — likely bot-challenge page`);
}
await delay(1500);

फिर पूरे पृष्ठ को भागों में स्क्रॉल करके लेजी लोडिंग को ट्रिगर करें। विवरण, सामग्री और शिपिंग अनुभाग सभी स्क्रॉल करने पर लोड होते हैं।

चरण 6 — अवलोकन क्षेत्रों को निकालें

Extractor की एक दो-चरणीय संरचना होती है जो नीचे के प्रत्येक extractor में दोहराई जाती है: ब्राउजर-साइड (स्क्रॉल + waitForFunction ताकि लेजी क्षेत्रों को हाइड्रेट किया जा सके) → नोड-साइड (एक बार page.content() के माध्यम से पृष्ठ का HTML खींचें और फिर cheerio के साथ पार्स करें)। यह विभाजन हमें जब जरूरत हो तो लाइव-DOM व्यवहार और जब न हो तो टाइप की गई, परीक्षण योग्य सर्वर-साइड पार्सिंग प्रदान करता है।

ts Copy
async function extractOverview(page: Page): Promise<Partial<EtsyProduct>> {
  // ब्राउजर-साइड: भागों में स्क्रॉल करें ताकि विवरण / सामग्री / शिपिंग
  // लेजी-लोड हो जाए, फिर खरीद-बॉक्स के "लोड हो रहा है" 
  // प्लेसहोल्डर के आगे हाइड्रेट होने की प्रतीक्षा करें।
  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(
      // तब तक प्रतीक्षा करें जब तक कोई भी संभावित मूल्यwrapper संख्या का मान न हो
      // (न कि "लोड हो रहा है" प्लेसहोल्डर जिसे Etsy थोड़ी देर के लिए दिखाता है)। 
      // खरीद-बॉक्स wrapper पर केवल कैसिंग से व्यापक जाल डालने से धीमे 
      // लिस्टिंग पर हिट दर में सुधार होता है जहाँ मूल्य सबसे पहले .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 { /* extractor इसके नीचे अपना सर्वश्रेष्ठ प्रयास करता है */ }

  // नोड-साइड: एक बार प्रस्तुत HTML खींचें और cheerio के साथ पार्स करें।
  const $ = await parseWithCheerio(page);

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

  // मूल्य — तीन-चरणीय श्रृंखला। (1) स्पष्ट `[data-selector='price-only']`
  // उपपर्ण को प्राथमिकता दें जिसे Etsy वर्तमान मूल्य के रूप में चिह्नित करता है; (2) पहले
  // `span.currency-value` पर वापस गिरें जिसका पूर्वज स्ट्राइकथ्रू / मूल-मूल्य
  // wrappers नहीं है; (3) अंतिम उपाय शरीर-टेक्स्ट regex।
  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(/(?:अब\s+)?कीमत:?\s*([$£€]?[\d.,]+)/i);
    if (bodyPriceMatch) price = bodyPriceMatch[1].trim();
  }

  // रेटिंग — कोई भी aria-label जो "सितारा", "रेटिंग", या "कितना" का उल्लेख करता है।
  let rating: number | null = null;
  $("[aria-label*='star' i], [aria-label*='rating' i]").each((_, n) => {
    if (rating !== null) return false;
    const a = $(n).attr("aria-label") || "";
    const rm = a.match(/(\d+(?:\.\d+)?)\s*(?:बाहर|सितारा|रेटिंग)/i);
    if (rm) rating = parseFloat(rm[1]);
  });

Here is the translation of the provided English text into Hindi:

Copy
// स्थिति बैज — पृष्ठ शरीर को regex करें।
const bodyText = $("body").text();
const isBestseller = /Bestseller/i.test(bodyText);
const isFreeShipping = /free shipping/i.test(bodyText);
const isStarSeller = /Star Seller/i.test(bodyText);
const inStock = !/out of stock|sold out/i.test(bodyText);

// दुकान साइडकार।
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>;
}

_price_raw उपसर्ग एक पारंपरिक है: नीचे की enrichProduct इसे extractNumber के माध्यम से भेजता है और फिर अंतिम JSON जारी होने से पहले कच्चे-स्ट्रिंग फ़ील्ड को मिटा देता है। स्निपेट संक्षिप्त है — पूर्ण extractOverview को index.ts में currency, discountPercent, reviewsCount, favoritesCount, description, materials, itemDetails, shippingFrom, processingTime, tags, listedDate और बाकी दुकान फ़ील्ड को भी खींचने के लिए विस्तारित किया गया है। पूरे पृष्ठ में वही cheerio-प्रथम पैटर्न है, केवल अधिक चयनकर्ता हैं।

एक छोटा मुद्रा-टॉलरेंट extractNumber सहायक "$24.99", "24,99 €", या "1,234" को एक साफ नंबर में बदलता है — Etsy मूल्य स्थानीय प्रारूप में प्रदान करता है जो प्रोक्सी देश पर निर्भर करता है और आप नहीं चाहते कि आपके संख्यात्मक क्षेत्र स्ट्रिंग्स हों।

चरण 7 — समीक्षाएं, छवियां, दुकान साइडकार, विविधताएं, ब्रेडक्रंब, संबंधित खोजें

समीक्षाएं। Etsy के समीक्षा कार्ड div[data-review-region] में रहते हैं (DOM संशोधनों के लिए div[class*='review-card'] और div[class*='review-item'] बैकअप के रूप में)। समीक्षाओं के क्षेत्र में स्क्रॉल करें, फिर प्रत्येक कार्ड को लेखक / रेटिंग / पाठ / तिथि plus तीन सब-रेटिंग में मैप करें।

ts Copy
async function extractReviews(page: Page, max: number): Promise<EtsyReview[]> {
  // ब्राउज़र-पक्ष: समीक्षाओं के क्षेत्र में स्क्रॉल करें ताकि सुस्त समीक्षा कार्ड रेंडर हों।
  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);

  // नोड-पक्ष: अब हाइड्रेटेड पृष्ठ को 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();
    // कई समीक्षाओं को एक साथ रखने वाले समग्र कंटेनरों को छोड़ें।
    if (cardText.length > 6000) return;

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

    // रेटिंग: पहले `data-rating` विशेषता, फिर aria-label।
    let rating: number | null = null;
    const $starEl = $card.find("[aria-label*='star' 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]);
      }
    }

    // पाठ: समर्पित चयनकर्ता, फिर कार्ड में सबसे लंबा पैराग्राफ।
    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;
    }

    // तारीख: पहले समर्पित तत्व, फिर एक महीने का नाम या संख्यात्मक regex।
    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];
    }

    // सब-रेटिंग - कार्ड के अंदर लेबल किए गए पंक्तियाँ। लेबल मेल खाएं, बगल में संख्या पार्स करें।
    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;
    };

    // समीक्षक-द्वारा अपलोड की गई तस्वीरें — लिस्टिंग छवियों के समान `il_fullxfull` उन्नयन।
    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;

This translation keeps the technical context intact while converting the content into Hindi.

hi 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("आइटम गुणवत्ता"),
  shipping: subRating("शिपिंग"),
  customerService: subRating("ग्राहक सेवा"),
  photos,
});
});

return out;
}

चार कार्ड-चयनकर्ता विकल्प Etsy के चल रहे A/B संशोधनों को कवर करते हैं - [data-review-region] वर्तमान हॉट चयनकर्ता है; [class*='review-card'], [class*='review-item'] और li[class*='review'] पुराने और नए संस्करण हैं जो खाते और लिस्टिंग के अनुसार अभी भी दिखाई देते हैं। शीर्ष पर कार्डटेक्स्ट लंबाई गार्ड उस अनुप्रासक तत्वों को छोड़ देता है जो गलती से मेल खाते हैं और एक साथ कई समीक्षाओं को वापस करते हैं।

चित्र। Etsy स्वचालित रूप से थंबनेल प्रदान करता है। उन्हें पूर्ण-रिज़ॉल्यूशन में अपग्रेड करने के लिए URL में आकार प्रत्यय को बदलें: il_75x75il_fullxfull, या _300x300.jpg_1024x1024.jpg। वही चित्र, लेकिन उच्च रिज़ॉल्यूशन, कोई अतिरिक्त अनुरोध नहीं।

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;
    // थंबनेल आकार प्रत्यय को संभव हो तो पूर्ण रिज़ॉल्यूशन में अपग्रेड करें।
    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;
}

img[data-src*='etsystatic'] पैटर्न चयनकर्ता में महत्वपूर्ण है - Etsy लाज़ी-लोड करता है गैलरी थंबनेल को data-src के पीछे और यह src को तब तक भरता नहीं है जब तक कि वे दृश्य क्षेत्र में नहीं आते।

भिन्नताएं, ब्रेडक्रंब, संबंधित खोजें। तीन अतिरिक्त एक्सट्रैक्टर्स समीक्षाओं और छवियों के बाद चलते हैं, प्रत्येक को अपने स्वयं के try/catch में लपेटा जाता है ताकि एक छूटे हुए चयनकर्ता एक खाली सूची में घटित हो जाए बजाय कि पंक्ति को तोड़ने के:

  • extractVariations(page) — आकार / रंग / वैयक्तिकरण विकल्पों को खींचता है जो विक्रेता उजागर करता है, के रूप में {name, options[]}[] सूची। [data-selector*='variation'] उप-ट्री में <select> तत्वों से भरता है।
  • extractBreadcrumbs(page) — श्रेणी मार्ग को पकड़ता है (जैसे ["Homepage", "Bags & Purses", "Wallets & Money Clips", "Wallets"]) एंकर टैग से जो ref=breadcrumb_listing लाते हैं उनके href में। Etsy इनकी लिपटने में <nav aria-label="breadcrumb"> नहीं लगाती - ये सामान्य लिंक हैं जिनमें एक संदर्भ पैरामीटर होता है।
  • extractRelatedSearches(page) — "संबंधित खोजों का अन्वेषण करें" लिंक जो Etsy लिस्टिंग पृष्ठों के नीचे प्रदर्शित करती है। एक्सट्रैक्टर पृष्ठ के पैर पर वापस स्क्रॉल करता है और लेज़ी-लोडेड टैग अनुभाग के लिए प्रतीक्षा करता है, लिंक पाठ को पढ़ने से पहले। Etsy A/B-टेस्ट छवि-केवल चिप्स (कोई पाठ नहीं) बनाम पाठ-लेबल वाले चिप्स, इसलिए इस फ़ील्ड को लगभग आधे लिस्टिंग पर भरने का अनुमान करें।

लिस्ट की गई तिथि और दुकान स्तर कुल extractOverview के भीतर खींची जाती हैं बुनियादी क्षेत्रों के साथ। listedDate "Mon DD, YYYY" की स्ट्रिंग को पार्स करता है जिसे Etsy आइटम विवरण के पास प्रदर्शित करता है - ध्यान दें कि यह सबसे हालिया पुनः सूचीबद्ध/स्वत: नवीनीकरण की तिथि को दर्शाता है, न कि मूल निर्माण तिथि। shop.reviewsCountShop केवल तब भरा जाता है जब Etsy स्पष्ट रूप से संख्या को दुकान स्तर पर भेदित करता है (कई सूची लेआउट इसे प्रदर्शित नहीं करते - वहाँ ईमानदार उत्तर है 'null')।

समीक्षक-अपलोड की गई तस्वीरें प्रत्येक समीक्षा कार्ड के भीतर होती हैं। extractReviews अब प्रति समीक्षा 6 तस्वीरें कैप्चर करता है जिसमें उसी il_fullxfull अपग्रेड का उपयोग किया गया है जो लिस्टिंग छवियों के लिए किया गया है, जिससे डाउनस्ट्रीम कोड के लिए दृश्य विश्लेषण या समीक्षा सत्यापन के लिए समान छवि कॉर्पस मिलता है।

चरण 8 - प्रति उत्पाद समृद्धि और त्रुटि हैंडलिंग

एक लिस्टिंग को स्क्रैप करना सीधा है। एक सौ एक के बाद स्क्रैपिंग तब है जब अस्थायी विफलताएं दिखाई देने लगती हैं - Etsy कभी-कभी एक पुराना कैश देता है, h1 पूरी नहीं होती, एकल प्रॉक्सी अनुरोध समय समाप्त हो जाता है। ये सभी को पैमाने पर संभालने के लिए तीन सुरक्षात्मक परतें हैं:

प्रत्येक उत्पाद के लिए ताज़ा ब्राउज़र। जब खोज हिट एकत्रित हो जाते हैं, तो प्रत्येक समृद्धि के लिए नए Scrapeless स्क्रैपिंग ब्राउज़र सत्र को खोलें। राज्य उत्पादों के बीच लीक नहीं होता है और एक सत्र-स्तरीय त्रुटि दौड़ के बाकी को खराब नहीं करती है। प्रत्येक ताज़ा सत्र एक नया निवास IP घुमाता है, इसलिए जब DataDome एक IP पर 403 लौटाता है, तो अगली कोशिश एक अलग पर बैठती है।

cfg.maxRetries पुनः प्रयास प्रयासों तक (डिफ़ॉल्ट 10) बढ़ती हुई बैकऑफ के साथ। एक स्वच्छ रन में अधिकांश उत्पाद पहले प्रयास पर सफल होते हैं; एक खराब-IP रन पर, सत्र को एक साफ निवासी IP पर बैठने में 3–6 प्रयास लग सकते हैं। उच्च पुनः प्रयास बजट 50% हिट दर और 100% के बीच का अंतर है।

Copy
**श्रेणीबद्ध त्रुटि वर्गीकरण।** `categorizeError(err)` हर कच्ची विफलता (HTTP 403/404/429, `ERR_SSL_*`, `ERR_TUNNEL_*`, h1-मिसिंग, नेविगेशन टाइमआउट, WSS हैंडशेक) को आठ `ScrapeErrorKind` मानों में से एक से जोड़ता है, जिसमें एक `retryable: boolean` फ्लैग होता है। पुनः प्रयास योग्य त्रुटियाँ बैकऑफ लूप को फीड करती हैं; गैर-पुनः प्रयास योग्य त्रुटियाँ (जैसे कि स्टेल लिस्टिंग पर HTTP 404) तुरंत समाप्त हो जाती हैं। जब सभी प्रयास समाप्त हो जाते हैं, तो उत्पाद `error: { kind, message, attempts }` के साथ भेजा जाता है ताकि डाउनस्ट्रीम कोड यह जान सके कि एक पंक्ति अचानक खाली क्यों आई।

```ts
// मुख्य लूप के अंदर, प्रत्येक खोज हिट h के लिए एक बार (i द्वारा अनुक्रमित):
const MAX_ATTEMPTS = cfg.maxRetries;   // डिफ़ॉल्ट 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(`openBrowser विफल: ${e?.message ?? e}`));
    log(`    प्रयास ${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(`no h1 on ${h.url} — title was null`));
  } catch (e: any) {
    lastError = categorizeError(e);
    log(`    प्रयास ${attempt}/${MAX_ATTEMPTS} — ${lastError.kind}: ${lastError.message.slice(0, 120)}`);
    if (!lastError.retryable) break;   // गैर-पुनः प्रयास योग्य: 404, आदि। जल्दी विफल।
  } finally {
    await eb.close().catch(() => {});
  }
  if (attempt < MAX_ATTEMPTS) {
    // कॉन्फ़िगर करने योग्य बढ़ती बैकऑफ। डिफ़ॉल्ट (3000, 1500, 500) का परिणाम
    // 5s, 8s, 12s, 17s, 23s, 30s, 38s, 47s, 57s के बीच प्रयासों का होता है।
    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);
// उत्पादों के बीच का अंतराल (कॉन्फ़िगर करने योग्य) — लगातार लिस्टिंग को तोड़ता है
// नेविगेशनों को ताकि सत्र का पैटर्न DataDome के लिए बोट के आकार का न दिखे।
if (i < hits.length - 1) await delay(cfg.interProductDelayMs);

कुछ विवरण जो पैमाने पर महत्वपूर्ण हैं: सत्र का नाम उत्पाद अनुक्रमांक i और Date.now() को शामिल करता है ताकि ताज़ा सत्रों के बीच टकराव न हो; openBrowser को अपनी खुद की try/catch में लपेटा गया है ताकि विफल WSS हैंडशेक पुनः प्रयास को न छोड़ दे; eb.close() को .catch(() => {}) के साथ समाहित किया गया है क्योंकि सत्र पहले से ही मर चुका होता है जब आप इसे बंद कर रहे होते हैं; बढ़ती बैकऑफ इतनी धीरे-धीरे बढ़ती है कि आसान उत्पाद तेजी से समाप्त हो जाते हैं लेकिन कठिन उत्पादों को DataDome द्वारा निर्धारित कई सेकंड का विंडो मिलता है; और उत्पादों के बीच का अंतराल संख्यात्मक रूप से संबंधित ब्लॉकों के अवसर को कम करता है।

आठ त्रुटियों के प्रकार

हर विफल उत्पाद एक error: { kind, message, attempts } ऑब्जेक्ट ले जाता है। kind फ़ील्ड डाउनस्ट्रीम कोड को यह बताती है कि प्रतिक्रिया कैसे देनी है बिना मुक्त-फॉर्म संदेश को पार्स किए:

kind ट्रिगर पुनः प्रयास योग्य
blocked HTTP 403 या 429 — DataDome या दर सीमा ✅ हाँ
not-found HTTP 404 — लिस्टिंग हटा दी गई या कभी अस्तित्व में नहीं थी ❌ नहीं (जल्दी विफल)
tls ERR_SSL_* / ERR_CERT_* — अस्थायी प्रॉक्सी हिकप ✅ हाँ
network ERR_TUNNEL / ERR_CONNECTION_* / ERR_ABORTED ✅ हाँ
no-h1 पृष्ठ लोड हुआ लेकिन <h1> कभी नहीं आया — सॉफ्ट चैलेंज पृष्ठ ✅ हाँ
timeout नेविगेशन टाइमआउट pageTimeoutMs से अधिक ✅ हाँ
open-browser Scrapeless के लिए WSS हैंडशेक विफल ✅ हाँ
unknown कुछ और ✅ हाँ (डिफ़ॉल्ट)

हर टन के कोण को ट्यून किया जा सकता है

सभी पुनः प्रयास और तालिका मान ScraperInput पर रहते हैं — कुछ भी हार्ड-कोडेड नहीं है। जब आपको सख्त योजना पर पूर्वानुमेय थ्रूपुट की आवश्यकता हो या कठिन लक्ष्य पर अधिक आक्रामक पुनः प्रयास की आवश्यकता हो, तब इन्हें समायोजित करें:

CONFIG फ़ील्ड डिफ़ॉल्ट भूमिका
maxRetries 10 उत्पाद पर कुल प्रयास जो हार मानने से पहले होते हैं
retryInitialBackoffMs 3000 बढ़ते-बैकऑफ फ़ार्मूले का आधार
retryBackoffLinearMs 1500 रैखिक शब्द
retryBackoffQuadraticMs 500 द्विघातीय शब्द
interProductDelayMs 3000 लगातार उत्पाद संवर्द्धनों के बीच रुका हुआ
pageTimeoutMs 60000 page.goto टाइमआउट
h1TimeoutMs 15000 waitForSelector("h1") टाइमआउट
postLoadDelayMs 1500 h1 दिखने के बाद, निष्कर्षण से पहले की देरी

आपको क्या मिलता है

प्रत्येक उत्पाद के लिए एक फ्लैट JSON ऑब्जेक्ट। जानबूझकर चौड़ा, ताकि वही स्क्रैपर बिना दूसरे चरण के हर डाउनस्ट्रीम उपयोग मामले को फीड कर सके।

"लेदर वॉलेट" खोज पर इस सटीक टेम्पलेट से चलने वाला वास्तविक पहला परिणाम:

json Copy
{
  "listingId": "547491922",
json Copy
{
  "title": "लेदर वॉलेट•वॉलेट•पुरुष लेदर वॉलेट•मिनिमलिस्ट वॉलेट•व्यक्तिगत वॉलेट•लेदर एनिवर्सरी•स्लिम लेदर वॉलेट•पुरुष वॉलेट",
  "url": "https://www.etsy.com/listing/547491922/leather-walletwalletman-leather",
  "rank": 1,
  "price": 5.52,
  "originalPrice": 68.99,
  "currency": "$",
  "discountPercent": 92,
  "inStock": false,
  "rating": 4.9,
  "reviewsCount": 929,
  "favoritesCount": 850,
  "isBestseller": false,
  "isFreeShipping": false,
  "isStarSeller": true,
  "tags": ["ब्राइड्समेड उपहार", "गroomsmen उपहार", "शादी के उपहार", "एंगेजमेंट उपहार"],
  "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 और यूआरएल"
  ],
  "variations": [
    { "name": "व्यक्तिगतकरण", "options": ["हाँ, उत्कीर्णन जोड़ें", "नहीं, धन्यवाद"] },
    { "name": "रंग विकल्प", "options": ["चेस्टनट", "काला", "टैन"] }
  ],
  "breadcrumbs": ["मुखपृष्ठ", "बैग और पर्स", "वॉलेट और पैसे क्लिप", "वॉलेट"],
  "relatedSearches": ["पुरुषों के लिए लेदर वॉलेट", "स्लीक पुरुष वॉलेट", "कस्टम स्लिम लेदर बिफोल्ड वॉलेट"],
  "listedDate": "15 अप्रैल, 2026",
  "priceBucket": null,
  "reviews": [
    {
      "author": "लिज",
      "rating": 0,
      "text": "जैसा वर्णित था और तेजी से भेजा गया। धन्यवाद!",
      "date": "12 अप्रैल, 2026",
      "itemQuality": null,
      "shipping": null,
      "customerService": null,
      "photos": []
    },
    "... 9 और समीक्षाएँ"
  ],
  "error": null,
  "scrapedAt": "2026-04-16T17:09:48.919Z"
}

एक समान कनेक्शन हेल्पर, रिट्राई टैक्सोनोमी और सत्र-प्रति-लक्ष्य पैटर्न व्यापक Scrapeless कैटालॉग में फैला हुआ है: इस गाइड को Scrapeless MCP Server के साथ जोड़ें ताकि Etsy डेटा को सीधे एक AI एजेंट के टूल सरफेस में वायर्ड किया जा सके, या सर्वश्रेष्ठ AI एजेंटों की सूची के साथ संदर्भ के लिए कि यह पाइपलाइन व्यापक स्वचालन कार्यप्रवाहों में कैसे प्लग होती है।

proxyCountry को उस मार्केटप्लेस से मेल खाने के लिए पिन करें जिसके लिए आप मूल्य निर्धारण चाहते हैं, sessionRecording: "true" रखकर सुनिश्चित करें कि कोई भी शून्य पंक्ति को एंड-टू-एंड फिर से चलाया जा सके, अनुपस्थित फ़ील्ड (materials, shop.location, reviews[].itemQuality) को छूटी डेटा बग के बजाय नल के रूप में ट्रीट करें, और बढ़ती बैकऑफ को अस्थायी 403s को अवशोषित करने दें। यही पूरा प्लेबुक है।


क्या आप अपने AI-संचालित डेटा पाइपलाइन बनाने के लिए तैयार हैं?

हमारे समुदाय में शामिल हों ताकि एक मुफ्त योजना का दावा करें और उन डेवलपर्स से जुड़ें जो Etsy इंटेलिजेंस पाइपलाइनों का निर्माण कर रहे हैं: Discord · Telegram.

app.scrapeless.com पर साइन अप करें मुफ्त Scraping Browser रनटाइम के लिए — मुफ्त ट्रायल पर 100 घंटे तक का ब्राउज़र रन — और ऊपर दिए गए पैटर्न को Etsy श्रेणियों, दुकानों और कीवर्ड के लिए अनुकूलित करें जिनकी आपके पाइपलाइन को आवश्यकता है।


सामान्य प्रश्न

क्या Etsy से डेटा स्क्रैप करना कानूनी है?

सार्वजनिक रूप से उपलब्ध डेटा को मूल्य निगरानी और शोध के लिए स्क्रैप करना आमतौर पर कानूनी है, बशर्ते आप Etsy की उपयोग की शर्तों का सम्मान करें और व्यक्तिगत उपयोगकर्ता डेटा को स्क्रैप करने से बचें। Scrapeless का उपयोग आपके स्क्रैपिंग गतिविधि को प्रबंधित गति के माध्यम से सर्वर संसाधनों का सम्मान करने की सुनिश्चितता देता है।

Scrapeless Etsy के DataDome सुरक्षा में कैसे कार्य करता है?

मानक प्रॉक्सियों के विपरीत, Scrapeless पूरा ब्राउज़र फिंगरप्रिंट और TLS हैंडशेक प्रबंधित करता है। इससे आपका स्क्रैपर एक असली उपयोगकर्ता से अप्रभेद्य होता है, जिससे आप DataDome की sofisticaded बॉट डिटेक्शन को बिना मैनुअल स्टील्थ कॉन्फ़िगरेशन के बायपास कर सकते हैं।

प्रश्न 1: क्या Etsy को स्क्रैप करने के लिए प्रॉक्सी की आवश्यकता है?

ज्यां. बिना एक आवासीय प्रॉक्सी के, DataDome जल्दी ही डेटा सेंटर ट्रैफ़िक को झंडी दिखाता है — फिंगरप्रिंट और IP-प्रतिष्ठा संयोजन आमतौर पर अस्वीकृति बाल्टी में स्कोर करता है और सीधे नेविगेशन अनुरोध /listing/ पृष्ठों पर HTTP 403 के साथ एक JavaScript चुनौती पृष्ठ वापस करता है। Scrapeless Scraping Browser में अंतर्निहित आवासीय प्रॉक्सी होते हैं — हर सत्र चुने हुए देश में एक अलग आवासीय IP के माध्यम से रूट करता है, जो लगातार ताज़ा सत्रों द्वारा अद्वितीय आउटबाउंड IPs (api.ipify.org प्रॉब) लौटाने में परीक्षण द्वारा सत्यापित किया गया है।

प्रश्न 2: मैं पिछले रन में स्क्रैपर ने क्या किया, कैसे देख सकता हूं?

इस टेम्पलेट में हर सत्र WSS URL पर sessionRecording: "true" सेट करता है, इसलिए Scrapeless हर पृष्ठ का एक पूर्ण वीडियो-शैली का पुनरावलोकन सहेजता है जिसे क्लाउड ब्राउज़र ने छुआ — स्क्रॉल स्थिति, DOM स्थिति और नेटवर्क गतिविधि। पुनरावलोकन को app.scrapeless.comScraping BrowserSessions पर खोजें, और प्रत्येक प्रयास (जैसे etsy-enrich-3-2-1713198231047) के लिए स्क्रैपर लॉग द्वारा sessionName मान से मेल करें।

यदि डैशबोर्ड "पुनरावलोकन अस्वीकृत — सत्र पुनरावलोकन देखने के लिए 'वेब रिकॉर्डिंग' सक्षम करें" दिखाता है, तो अपने Scrapeless खाता सेटिंग्स पृष्ठ पर वेब रिकॉर्डिंग टॉगल चालू करें। यह हर योजना पर मुफ्त है; यह केवल डिफ़ॉल्ट रूप से बंद है। एक बार सक्षम होने पर, सभी भविष्य के सत्र अपने आप रिकॉर्ड होते हैं — रिकॉर्डिंग बंद होने के दौरान जो पिछले सत्र चलाए गए, उन्हें रेट्रोस्पेक्टिवली पुनर्प्राप्त नहीं किया जा सकता।

पुनरावलोकन यह जानने का सबसे तेज़ तरीका है कि एक पंक्ति title: null के साथ क्यों लौटाई गई। सत्र खोलें, उस समय तक टाइमलाइन को स्क्रब करें जब page.goto चालू हुआ, और आप देखेंगे कि सर्वर ने असली लिस्टिंग, DataDome चुनौती या एक स्टेल-यूआरएल रिडायरेक्ट लौटाया।

प्रश्न 3: कभी-कभी समीक्षाएँ पृष्ठ के बजाय आंतरिक एंडपॉइंट के माध्यम से क्यों लोड होती हैं?

नए Etsy लिस्टिंग कुछ समीक्षा बैचों को पृष्ठ के रेंडर होने के बाद आंतरिक POST अनुरोधों के माध्यम से लोड करते हैं। स्क्रैपर इसे समीक्षाओं क्षेत्र में स्क्रॉल करके और प्रतीक्षा करके संभालता है — जब पार्सर चल रहा होता है, कार्ड DOM में होते हैं। हजारों समीक्षाओं वाले उत्पादों के लिए, आप पहले ~30 (या जो भी आप maxReviews पर सेट करते हैं) प्राप्त करेंगे। गहराई से जाने की आवश्यकता ग्राफ़क्यूएल एंडपॉइंट को सीधे इंटरसेप्ट करने की है, जो यहाँ के दायरे से बाहर है।

प्रश्न 4: क्षेत्र और मुद्रा रिडायरेक्ट के बारे में क्या?

Etsy IP द्वारा स्थानीयकृत संस्करणों पर रिडायरेक्ट करता है (जर्मन IP से etsy.de, फ्रेंच से etsy.fr)। कीमतें और मुद्रा स्ट्रिंग क्षेत्र के अनुसार भिन्न होती हैं। स्क्रैपर का extractNumber हेल्पर 1,234.56 (en-US) और 1.234,56 (de-DE) प्रारूप दोनों को संभालता है। यदि आप सभी रनों में लगातार USD मूल्य निर्धारण चाहते हैं, तो proxyCountry: "US" पर पिन करें।

प्रश्न 5: मैं मूल्य, बिक्री पर, मुफ्त शिपिंग, या स्थिति के अनुसार कैसे फ़िल्टर कर सकता हूँ?

आठ filters.* कुंजियों का कोई भी संयोजन सेट करें। ये searchQuery और categoryUrl मोड के साथ मिलकर काम करते हैं और Etsy के URL पर सीधे एन्कोड होते हैं:

ts Copy
const CONFIG: ScraperInput = {

categoryUrl: "https://www.etsy.com/c/bags-and-purses/wallets-and-money-clips/wallets",
filters: {
onSale: true, // → &is_on_sale=1
freeShipping: true, // → &free_shipping=1
customizable: true, // → &is_personalizable=1
shipsTo: "US", // → &ships_to=US (ISO देश कोड)
minPrice: 20, // → &min=20
maxPrice: 60, // → &max=60
condition: "vintage", // "नया" | "विंटेज" (→ &explicit=vintage)
orderBy: "price_asc", // "सबसे महत्वपूर्ण" | "दिनांक_desc" | "price_asc" | "price_desc" | "उच्चतम समीक्षाएँ"
},
// ...
};

Copy
दो caveats जिससे अवगत होना आवश्यक है: `filters.minPrice` / `filters.maxPrice` पर एक `/search?q=...` URL DataDome-संवेदनशील है (छानबीन की गई खोज URLs को असाधारण रूप से 403'd किया जाता है) इसलिए `expandStrategy: "prices"` अब एक व्यापक खोज करता है और परिणामों को क्लाइंट-साइड `priceBucket` के माध्यम से टैग करता है - वही उपयोगकर्ता इरादा, कोई URL-छानबीन ब्लॉक नहीं। `categoryUrl` पर मूल्य फ़िल्टर सामान्य रूप से काम करता है।

### Q6: क्या मैं retries, timeouts और pacing को समायोजित कर सकता हूँ?

हाँ। प्रत्येक retry और pacing मान `ScraperInput` पर एक CONFIG फ़ील्ड है:

| फ़ील्ड | डिफ़ॉल्ट | भूमिका |
|---|---|---|
| `maxRetries` | `10` | उत्पाद पर कुल प्रयास इससे पहले कि हार मान लें |
| `retryInitialBackoffMs` | `3000` | बढ़ती बैकऑफ सूत्र का आधार |
| `retryBackoffLinearMs` | `1500` | रैखिक पद |
| `retryBackoffQuadraticMs` | `500` | गुणात्मक पद (5 स → 57 स की प्रगति देता है) |
| `interProductDelayMs` | `3000` | लगातार उत्पाद समृद्धियों के बीच विराम |
| `pageTimeoutMs` | `60000` | `page.goto` टाईमआउट |
| `h1TimeoutMs` | `15000` | `waitForSelector("h1")` टाईमआउट |
| `postLoadDelayMs` | `1500` | `h1` प्रकट होने के बाद, निष्कर्षण से पहले देरी |

कठोर Scrapeless योजनाएं कम `interProductDelayMs` + कम `maxRetries` से लाभ उठाती हैं; कठिन एंटी-बॉट लक्ष्यों को दोनों पर उच्च मानों की आवश्यकता होती है।

### Q7: मुझे कौन से विफलता श्रेणियाँ अपेक्षित हो सकती हैं?

हर उत्पाद जो retry समाप्त करता है एक संरचित `error: { kind, message, attempts }` फ़ील्ड लेकर आता है। आठ श्रेणीबद्ध प्रकार:

- `blocked` — DataDome से HTTP 403/429 या दर सीमा (retryable)
- `not-found` — HTTP 404, पुराने या हटाए गए लिस्टिंग (non-retryable — तेज़ी से विफल)
- `tls` — `ERR_SSL_*` / `ERR_CERT_*` प्रॉक्सी TLS झटका (retryable)
- `network` — `ERR_TUNNEL` / `ERR_CONNECTION_*` / `ERR_ABORTED` (retryable)
- `no-h1` — पृष्ठ लोड हो गया लेकिन `<h1>` कभी नहीं प्रकट हुआ, संभवतः एक नरम DD चुनौती पृष्ठ (retryable)
- `timeout` — नैविगेशन टाइमआउट पार किया (retryable)
- `open-browser` — Scrapeless के लिए WSS हैंडशेक विफल (retryable)
- `unknown` — कुछ और (डिफ़ॉल्ट रूप से retryable)

डाउनस्ट्रीम कोड `kind: "not-found"` को "इस URL को छोड़ दें, इसे कभी पुनः कतारबद्ध न करें" और `kind: "blocked"` को "इस एक को अगले घंटे में पुनः प्रयास करें जब DataDome की IP प्रतिष्ठा खिड़की रिसेट होती है" के रूप में मान सकता है।

स्क्रैपलेस में, हम केवल सार्वजनिक रूप से उपलब्ध डेटा का उपयोग करते हैं, जबकि लागू कानूनों, विनियमों और वेबसाइट गोपनीयता नीतियों का सख्ती से अनुपालन करते हैं। इस ब्लॉग में सामग्री केवल प्रदर्शन उद्देश्यों के लिए है और इसमें कोई अवैध या उल्लंघन करने वाली गतिविधियों को शामिल नहीं किया गया है। हम इस ब्लॉग या तृतीय-पक्ष लिंक से जानकारी के उपयोग के लिए सभी देयता को कोई गारंटी नहीं देते हैं और सभी देयता का खुलासा करते हैं। किसी भी स्क्रैपिंग गतिविधियों में संलग्न होने से पहले, अपने कानूनी सलाहकार से परामर्श करें और लक्ष्य वेबसाइट की सेवा की शर्तों की समीक्षा करें या आवश्यक अनुमतियाँ प्राप्त करें।

सबसे लोकप्रिय लेख

सूची