Etsy scrapen: Leitfaden 2026

Etsy betreibt DataDome und hat alle 9 Transportwege, die wir getestet haben, abgelehnt. Erfahren Sie, was robots.txt erlaubt, was einen Datensatz beschädigt und wann Bright Data besser passt.
18 min lesen
How to Scrape Etsy blog image

Etsy scrapen bedeutet, Listing-, Shop- und Bewertungsdaten von öffentlichen Etsy-Seiten zu erfassen. Etsy meldete mehr als 100 Millionen Artikel und 5,6 Millionen aktive Verkäufer im Jahresbericht 2025. Etsy setzt DataDome ein. Eine direkte Anfrage gibt einen 403-Fehler und eine Blockseite zurück. Die 2 häufigsten Lösungsansätze sind ein Chrome User-Agent und TLS-Impersonation. Beide führen weiterhin zu einem 403. Dieser Leitfaden zeigt, was den 403-Fehler behebt, was robots.txt blockiert und welche Extraktionsprobleme einen Etsy-Datensatz beschädigen, ohne einen Fehler auszulösen.

TL;DR

  • Etsy betreibt DataDome hinter Fastly. DataDome schreibt einen Risikoscore in seinen eigenen Response-Header. Damit lässt sich eine Anfrage bewerten, bevor man einen Crawler entwickelt.
  • Etsy hat alle 9 Transportwege abgelehnt, die wir getestet haben. Ein headed Browser lud die erste Seite in 2,2 Sekunden. Die zweite Anfrage wurde von Etsy abgelehnt.
  • robots.txt verbietet die Keyword-Suche und die Verkaufshistorie und deklariert keinen Sitemap. Listing- und Shop-Seiten blieben dort offen. Etsys Nutzungsbedingungen sind jedoch strenger.
  • Währung und Preis richten sich nach der Exit-IP. Unser Datensatz-Sample enthielt 19 Währungen. Ein ungefilterter Durchschnitt dieser Spalte war 227-mal zu hoch.

Was Etsy auf eine automatisierte Anfrage zurückgibt

Lesen Sie zuerst die Ablehnung. Wiederholen Sie die Anfrage nicht. Eine einfache Anfrage zeigt, welcher Anbieter am Edge läuft und wie dieser Anbieter Sie bewertet hat. Jede Listing-URL funktioniert. Diese gehört einem Verkäufer und ist möglicherweise bereits nicht mehr verfügbar:

curl -sD - -o /dev/null https://www.etsy.com/listing/753913297/smoky-quartz-ring-rose-gold-ring-women

Der Server gibt einen 403-Fehler mit diesen Headern zurück:

HTTP/2 403
server: DataDome
x-datadome: protected
x-datadome-riskscore: 0.9230727100377928
accept-ch: Sec-CH-UA,Sec-CH-UA-Mobile,Sec-CH-UA-Platform,Sec-CH-UA-Arch,Sec-CH-UA-Full-Version-List,Sec-CH-UA-Model,Sec-CH-Device-Memory
set-cookie: datadome=ovuDEZ4S0taK1jDAOvZ9W3Uq2qUujN_iPPE3uXp7~r3msKXSvMFPp4j30em5IvR0...
via: 1.1 varnish
x-served-by: cache-del-vibw2260027-DEL

Dieser Block enthält 3 Fakten. Etsy verwendet DataDome, nicht den Akamai Bot Manager, den ältere Leitfäden noch erwähnen, also plant man gegen DataDomes Erkennungsschichten. Fastly läuft vor Etsy, und die Via– und X-Served-By-Header zeigen diesen Hop. X-DataDome-riskscore ist DataDomes Score dafür, wie bot-ähnlich die Anfrage wirkte, auf einer Skala, bei der 1,0 der schlechteste Wert ist. Etsy kann Anbieter wechseln, also führen Sie den Befehl erneut aus, bevor Sie gegen diese 3 Fakten planen.

Dieser Header gibt Ihnen eine Zahl zum Messen. In unseren Tests war der Score deterministisch. Alle 6 verschachtelten Samples derselben Anfrage gaben 0.9230727100377928 zurück. Der Score verfolgt die Anfrage-Signatur, die aufrufende IP und den datadome-Cookie, sobald Sie einen zurücksenden, nicht eine laufende Zählung von Anfragen. Ihre eigenen Zahlen können also abweichen, und bei einer anderen Adresse kann das Ranking zwischen Client-Konfigurationen ebenfalls abweichen.

Der Body, 776 Bytes in unseren Läufen, ist eine DataDome-Blockseite, die ct.captcha-delivery.com/c.js lädt. Diese Blockseite enthält ein Challenge-Skript, keine Listing-Daten, sodass ein Parser, der sie liest, leere Felder statt eines Fehlers zurückgibt. Die eingebettete Konfiguration enthielt 't':'fe', DataDomes Geräteprüfung, sodass dieser Fetch eine Challenge erhielt, die ein echter Browser beantworten kann, statt eines dauerhaften Bans. Das 't'-Feld zeigt die aktuelle Bewertung und nimmt andere Werte an, wenn sich diese ändert, also lesen Sie Ihren eigenen Wert, statt 'fe' anzunehmen.

Wir haben jede Anfrage von 1 minimalen 3-Header-Client gesendet und nur den Pfad variiert. Jeder Inhaltspfad, den wir versuchten, gab denselben 403 und denselben 0,482-Score zurück, während /robots.txt und eine URL, die zu nichts auflöst, ungeschützt zu Apache gingen:

path                                       HTTP server     riskscore
/                                          403  DataDome   0.482
/listing/753913297/smoky-quartz-ring...    403  DataDome   0.482
/shop/AnemoneJewelry                       403  DataDome   0.482
/legal/terms/                              403  DataDome   0.482
/robots.txt                                200  Apache     -
/nonexistent-path-xyz                      404  Apache     -

Auf den von DataDome geschützten Pfaden bewertet es also den Aufrufer und nicht die URL.

Eine Anfrage mit einem Googlebot User-Agent umging DataDome und erreichte stattdessen einen Rate-Limiter. Apache antwortete mit 429 Too Many Requests und einem nicki_-Referenzstring. Mindestens 1 deklarierter Suchmaschinen-Agent erreicht einen eigenen Rate-Limiter, getrennt von DataDome.

Warum Header und TLS-Fingerprints nicht ausreichen

Der Risikoscore ermöglicht es, den üblichen Rat zu Headern zu testen, wobei eine niedrigere Zahl weniger bot-ähnlich bedeutet. Wir hielten IP und URL konstant, variierten nur die Request-Header eines requests-Clients und notierten den von DataDome zurückgegebenen Score:

headers sent                                         score    HTTP
requests, library default headers                    0.977    403
+ Chrome User-Agent only                             0.923    403
full 12-header Chrome set                            0.503    403
User-Agent + accept + sec-fetch-site (3 headers)     0.482    403

Diese Tabelle enthält 2 Ergebnisse. Das Hinzufügen eines Chrome User-Agents hat die Bewertung kaum verändert, weil alles andere in der Anfrage immer noch von requests stammte. Und das 3-Header-Set erzielte einen besseren Score als das vollständige 12-Header-Set, daher war bei diesem Ziel Konsistenz wichtiger als die Header-Anzahl.

Diese 3 Header erzielten 0,482, also kopieren Sie den accept-Wert exakt:

User-Agent:      Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
                 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
accept:          text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,
                 image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
sec-fetch-site:  none

Wir haben jeweils 1 Header aus dem vollständigen Set entfernt und jede Änderung notiert, wobei ein Pluszeichen bedeutet, dass der Score schlechter wurde:

run                              score     change
full set (baseline)              0.503
minus accept                     0.845     +0.342
minus user-agent                 0.784     +0.281
minus sec-fetch-site             0.669     +0.166
minus every sec-ch-ua* header    0.503      0.000

DataDome bewirbt 7 Client-Hints in seinem eigenen Accept-CH-Response-Header. Unser vollständiges Set hatte 3 davon, aber das Entfernen aller sec-ch-ua-Header änderte nichts. Der accept-Header bewegte den Score stärker als der User-Agent.

TLS-Impersonation ist der übliche nächste Schritt, also haben wir sie allein getestet. Jeder Lauf sendete dieselben 3 Header, und nur der TLS- und HTTP/2-Fingerprint änderte sich. Die JA4-Werte unten stammen aus curl_cffi und nicht von Etsy, daher ändern sie sich, wenn diese Bibliothek ein Profil aktualisiert:

transport                             JA4                      riskscore  HTTP
python requests (OpenSSL, HTTP/1.1)   t13d1712h1_ab0a1bf427ad  0.482      403
curl_cffi impersonate=chrome110       t13d1516h2_8daaf6152771  0.663      403
curl_cffi impersonate=chrome116       t13d1516h2_8daaf6152771  0.663      403
curl_cffi impersonate=chrome124       t13d1516h2_8daaf6152771  0.503      403
curl_cffi impersonate=chrome131       t13d1516h2_8daaf6152771  0.503      403
curl_cffi impersonate=chrome133a      t13d1516h2_8daaf6152771  0.503      403
curl_cffi impersonate=firefox133      t13d1716h2_5b57614c22b0  0.503      403
curl_cffi impersonate=safari17_0      t13d2014h2_a09f3c656075  0.663      403
curl_cffi impersonate=safari17_2_ios  t13d2014h2_a09f3c656075  0.663      403

Diese Tabelle enthält 3 verschiedene Fingerprints, je einen für Chrome, Firefox und Safari. Einfaches requests über HTTP/1.1 erzielte immer noch einen besseren Score als alle 3. Diese Spalte gibt nur die ersten 2 Teile eines JA4 aus. Der dritte Teil kodiert die Erweiterungsliste und unterscheidet sich zwischen den Chrome-Profilen, die hier ein Präfix teilen.

Der Transport bewegte den Score zwischen dem besten und schlechtesten Lauf um 0,181, aber kein Lauf gab eine Seite zurück. Kein Fingerprint, den wir getestet haben, reichte allein aus.

Also lohnen sich TLS-Fingerprints bei diesem Ziel nicht. Wir haben die HTTP/2-Frames von jedem Profil zurückgelesen, einschließlich der Pseudo-Header-Reihenfolge, die sich je nach Engine unterscheidet:

profile      SETTINGS                       | window   | pri | pseudo-header order
chrome131    1:65536;2:0;4:6291456;6:262144 | 15663105 | 0   | m,a,s,p
firefox133   1:65536;2:0;4:131072;5:16384   | 12517377 | 0   | m,p,a,s
safari17_0   2:0;4:4194304;3:100            | 10485760 | 0   | m,s,p,a

Diese 3 Handshakes sind korrekte Kopien, und Etsy hat alle 3 abgelehnt. DataDome entscheidet also anhand von etwas anderem als dem Handshake.

Was die Prüfung tatsächlich ausführt

In einem headed Browser rendert DataDomes Challenge als Slider, und 2 der 4 Gründe sind die aufrufende IP und die Verwendung von Entwicklertools:

Etsys DataDome-Challenge-Seite. Eine Überschrift lautet 'Verification Required' über einem Widget mit der Aufschrift 'Slide right to secure your access' mit einem Ziehgriff, einem Bild-oder-Audio-Umschalter und einer Aktualisierungssteuerung. Darunter eine Grundliste: schnelle Taps oder Klicks, JavaScript deaktiviert oder nicht funktionsfähig, automatisierte Aktivität in Ihrem Netzwerk mit geschwärzter IP und Verwendung von Entwickler- oder Inspektionstools

Dieser c.js-Loader ist 14 KB groß, erstellt eine iframe-URL und ruft eine zweite Seite ab. Diese zweite Seite enthielt an dem Tag, an dem wir sie abgerufen haben, ein 596 KB großes Inline-Skript, und seine Modulnamen zeigen, was man reimplementieren müsste:

detection-js/dist/vm-obf.js     the detection engine, VM-obfuscated
detection-js/dist/captcha.js    challenge coordination
./picasso                       canvas-based device-class fingerprinting
./mouseMaths                    pointer-movement analysis
./slidercaptcha  ./hash  ./helpers  ./bean

Das Erkennungsmodul wird als Bytecode für einen im selben File gebündelten Interpreter gespeichert. Deshalb findet eine Textsuche im Bundle weder webdriver, noch cdc_, noch headless, noch _phantom. Ein Bytecode-Bundle verbirgt diese Strings, egal ob die Probes laufen oder nicht, sodass das Suchergebnis nichts aussagt.

Der Build, den wir abgerufen haben, meldete sich selbst als 1.34.0, maß seine eigene Ausführungszeit und sendete diese Dauer als Signal. Dieser Build protokollierte auch eine Konsolenwarnung, die Sie aufforderte, DevTools zu schließen, bevor Sie fortfahren. Erwarten Sie eine andere Version und eine andere Modulliste, wenn Sie nachschauen, da diese Engine dem Release-Zyklus von DataDome und nicht dem von Etsy folgt. Die Architektur hinter diesen Namen ändert sich weit langsamer.

Die Check-Anfrage hat 16 Felder und kodiert die Browser-Umgebung in userEnv, ddCaptchaEnv und plv3.

Es gibt also 2 praktische Schlussfolgerungen. Diesen Output aus requests zu reproduzieren bedeutet, eine obfuskierte VM gegen einen Versionsstring reimplementieren zu müssen, der sich erhöht. Und DataDome sammelt Canvas-Output und Zeigerbewegungen, die nur existieren, nachdem eine echte Browser-Engine die Seite gerendert hat. DataDome-spezifisches Entsperren betreibt diese Browser-Engine als Dienst und ist darauf ausgelegt, die gerenderte Seite zurückzugeben statt des Blocks.

Diese zweite Schlussfolgerung ist testbar, also haben wir einen einfachen Playwright Chromium ohne Stealth-Patches und ohne Proxy ausgeführt. Jeder Modus wurde 3 Mal von derselben Adresse aus gestartet, von der alle oben genannten Transporte abgelehnt wurden:

headless=True (default)        403  403  403     1,530 B    0.5s
headless=True --headless=new   403  403  403     1,530 B    0.4s
headless=False (headed)        200  200  200   532,049 B    2.2s   Product JSON-LD present

Beide Fenster liefen von 1 Maschine ohne Proxy, sodass das headed Fenster Preise in der lokalen Währung anzeigt:

Zwei Chromium-Fenster nebeneinander. Das headless-Fenster zeigt Etsys Blockseite mit der Aufschrift 'Access is temporarily restricted' über einer Liste von Gründen einschließlich automatisierter Aktivität im Netzwerk. Das headed Fenster zeigt die vollständige Listing-Seite mit Produktfoto, Preis, Variations-Dropdowns und einer Schaltfläche 'In den Warenkorb'

Headless Chromium enthält HeadlessChrome in seinem eigenen User-Agent, sodass sich diese Zeilen in mehr als dem Fenster unterscheiden. Für die manuelle Überprüfung einer Seite oder das Abrufen einiger weniger Seiten ist ein headed Browser die einfachste Antwort. Entsperr-Infrastruktur ist für die Anfragen nach der ersten ausgelegt, und für einen einzelnen Fetch ist ein headed Lauf etwa 10 Mal schneller als die Weiterleitung durch sie.

Warum ein Browser kein Crawler ist

Wir haben 12 Listings nacheinander in einer einzigen Browser-Seite geladen, 2 Sekunden auseinander, und 1 Erfolg und 11 Ablehnungen erhalten:

#1   200   444,123 B
#2   403     1,527 B
#3-12 403   ~1,530 B each

Das Verwerfen des Browsing-Kontexts zwischen Navigationen stellte den 200-Status wieder her, sodass die Ablehnungen aus dem Session-Zustand und nicht aus der Adresse kamen. Später im selben Testzeitraum hörte diese Lösung auf zu funktionieren. Etsy lehnte dann jede Anfrage von dieser Adresse ab, unabhängig davon, wie neu der Kontext war.

Der Risikoscore bewegte sich nicht, während die Erfolgsrate von jeder Anfrage auf keine sank. Der 3-Header-Lauf maß noch immer 0.4822 auf 4 Dezimalstellen, Stunden und mehrere hundert Anfragen nach dem ersten Messwert.

Der X-DataDome-riskscore-Header bewertet 1 Anfrage gleichzeitig, ist also nützlich zum Testen einer Änderung, aber nicht zum Überwachen eines Crawls. Eine Pipeline, die ihn beobachtet, meldet Erfolg, während sie nichts sammelt.

Für das Sammeln von mehr als einigen wenigen Seiten sind 2 Dinge erforderlich: ein neuer Browsing-Kontext und eine Adresse, die DataDome noch nicht abgelehnt hat. Das Verwerfen des Kontexts zwischen Navigationen ist günstig, also beginnen Sie dort, aber diese Lösung hält nur an, bis DataDome die Adresse ablehnt. Residential-Proxys bieten Ihnen einen Pool von Adressen zum Rotieren.

Web Unlocker führt beides aus, und das Skript weiter unten bleibt im kostenlosen Kontingent. Browser API verarbeitet beides in einem gehosteten Browser, den Ihr Playwright-Code steuert.

Was robots.txt und die Etsy-Nutzungsbedingungen nicht erlauben

Lesen Sie Etsys robots.txt selbst, bevor Sie etwas aufbauen, da die Datei die Keyword-Suche verbietet und Etsy sie ohne Ankündigung neu schreibt. Als wir sie lasen, war diese Datei 1.818 Zeilen lang und deklarierte nur 3 User-Agent-Gruppen: *, AdsBot-Google-Mobile und Spinn3r:

User-agent: *
Disallow: /search?*q=
Disallow: /search/?*q=
Disallow: */shop/*/sold*
Disallow: */listing/*/favoriters*
Disallow: /api/
Allow:    /search/shops

Die Wildcard-Gruppe verbietet Keyword-Suchergebnisse in jeder Locale-Variante. Diese Gruppe umfasst jeden Crawler, den die anderen 2 Gruppen nicht namentlich nennen. Auch die Verkaufshistorie und Favoriten-Zahlen sind verboten, und sie gehören zu den klarsten Nachfragesignalen, die Etsy veröffentlicht. Listing-Seiten und Shop-Seiten haben keine Disallow-Regel, sodass beide offen bleiben.

Die Datei deklariert keine Sitemap:-Direktive, und /sitemaps.xml antwortet mit 403 und leerem Body. Diese 2 Abwesenheiten sind genauso wichtig wie die Disallow-Regeln oben. Beide sind 1-Zeilen-Prüfungen, die es wert sind, wiederholt zu werden, da Etsy beides ohne Ankündigung wieder hinzufügen kann. Solange sie fehlen, müssen Sie Listings von den Seiten entdecken, die Etsy offen lässt.

Etsy nannte in der Datei, als wir sie lasen, keinen KI-Crawler und veröffentlichte weder llms.txt noch ai.txt. Diese Abwesenheit ist das Wahrscheinlichste in diesem Abschnitt, das sich seit unserer Prüfung geändert haben könnte.

DataDome lehnt diese Crawler am Edge ab, unabhängig davon. GPTBot und ClaudeBot erhielten beide einen 403-Fehler und erzielten in unserem Test beide 0,9814, höher als die 0,923, die dieselbe Adresse mit einem Chrome User-Agent erzielte. Sinnlose User-Agent-Strings mit denselben Headern erzielten denselben Score, also bewertet DataDome die Abwesenheit eines bekannten Browsers und nicht den Crawler-Namen.

Die Etsy-Nutzungsbedingungen, zuletzt aktualisiert am 26. August 2025, besagen, dass Sie zustimmen, “keine Seite der Dienste zu crawlen, zu scrapen oder zu spidern” ohne ausdrückliche Genehmigung.

Prüfen Sie, ob Ihre Collection-Schicht robots.txt für Sie durchsetzt und zu welchem Zeitpunkt sie entscheidet. Der Residential-Entsperr-Endpunkt entscheidet pro Anfrage. Bei einem Konto ohne abgeschlossene KYC-Überprüfung gibt eine Anfrage für den verbotenen Suchpfad die Regel und das KYC-Formular zurück:

Residential Failed (bad_endpoint): Requested site is not available for immediate
residential (no KYC) access mode in accordance with robots.txt. To get full
residential access for targeting this site, fill in the KYC form:
https://brightdata.com/cp/kyc

Dasselbe Konto rief /shop/AnemoneJewelry ohne Fehler ab, und die Antwort war 983.937 Bytes groß. Der Endpunkt liest dieselbe robots.txt wie Sie, bedient dann die erlaubten Pfade und leitet die verbotenen Pfade zur Compliance-Überprüfung statt zu einem Proxy weiter. Entscheiden Sie, ob Ihr Anwendungsfall die verbotenen Pfade benötigt, bevor Sie mit dem Aufbau beginnen.

Wenn Sie die Fetch-Schicht selbst aufbauen, treffen Sie diese Entscheidung im Code und sind für die robots.txt-Verarbeitung pro Ziel verantwortlich und müssen sie pflegen.

Was die offizielle Etsy-API zurückgibt und was sie weglässt

Die API ist der autorisierte Weg, also prüfen Sie, was sie tut, bevor Sie sie ablehnen. Die Wahl zwischen einer offiziellen API und Web-Scraping ist allgemein, und bei Etsy hängt sie von den Feldern ab, die die Spezifikation weglässt. Wir haben die OpenAPI-Spezifikation direkt abgerufen und 76 Pfade gezählt, von denen 31 GET-Operationen nur einen Anwendungsschlüssel und kein Verkäufer-OAuth benötigen. Diese Gesamtzahlen ändern sich, wenn Etsy einen Endpunkt hinzufügt oder entfernt, also zählen Sie aus dieser Datei neu und nicht aus diesem Absatz.

Gängige Behauptungen über die v3-API sind in 3 Punkten falsch. findAllListingsActive wurde nicht entfernt und akzeptiert keywords, min_price, max_price, taxonomy_id, shop_location, currency und buyer_country, mit einem maximalen limit von 100 pro Aufruf. getReviewsByListing und getReviewsByShop geben Bewertungstext nur mit einem App-Key zurück. getShop gibt transaction_sold_count, review_count, review_average, num_favorers und listing_active_count für jeden Shop zurück.

Die Spezifikation enthält 1 Nachfragefeld und lässt den Rest weg. getListing benötigt nur einen Anwendungsschlüssel und gibt views zurück, eine kumulative Aufrufsanzahl, die einmal täglich aktualisiert wird, im ShopListingWithAssociations-Schema. Eine Suche im gesamten Dokument ergibt kein Feld für Suchvolumen, Impressionen, Konversionsrate oder Verkäufe pro Listing. Das ShopListing-Schema hatte 50 Eigenschaften, als wir zählten, einschließlich num_favorers, quantity und price, und keine davon war eine Verkaufsanzahl. Überprüfen Sie die Zahl anhand der Spezifikation, aber bisher ist kein Verkaufsfeld erschienen.

Transaktionen pro Listing haben zwar einen Endpunkt, getShopReceiptTransactionsByListing, aber er benötigt den transactions_r-OAuth-Scope, den nur ein Shop-Inhaber für seinen eigenen Shop gewähren kann. Für jeden Shop, den Sie nicht betreiben, veröffentlicht Etsy Lifetime-Verkäufe auf Shop-Ebene als aktuelle Gesamtsumme ohne Historie und nichts pro Listing.

Diese Lücke erklärt den Drittanbieter-Tool-Markt rund um Etsy. Einige Produkte verkaufen Verkaufsschätzungen pro Listing oder Keyword-Suchvolumen für Shops, die sie nicht betreiben. Nichts in der Spezifikation, die wir gelesen haben, gibt diese Zahlen für einen Shop zurück, den Sie nicht besitzen, also müssen diese Daten von außerhalb dieser API stammen.

Etsy setzt Rate-Limits pro Anwendungsschlüssel, pro Sekunde und pro Tag durch. Die x-limit-per-second– und x-limit-per-day-Response-Header melden beides, und Etsy gibt einen 429 mit einem retry-after zurück, wenn Sie eines überschreiten.

Die Rate-Limits-Seite kennzeichnet ihre Zahlen als “Beispielwert” und nicht als Standards. Das ältere Paar “10.000 pro Tag, 10 pro Sekunde” war von dieser Seite verschwunden, als wir sie lasen. Lesen Sie Ihre eigenen Limits aus dem Developer Portal.

Etsy-Listing- und Shop-Seiten mit Python scrapen

Etsy-Listing-Seiten betten schema.org JSON-LD ein, sodass der Extraktionsschritt keine CSS-Selektoren benötigt und nicht bricht, wenn Etsy das Markup drum herum neu gestaltet. Der Fetch-Schritt benötigt Infrastruktur, und Sie haben 2 Möglichkeiten. Eine entsperrte API läuft überall und benötigt ein Konto. Ein lokaler Browser benötigt kein Konto und eignet sich für einige wenige Seiten.

Das folgende Skript benötigt 1 Bibliothek und 2 Umgebungsvariablen:

python3 -m venv .venv && source .venv/bin/activate
pip install requests
export BRIGHTDATA_API_KEY="your-api-token"
export BRIGHTDATA_ZONE="web_unlocker"

Das Token stammt aus dem Bright Data Control Panel. Jede Anfrage mit diesem Token verbraucht Ihr Kontoguthaben, also halten Sie es in einer Umgebungsvariablen statt in einem committeten Skript.

Sie erstellen den zweiten Wert unter Web Access → Add API → Web Unlocker API. Die Anfrage-Payload nennt es eine Zone, aber das Control Panel bezeichnete es als API, als wir das einrichteten, daher fand eine Suche im Panel nach “Zone” nichts. BRIGHTDATA_ZONE muss mit dem Namen übereinstimmen, den Sie dort eingeben, da das Skript standardmäßig web_unlocker verwendet. Das Formular warnt, dass der Name dauerhaft ist:

Das Bright Data Control Panel im Abschnitt Web Access, Breadcrumb Web Access dann Add API, bei Schritt 2 von vier: API-Typ wählen, API konfigurieren, Zahlungsmethode hinzufügen, API testen. Der API-Typ lautet Web Unlocker API, berechnet nur für erfolgreiche Anfragen. Ein erforderliches Namensfeld enthält web_unlocker_test über einem Hinweis, der lautet: 'Dieser Name kann später nicht geändert werden.' Ein Panel auf der rechten Seite zeigt den aktuellen Plan als Pay-as-you-go zu einem als CPM angegebenen Preis

Die Zone in dieser Aufnahme heißt web_unlocker_test, also würde die Standardeinstellung des Skripts sie verfehlen. Setzen Sie entweder BRIGHTDATA_ZONE auf den Namen, den Sie eingegeben haben, oder verwenden Sie web_unlocker und lassen Sie den Standard in Ruhe. Der Web Unlocker Quickstart dokumentiert beides, und wir haben ohne Karte mit dem kostenlosen Kontingent begonnen, als wir das einrichteten. Die Web Unlocker-Preisseite listet das aktuelle kostenlose Kontingent und den Pay-as-you-go-Preis darüber auf, angegeben pro 1K erfolgreiche Anfragen. Das Panel schreibt diesen Preis als CPM.

Dieses Skript sendet die Anfrage durch den Web Unlocker-Endpunkt und parst das Ergebnis:

import json
import os
import re
import requests

API_KEY = os.environ.get("BRIGHTDATA_API_KEY")
ZONE = os.environ.get("BRIGHTDATA_ZONE", "web_unlocker")
ENDPOINT = "https://api.brightdata.com/request"

LD_JSON = re.compile(
    r'<script[^>]*type\s*=\s*[\'"]application/ld\+json[\'"][^>]*>(.*?)</script\s*>',
    re.S | re.I,
)


def fetch_html(url, country="us"):
    """Return the rendered HTML for an Etsy URL, or raise on failure."""
    # Checked here rather than at import, so the browser path below runs
    # without an account.
    if not API_KEY:
        raise SystemExit("BRIGHTDATA_API_KEY is not set, see the exports above")
    response = requests.post(
        ENDPOINT,
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"zone": ZONE, "url": url, "format": "raw", "country": country},
        timeout=90,
    )
    response.raise_for_status()
    body = response.text
    # A quota error arrives as HTTP 200 with a short text body, and a block page
    # arrives as HTTP 200 of valid HTML. Neither contains ld+json, which is the
    # content this actually wants, so test for that rather than for either error.
    if not LD_JSON.search(body):
        raise RuntimeError(f"no ld+json in response, most likely blocked: {body[:200]!r}")
    return body


def product_jsonld(html):
    """Pick the Product block by @type. Every listing we opened had four."""
    for block in LD_JSON.findall(html):
        try:
            parsed = json.loads(block.strip())
        except json.JSONDecodeError:
            continue
        for node in parsed if isinstance(parsed, list) else [parsed]:
            if node.get("@type") == "Product":
                return node
    return None


def parse_listing(node):
    """Flatten a Product node, keeping the offer's range rather than its lowest price."""
    offer = node.get("offers", {})
    # schema.org allows a list of offers and a single priceSpecification object,
    # and Etsy serves both, so normalize before indexing into them.
    if isinstance(offer, list):
        offer = offer[0] if offer else {}
    specs = offer.get("priceSpecification", [])
    if isinstance(specs, dict):
        specs = [specs]
    base = next((s for s in specs if "priceType" not in s), {})
    was = next(
        (s for s in specs if "Strikethrough" in str(s.get("priceType", ""))), {}
    )
    # Not every listing carries a priceSpecification. Without this fallback a
    # single-variant listing records a null price and raises nothing.
    # A range arrives three ways: nested in priceSpecification, as AggregateOffer's
    # own lowPrice and highPrice, or not at all. Try them in that order.
    low = base.get("minPrice") or offer.get("lowPrice") or offer.get("price")
    high = base.get("maxPrice") or offer.get("highPrice") or offer.get("price")
    # availability is the only field that marks a dead listing, and JSON-LD lets it
    # arrive as a bare term, an array, or an @id object. Normalize before comparing.
    avail = offer.get("availability") or ""
    if isinstance(avail, list):
        avail = avail[0] if avail else ""
    if isinstance(avail, dict):
        avail = avail.get("@id", "")
    rating = node.get("aggregateRating", {})
    return {
        "sku": node.get("sku"),
        "title": node.get("name"),
        # Etsy serves a dead listing as a full HTTP 200 page that still
        # carries a price, so availability is the only field that says so.
        "availability": str(avail).rsplit("/", 1)[-1],
        "currency": offer.get("priceCurrency"),
        "price_min": low,
        # high can be a bundle maximum rather than the item's, where a listing
        # has an add-on axis. Count the axes before trusting it as a maximum.
        "price_max": high,
        "list_price": was.get("price"),
        # True only where the variations differ in price. Same-price variants
        # read false, so this is a price-spread test, not a variation test.
        "has_variations": None if low is None else low != high,
        "rating": rating.get("ratingValue"),
        "review_count": rating.get("reviewCount"),
        "shop": node.get("brand", {}).get("name"),
    }


if __name__ == "__main__":
    url = (
        "https://www.etsy.com/listing/753913297/"
        "smoky-quartz-ring-rose-gold-ring-women"
    )
    node = product_jsonld(fetch_html(url, country="us"))
    if node is None:
        raise SystemExit("no Product block on page: blocked, or the layout changed")
    print(json.dumps(parse_listing(node), indent=2))

Wir haben das Skript gegen ein Live-Listing ausgeführt, und es gab einen flachen Datensatz zurück. Der Preisbereich stammt von 1 Listing mit vielen separat bepreisten Variationen:

{
  "sku": "753913297",
  "title": "Smoky Quartz Ring · Rose Gold Ring Women · Cocktail Rings · ...",
  "availability": "InStock",
  "currency": "USD",
  "price_min": "89.25",
  "price_max": "5613.75",
  "list_price": "119.00",
  "has_variations": true,
  "rating": "4.5",
  "review_count": 99,
  "shop": "AnemoneJewelry"
}

Über 3 Läufe gegen dasselbe Listing dauerten Fetches 23 bis 27 Sekunden und gaben zwischen 540 KB und 750 KB zurück. Diese Zahlen sind die Kosten des Entsperrschritts. Planen Sie für Latenz in diesem Bereich und nicht für das Sub-Sekunden-Timing eines Blocks.

Wenn Sie nur einige wenige Seiten benötigen und lieber kein Konto eröffnen möchten, erreicht ein lokaler Browser dasselbe Ergebnis mit etwa demselben Codeaufwand. Dieser Browser läuft von einer Adresse aus, die DataDome noch nicht abgelehnt hat. Er benötigt ein Display, also führen Sie ihn auf einem Server headed unter einem virtuellen Display wie Xvfb aus statt im headless-Modus. Installieren Sie Playwright und Chromium einmal:

python3 -m venv .venv && source .venv/bin/activate   # skip if already active
pip install playwright requests
playwright install chromium

Dann tauschen Sie den Fetch aus und behalten dieselben 2 Parsing-Funktionen. Fügen Sie dies über dem __main__-Block ein, zusammen mit den anderen Funktionen:

from playwright.sync_api import sync_playwright


def fetch_html_browser(url):
    """Fetch one page with a visible browser, since headless returns a 403."""
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        context = browser.new_context(locale="en-US")
        page = context.new_page()
        page.goto(url, wait_until="domcontentloaded", timeout=45000)
        html = page.content()
        browser.close()
        # Same rule as the guard above: test for the content you want. A block
        # page is valid HTML, so only the missing ld+json shows it.
        if not LD_JSON.search(html):
            raise RuntimeError("no ld+json on page, most likely a block page")
        return html

Ändern Sie 1 Zeile im __main__-Block und sonst nichts:

node = product_jsonld(fetch_html_browser(url))   # was: fetch_html(url, country="us")

Unser Lauf gab 535.247 Bytes zurück und wurde durch dieselben 2 Funktionen sauber geparst. Die Seite war auch in INR bepreist, weil ein Browser auf Ihrer Maschine von Ihrer eigenen Adresse ausgeht, und der Browser-Pfad nimmt kein country-Argument. Ohne dieses Argument können Sie das Exit-Land nicht festlegen, sodass dieser Pfad für einige wenige Seiten geeignet ist und nicht für einen Datensatz.

Der obige Code parst die Seite, anstatt sie an ein Modell zu senden. Etsy veröffentlicht die Felder als strukturierte Daten, sodass das Lesen deterministisch und praktisch kostenlos ist. Bei dem Listing, das wir gemessen haben, dauert die Auswahl des Product-Blocks und seine Vereinfachung einen Median von 0,3 Millisekunden.

Das Übergeben derselben Seite an ein Sprachmodell bedeutet dagegen 179.599 Tokens rohen HTMLs, das ist fast ein 200K-Token-Kontextfenster für 1 Produkt. Das Entfernen von Markup reduziert das auf 5.352 Tokens, eine Reduktion um 97 %. Dieses Verhältnis ist wichtiger als die Wahl des Modells.

Aber ein Parser, der aufhört zu matchen, gibt ein leeres Feld statt eines Fehlers zurück, während ein Modell zumindest etwas Falsches und Sichtbares produzieren würde. Nehmen Sie also den deterministischen Weg auf einer Site, die schema.org veröffentlicht. Nutzen Sie die gesparte Zeit, um zu überprüfen, ob der Parser noch die erwarteten Felder zurückgibt.

Shop-Seiten haben ebenfalls 4 dieser Blöcke, unter verschiedenen Typen, und einer davon zeigt, woher Listing-URLs kommen. /shop/{shop_name} gibt einen Organization-Knoten zurück, der den Shop beschreibt, und einen ItemList-Knoten, dessen itemListElement vollständige Listing-URLs enthält. shop_itemlist wählt die ItemList nach @type aus, sodass dieselbe Funktion mit Organization an ihrer Stelle die eigenen Felder des Shops zurückgibt. Bei dem Shop, den wir testeten, las numberOfItems 1.842, während eine einzelne Seite 36 URLs zurückgab, und ?page=2 gab 36 weitere ohne Überschneidungen zurück.

Der Enumerator nimmt einen Shop-Namen als Eingabe, also benötigen Sie eine Quelle für Shop-Namen, und 3 Quellen funktionieren, ohne den verbotenen Suchpfad zu berühren. Sie können Shops verwenden, die Sie bereits verfolgen, den oben genannten findAllListingsActive-Endpunkt oder einen vorbereiteten Datensatz. Dieser Endpunkt nimmt keywords mit einem Anwendungsschlüssel allein und enthält eine shop_id für jedes zurückgegebene Listing.

Wir haben Seiten über den Bereich und über das deklarierte Ende hinaus abgerufen:

page  2    36 items
page 25    36 items
page 51    36 items
page 52     6 items      51 x 36 + 6 = 1,842
page 53    no ItemList block
page 60    no ItemList block

Die Gesamtzahl entspricht den 1.842, die der Shop deklariert. Über das Ende hinaus antwortet Etsy weiterhin mit einer vollständigen Seite von etwa 420 KB ohne ItemList.

Etsy gibt keinen Fehler und kein leeres Array zurück, auf das man warten könnte, sodass eine Schleife, die auf eines von beiden wartet, ewig gegen Seiten paginiert, die gut aussehen. Beenden Sie stattdessen beim fehlenden Block und lassen Sie die deklarierte Anzahl Ihre Arbeit überprüfen. Die üblichen Muster für paginierte Sammlung setzen eines dieser 2 Signale voraus, also gelten sie hier nicht.

Beide Funktionen gehören in dieselbe Datei wie die früheren Funktionen, da sie LD_JSON und fetch_html verwenden. Kommentieren Sie den Einstiegspunkt aus, den Sie nicht ausführen, da der Enumerator 20 Minuten dauert:

def shop_itemlist(html):
    """Pick the ItemList block. The first block on a shop page is a video."""
    for block in LD_JSON.findall(html):
        try:
            parsed = json.loads(block.strip())
        except json.JSONDecodeError:
            continue
        for node in parsed if isinstance(parsed, list) else [parsed]:
            if node.get("@type") == "ItemList":
                return node
    return None


def enumerate_shop(shop_name, country="us"):
    """Page a shop until the ItemList stops appearing, and hand back the declared
    count so the caller can check it."""
    base = f"https://www.etsy.com/shop/{shop_name}"
    urls, declared, page = [], None, 1
    while True:
        suffix = "" if page == 1 else f"?page={page}"
        try:
            node = shop_itemlist(fetch_html(base + suffix, country=country))
            if node is None:
                break
            declared = node.get("numberOfItems", declared)
            urls += [item["item"]["url"] for item in node["itemListElement"]]
        except (RuntimeError, requests.RequestException, KeyError, TypeError,
            AttributeError) as err:
            print(f"stopped at page {page}: {err}", flush=True)
            break
        print(f"page {page}: {len(urls)} of {declared}", flush=True)
        # Breaking on the declared count here would make the comparison below
        # vacuous, so page until the ItemList block stops appearing.
        page += 1
    return urls, declared


if __name__ == "__main__":
    urls, declared = enumerate_shop("AnemoneJewelry")
    print(len(urls), "collected,", declared, "declared")

Der Enumerator macht 53 Fetches gegen die oben gemessenen Seiten, gibt 1.842 URLs zurück und überprüft die Gesamtzahl gegen die vom Shop deklarierte Anzahl. Vergleichen Sie diese 2 Zahlen bei jedem Lauf, da ein kurzer Lauf weniger URLs zurückgibt und keinen Fehler auslöst.

Das try muss sowohl den Parse als auch den Fetch abdecken. Eine 53-Anfragen-Schleife ist lang genug, um die Rate-Limit-Antwort zu erhalten, und ein einzelnes fehlerhaftes itemListElement verursacht denselben Schaden wie eine fehlgeschlagene Anfrage. Ohne den Guard löst eines von beiden bei Seite 30 einen Fehler aus und verwirft alle bisher gesammelten URLs. Das Abfangen beider führt stattdessen zu einem kurzen Lauf, und der Anzahlvergleich zeigt es. Das Behandeln fehlgeschlagener Anfragen in Python folgt einem Standardmuster, und der einzige Etsy-spezifische Teil ist das Einbetten des Parse in den Guard.

Bei der oben gemessenen Latenz sind 53 Fetches 20 bis 24 Minuten und 53 Anfragen aus dem kostenlosen monatlichen Kontingent für 1 Shop. Führen Sie den Enumerator zuerst auf einem kleinen Shop aus und beobachten Sie die zunehmende Seitenanzahl, bevor Sie ihn auf einem Shop mit 1.842 Listings verwenden. Legen Sie auch ein Nutzungslimit auf der Zone fest, damit ein fehlgeschlagener Lauf bei einem Limit stoppt statt bei Ihrem Guthaben.

Wir haben dieselbe Entdeckung über den Etsy-Shop-Endpunkt der Web Scraper API als Batch-Job ausgeführt. Wir haben den Job auf dem Shop und nicht auf Listing-URLs ausgeführt und ihn auf 50 Datensätze für den Vergleich begrenzt. Er gab 50 Listings in 168,9 Sekunden zurück, also etwa 3,4 Sekunden pro Datensatz, ohne Duplikate und ohne locale-präfixierte URLs in diesem Lauf.

Das Sammeln dieses ganzen Shops dauert 53 Enumerations-Fetches plus 1.842 Listing-Fetches, also 1.895 Anfragen und etwa 13 Stunden single-threaded. Prüfen Sie diese 1.895 gegen das aktuelle kostenlose Kontingent, bevor Sie beginnen. Über dieses Kontingent hinaus wird dieselbe Arbeit zum Pay-as-you-go-Preis pro 1K erfolgreicher Anfragen abgerechnet.

Fixieren Sie country auch hier. Wir haben denselben Shop ohne es abgerufen und /de/listing/-URLs und deutsche Titel erhalten. Diese URLs würden als verschiedene Schlüssel für bereits unter ihrer /listing/-Form gespeicherte Elemente in einen Datensatz eingehen.

Extraktionsprobleme, die einen Etsy-Datensatz unbemerkt beschädigen

Jedes Problem hier gibt HTTP 200 zurück und löst keine Exception aus. Von den 5 schreiben 4 eine plausible, aber falsche Zeile und die fünfte schreibt eine leere Zeile. Stattdessen findet man Monate später eine falsche Zahl. Datenqualitätsmetriken erkennen diese Art von Fehler, nachdem die Daten gespeichert sind, und das Nicht-Schreiben der schlechten Zeile ist günstiger.

Eine Listing-Seite hat 4 JSON-LD-Blöcke, nicht 1. Etsy enthielt Product, VideoObject, BreadcrumbList und FAQPage in 4 separaten Script-Tags auf jedem Listing, das wir öffneten, alle mit dem application/ld+json-Typ. Die Seitenquelle zeigt sie in 4 aufeinanderfolgenden Zeilen:

Vier aufeinanderfolgende Zeilen der Listing-Seitenquelle, nummeriert 144 bis 147, jede ein Script-Tag vom Typ application/ld+json. Ihre @type-Werte in der Reihenfolge sind Product, VideoObject, BreadcrumbList und FAQPage

Code, der find() aufruft und das erste Ergebnis nimmt, funktioniert auf Listing-Seiten und gibt auf Shop-Seiten unbemerkt den Video-Block zurück. Wählen Sie nach @type statt nach Position. Nichts anderes an der Mechanik des Parsens von JSON in Python ändert sich.

Eine Shop-Seite hat 4 eigene Blöcke in einer anderen Reihenfolge:

Chromes Suchleiste auf der Shop-Seitenquelle mit der Aufschrift 'application/ld+json' und einem Zähler von 1 von 4, über den vier übereinstimmenden Script-Tags. Ihre @type-Werte in der Dokumentreihenfolge sind VideoObject, FAQPage, Organization und ItemList

Bei dem Shop, den wir getestet haben, ist der erste Block ein Video und die gewünschte ItemList ist der letzte, sodass das Nehmen des ersten Ergebnisses fehlschlägt. Das Skript oben wählt stattdessen nach @type aus.

Bei einem Listing mit Variationen ist offers.price der niedrigste Preis im Bereich, nicht der gesamte Bereich. Bei dem Listing, das wir testeten, las offers.price 89.25, während der in offers verschachtelte priceSpecification-Eintrag minPrice 89.25 und maxPrice 5613.75 deklarierte. Ein Parser, der offers.price liest, zeichnet die günstigste Variation auf und lässt den Bereich fallen. Das Speichern von maxPrice stattdessen ist auch keine Lösung, da maxPrice bei diesem Listing der Preis eines Bundles und nicht des Artikels allein ist.

Wir haben dieselbe URL mit aktivierten Variationen gesammelt und 61 separate Datensätze erhalten, 1 pro Variante, bepreist von 89.25 bis 1.871.25. Eine einzelne Listing-Zeile in Ihrer Tabelle repräsentiert 61 kaufbare SKUs über einen 21-fachen Preisbereich.

Ein zweites Dropdown auf der Seite multipliziert diesen Bereich:

Das Dropdown 'Add Matching Jewelry' auf dem Listing geöffnet, zeigt vier Optionen, bepreist als Vielfache des Rings: 'No Thanks' $89.25 bis $1.871.25, entweder einzelnes passendes Stück $178.50 bis $3.742.50 und 'Full Set Earrings plus Pendant' $267.75 bis $5.613.75. Ein Metal Type-Dropdown sitzt darüber und der Überschriftspreis lautet $89.25 plus mit $119.00 durchgestrichen

Diese 2 Maxima messen verschiedene Dinge, und dieses Listing hat 2 Variationsachsen. Metal Type reicht von 14k Gold Filled bei 89.25 bis zu den 3 Massivgold-Optionen bei 1.871.25. Add Matching Jewelry (Optional) multipliziert dann diesen Bereich. No Thanks läuft von 89.25 bis 1.871.25, entweder einzelnes passendes Stück von 178.50 bis 3.742.50 und Full Set: Earrings + Pendant von 267.75 bis 5.613.75.

Der deklarierte maxPrice ist der Massivgold-Ring plus beide passenden Stücke, 3 Artikel zu 1 Preis. Der Variations-Extract gab den Ring allein zurück und stimmte mit der No Thanks-Zeile auf den Cent genau überein.

Immer wenn ein Listing also eine Add-on-Achse hat, ist maxPrice das Maximum für ein Bundle und nicht für den Artikel. Eine darauf aufgebaute Preisreihe verfolgt stillschweigend Bundles. Zählen Sie die Variationsachsen, bevor Sie einen Bereich als den eigenen des Produkts speichern. Sie sind nicht im JSON-LD, also lesen Sie sie von der gerenderten Seite oder rufen Sie den Variations-Extract ab.

Dasselbe priceSpecification-Array enthält auch einen zweiten Eintrag, der als StrikethroughPrice markiert ist, sodass der Aktionspreis und der Listenpreis nebeneinander erscheinen, unterschieden nur durch eine schema.org-URL. Dieser Eintrag hat sein eigenes minPrice und maxPrice. Der list_price, den das Skript aufzeichnet, ist der niedrigste Preis im Listenpreisbereich, genau wie offers.price der niedrigste im aktuellen Bereich ist.

Locale folgt der Exit-IP, und es ändert sich mehr als das Währungssymbol. Wir haben 1 Listing 3 Mal in derselben Stunde abgerufen und dabei nur das Exit-Land geändert:

country   currency   price    variation range      title
us        USD        89.25    89.25 - 5613.75      Smoky Quartz Ring, Rose Gold Ring Women
de        EUR        95.71    95.71 - 5058.76      Rauchquarzring, Damenring aus Roségold
gb        GBP        82.77    82.77 - 4338.22      Smoky Quartz Ring, Rose Gold Ring Women

Dasselbe Listing rendert von jedem Exit-Land unterschiedlich:

Dasselbe Etsy-Listing von drei Exit-Ländern bepreist: Der US-Exit zeigt $89.25 mit $119.00 durchgestrichen, der deutsche Exit zeigt ab 95,88 EUR mit ab 127,84 EUR durchgestrichen, und der UK-Exit zeigt GBP 82.86 mit GBP 110.49 durchgestrichen und 25% Rabatt

Diese Aufnahmen stammen von einem späteren Fetch als die 3 Zeilen, und nur die Dollar-Zahl blieb gleich. Die deutsche Seite schreibt auch ihren niedrigsten Preis als ab, und das + markiert dasselbe auf den anderen 2 Seiten.

Die 3 Fetches gaben dieselbe SKU, Bewertung und Bewertungsanzahl zurück, zu 3 verschiedenen Preisen in 3 Währungen. Das Verhältnis zwischen dem niedrigsten und höchsten Preis unterscheidet sich in den 3 Zeilen, 62,9-fach bei der US-Zeile gegen 52,9-fach und 52,4-fach, also bewegte sich zwischen diesen Fetches mehr als der Wechselkurs. Der deutsche Exit gab auch einen maschinell übersetzten Titel zurück, während die 2 englischen Locales den Original behielten.

Ein rotierender Pool, der Geografie ignoriert, erzeugt eine Preisreihe, die 3 Währungen mischt, und ein Textkorpus, der Sprachen mischt, und nichts in der Pipeline meldet ein Problem. Fixieren Sie das Exit-Land pro Sammlungslauf und speichern Sie die Währung neben jedem Preis.

Dasselbe Problem tritt in vorbereiteten Daten auf. Das von uns heruntergeladene 1.000-Datensatz-Sample enthielt 19 Währungen, mit 109 Zeilen in einer anderen Währung als USD, davon 14 in vietnamesischem Dong. Dieses Sample aktualisiert sich, also wird Ihre Kopie von diesen Gesamtzahlen abweichen. Führen Sie denselben Vergleich mit Ihrer eigenen Kopie durch.

Das Mitteln der Preisspalte ohne Lesen der Währungsspalte bläht den Mittelwert auf:

mean(final_price), all rows          31,238.84   n=991
mean(final_price), currency = USD       137.57   n=882
                                        ~227x
median(final_price), all rows            26.00

In dieser Spalte tragen 14 von 991 Zeilen 87% der Gesamtsumme bei, aber 109 Zeilen sind Nicht-USD und alle müssen behandelt werden. Die Abfrage läuft, die Spalte ist ein sauberer Float, und die Antwort ist um 2 Größenordnungen falsch. Der Median bewegt sich kaum, da die Nicht-USD-Zeilen nur einen kleinen Anteil der Spalte ausmachen. Ein Mittelwert und ein Median, die so stark voneinander abweichen, zeigen nur, dass ein schwerer Schwanz vorhanden ist. Das Gruppieren nach der Währungsspalte trennt gemischte Einheiten von gewöhnlicher Schiefe.

Ein arXiv-Papier vom März 2026 über Marketplace-Listings, das weiter unten erneut zitiert wird, beschränkte sein Sample auf Listings in USD, bevor es Analysen durchführte. Das löst das Problem nach der Erfassung und nicht während ihr. Das Filtern auf diese Weise ändert auch die Population, die der Durchschnitt beschreibt, da es Listings in anderen Währungen herausfiltert statt sie umzurechnen.

Dieses Problem ist auf dem Agent-Pfad am schwierigsten zu erkennen. Wir haben dieses Listing über einen MCP-Server abgerufen, dessen Schema nur eine URL akzeptiert, und den tschechischen Storefront in CZK erhalten. Der auf ein Land fixierte REST-Aufruf meldet dasselbe Listing zu 89.25 USD.

Die Ursache ist die Schnittstelle des Tools, nicht der Fetch. Ohne Land- oder Locale-Parameter zum Setzen nehmen Sie, welchen Exit der Server gerade verwendet. Prüfen Sie dieses Verhalten bei jedem solchen Server, bevor Sie ihn mit einem Agent verbinden, da der Exit die Währung jeder nachgelagerten Antwort bestimmt.

Also sammeln Sie Etsy-Daten in Ihrem eigenen Speicher auf einem fixierten Locale, und lassen Sie den Agent diesen Speicher lesen statt live pro Frage zu fetchen. Ein Agent, der jedes Mal stillschweigend in einer anderen Währung antwortet, ist schlechter als ein Agent, der nicht antworten kann.

Ein totes Listing ist eine vollständige Seite mit einem Preis darauf. Etsy gibt keinen 404-Fehler für ein abgelaufenes oder ausverkauftes Listing zurück. Wir haben ein Listing aus dem Jahr 2007 abgerufen und HTTP 200, 465.563 Bytes, einen vollständigen Product-Block und price 13.00 USD erhalten.

Die Seite rendert vollständig und zeigt sowohl das Ausverkauft-Banner als auch den Preis:

Eine Etsy-Listing-Seite. Ein Banner oben lautet 'This item is sold out.' Direkt daneben zeigt die Seite weiterhin einen Preis von $13.00, zusammen mit den Produktfotos, dem Verkäufernamen und den Artikeldetails, genau wie ein Live-Listing

offers.availability ist das einzige Feld, das dem Preis widerspricht, und es lautet schema.org/OutOfStock. Ein Parser, der es überspringt, zeichnet einen toten Artikel zum vollen Preis auf, also liest das Skript oben dieses Feld.

Dieses Feld ist in vorbereiteten Daten weniger zuverlässig als auf der Seite. In derselben Kopie waren 74 von 1.000 Zeilen ausverkaufte Listings, availability war bei allen 74 leer, und 65 zeigten noch einen numerischen Preis. Das Zeichen dort ist ein show_sold_out_detail-Parameter in der gespeicherten URL. Nur die Zeilen zu behalten, deren availability InStock lautet, würde 798 Live-Zeilen verwerfen, da das Feld auch bei den meisten von ihnen fehlt.

Eine 2xx-Antwort bedeutet nicht, dass Sie Daten haben. Dieses Problem liegt in der Collection-Schicht statt in der Payload, und wir haben es mit dem Skript oben während des Testens gesehen. Der Entsperr-Endpunkt erklärt sich im Body. Als das Konto ein Anfrage-Rate-Limit erreichte, antwortete es mit 200 und einem 111-Byte-Klartext-Body mit dem Inhalt Your system is sending too many of this type of request. Die Statuszeile bleibt ein Erfolg, also passiert raise_for_status(), der Parser findet keinen Product-Block, und eine Schleife schreibt leere Zeilen ohne eine einzige Exception.

Die Web Scraper API behandelt denselben Fall by Design. Ihr synchroner Collect-Endpunkt gibt Datensätze innerhalb von 1 Minute zurück, und längere Jobs laufen asynchron weiter. Derselbe Endpunkt antwortet mit 202 und einer snapshot_id sowie einem retry-after, wenn ein Job über diese Minute hinausgeht, sodass Sie das Ergebnis abrufen können, sobald es fertig ist, wie die API-Referenz dokumentiert. Beide Antworten sind Erfolgs-Statuscodes, also verzweigen Sie vor dem Indexieren in eine Datensatzliste auf den Statuscode.

Der Guard in fetch_html ist 2 Zeilen und testet auf eine Seite mit ld+json statt auf einen der beiden Fehler. Das Ausführen des Enumerators gegen einen Live-Shop fand einen dritten Fall, einen 200 mit leerem Body, und derselbe Guard fing ihn ohne jede Änderung ab. Testen Sie also die Antwort auf den gewünschten Inhalt statt gegen die bereits gesehenen Fehler, da Anbieter-Fehlertext keine stabile Schnittstelle ist. Schreiben Sie das Äquivalent für jede Fetch-Schicht, die Sie verwenden, denn ein Erfolgs-Statuscode garantiert keine Payload.

Ein gearbeitetes Beispiel beweist, dass ein Problem existiert, nicht dass es häufig ist. Wir haben die Preis-Bereich- und Durchstreichungsprobleme gegen 100 Listings aus beiden Entdeckungspfaden geprüft. Variationen sind bei 98 von 100 vorhanden, und initial_price unterscheidet sich bei denselben 98 von final_price. Keines der beiden Probleme ist ein Randfall, den man aufschieben kann. Diese 100 kamen über die 2 oben genannten Entdeckungspfade und nicht als Zufallsstichprobe aus Etsys Kategorien. Lesen Sie also 98 als Minimum für Schmuck-Listings statt als Rate für die gesamte Site.

Dieselbe Prüfung zeigt, dass das oben verwendete Listing in einer Hinsicht ungewöhnlich ist. Dieses Listing hat 99 Bewertungen, während der Median in diesem Crawl 1 und im veröffentlichten Datensatz-Sample 0 ist.

Wann man einen verwalteten Datensatz kauft statt einen Scraper zu pflegen

Sie können den Scraper aufbauen, und der obige Code ist der größte Teil davon. Die Entscheidung ist, welche Fehler Sie selbst behandeln möchten.

Wir haben dasselbe Listing erneut durch die Web Scraper API geführt, diesmal nach URL statt nach Shop. Sie gab 58 Felder zurück, gegenüber den 14 im rohen Product-Block, und die zusätzlichen Felder behandeln die meisten der oben genannten Probleme:

raw JSON-LD (geo=us)          Web Scraper API
-----------------------------------------------------
price 89.25 (range minimum)   final_price 89.25
119.00 (StrikethroughPrice)   initial_price 119
not present                   discount_percentage 25
not present                   listing_has_variations true
not present                   reviews_count_shop 15129
not present                   is_star_seller false
4 embedded reviews            6 top_reviews
14 fields                     58 fields

Sie konfigurieren diesen Extraktor pro Eingabe-URL und setzen all_variations auf true für das Preis-Bereich-Problem:

Die Bright Data Scraper-Bibliothek auf dem etsy.com-Scraper, überschrieben POST Etsy - collect by URL zu einem Preis pro tausend Datensätze. Eine Endpunktliste bietet collect by URL, discover by keywords und discover by shop url. Die Eingabetabelle enthält eine Zeile: eine Listing-URL mit all_variations auf true gesetzt. Eine Scraper-Modusauswahl bietet synchron, ausgewählt, oder asynchron. Ein Code-Panel zeigt die generierte authentifizierte Anfrage, die api.brightdata.com/datasets/v3/scrape aufruft

Die 6 Felder, die wir über beide Pfade verglichen, stimmten exakt überein, und zwar Währung, berechneter Preis, Listenpreis, Bewertung, Artikel-Bewertungsanzahl und Versandursprung. Führen Sie diese Prüfung durch, bevor Sie einem der beiden Pfade vertrauen.

Der Zeitvergleich kehrt sich um, und der Modus entscheidet, wie stark. Über den synchronen Endpunkt dauerte die strukturierte Extraktion etwa 50 Sekunden für einen einzelnen Datensatz, gegenüber 27 Sekunden für den rohen Fetch. Er rendert und normalisiert statt Bytes zurückzugeben. Die 50 Sekunden lassen etwa 10 Sekunden innerhalb des obigen 1-Minuten-Timeouts. Der Endpunkt ist für 1 URL und eine sofortige Antwort ausgelegt.

Wir haben denselben Scraper stattdessen als Batch-Job ausgeführt und die obigen 50 Datensätze zu 3,4 Sekunden pro Datensatz erhalten. Jeder Datensatz kostet etwa 1/8 der 27 Sekunden, die ein roher Fetch dauert. Im Batch-Modus ist der 202 die erwartete Antwort und kein zu behandelnder Fehler.

Für einen wiederkehrenden Crawl entfernt der strukturierte Pfad Selektor-Wartung, Locale-Fixierung und Preis-Bereich-Behandlung von Ihrem Team für die zurückgegebenen Felder. Für 1 Listing ist es in jedem Modus langsamer. Der obige Diff zeigt die Preisbehandlung gelöst, und der 50-Datensatz-Lauf zeigte keine locale-präfixierten URLs. Selektor-Wartung ist der einzige Teil, den kein einzelner Lauf testen kann. Der strukturierte Pfad wird auf dieselbe Weise wie Web Unlocker abgerechnet, mit dem aktuellen kostenlosen Kontingent und dem Per-1K-Datensatz-Preis auf der Web Scraper API-Preisseite.

Das gleiche Sample enthält 2 Arten von Datensatz statt 1, also planen Sie für beide auf der Kaufseite. Von den 1.000 Zeilen haben 128 den vollständigen Feldsatz, und die anderen 872 lassen 12 seiner Felder leer, einschließlich description, product_category und store_country.

Die Aufteilung verfolgt das Listing-Alter, mit dem vollständigen Datensatz bei Artikeln, die ab Ende 2025 gelistet wurden. Ein Bulk-Extract über Jahre mischt daher beide Arten in 1 Datei. Dieses Verhältnis sollte sich zum vollständigen Datensatz hin verschieben, wenn das Korpus altert. Prüfen Sie die Feldabdeckung gegen Ihre eigenen erforderlichen Spalten, bevor Sie die Bestellgröße festlegen, nicht danach.

Das Bewertungsvolumen benötigt dieselbe Prüfung, bevor Sie bestellen. In der von uns abgerufenen Kopie hatten 753 von 1.000 Listings keine Bewertungen, also ist der Median null. Die Top 10% der Listings haben 98% aller Bewertungen. Der Mittelwert von 52,5 pro Listing beschreibt die Datei als Ganzes und kein Listing, das Sie öffnen werden.

Die Bewertungsschiefe sieht aus wie der Währungsfall, ist aber ein anderer Fehler mit einer anderen Lösung. Der Währungsfall oben ist ein Einheitenfehler, und der Mittelwert ist falsch. Bewertungsvolumen ist Schiefe, und dort ist der Mittelwert für eine Gesamtsumme richtig. Eine Zufallsstichprobe von 1.000 Listings sollte etwa 52.500 Bewertungen zurückgeben, also verwenden Sie den Mittelwert, wenn Sie ein Bulk-Korpus planen. Bei einer Stichprobe in Pilot-Größe macht diese Konzentration die Schätzung unzuverlässig.

Abdeckung ist eine andere Frage. Mit 753 von 1.000 bei null haben nur etwa 25% der von Ihnen gekauften Zeilen Bewertungen.

Wenn die benötigten Daten historisch statt live sind, überspringt ein vorbereiteter Datensatz den Crawl. Die Etsy-Datensatz-Seite listet die aktuelle Feldanzahl, Gesamtdatensätze, den Pro-Datensatz-Preis und die Mindestbestellung auf, und diese 4 Zahlen sind die Kaufseiten-Arithmetik.

Die Pro-Datensatz-Zahl auf dieser Seite ist der Einmalpreis, und Aktualisierungspläne von halbjährlich bis täglich kommen als Abonnements, die ihn rabattieren. Wie aktuell die Daten sind, spielt ebenfalls eine Rolle. Die Seite beschreibt vorgesammelte Datensätze als Tage bis Monate alt, während die Erfassung auf Anfrage es Ihnen ermöglicht, dieses Limit vor dem Checkout festzulegen. Sie wählen den Aktualisierungsplan und das Volumen-Level direkt auf der Datensatz-Seite.

Diese Zahlen ändern sich ohne Ankündigung, also lesen Sie sie von der Seite, bevor Sie budgetieren, und behandeln Sie die 2 Preisseiten genauso. Die Datensatz-Seite zeigt ein Sample der Datensätze unter der Zusammenfassungszeile, bis Sie Zugang beantragen unscharf:

Die Bright Data Etsy-Datensatz-Seite. Eine Zusammenfassungszeile enthält die Datenfeld-Anzahl, die Gesamtdatensätze, den Startpreis pro Datensatz und die Mindestbestellung. Darüber befinden sich die Etsy-Datensatz-Überschrift, eine Beschreibung der Attribute des Datensatzes und Schaltflächen zum Kontaktieren des Vertriebs oder Kaufen des Datensatzes

Ein externer Referenzpunkt ist hier mehr wert als eine Anbieter-Behauptung. Dasselbe Papier, Mecha-nudges for Machines, dokumentiert in Anhang B, woher seine Daten stammen. Die Autoren geben an, dass die Rohdaten “von dem Unternehmen Bright Data erhalten wurden, das strukturierte Datensätze von Etsy-Produktlistings bereitstellt”. Ihr Extract “wurde am 12. November 2025 gesammelt und am selben Tag geliefert”. Sie beschreiben 2 Snapshots mit 5 Millionen und 1,06 Millionen Listings. Dieser Anhang ist überprüfbar, und eine Anbieter-Fallstudie ist es nicht.

Die Regel hier ist eng. Bauen Sie den Scraper, wenn Sie einige tausend Listings benötigen, die Sie per URL aufzählen können, und Sie 1 Locale fixieren können. Kaufen Sie die Collection-Schicht, wenn die Liste der URLs das Schwierige ist oder wenn der Crawl weiterlaufen muss. Kaufen Sie auch, wenn der Datensatz Preisarbeit speist, da die Preis-Bereich- und Durchstreichungsfelder bereits getrennt ankommen. Die Währungsspalte muss auf beiden Pfaden noch gefiltert werden.

Bei dem oben gemessenen Shop kostet der Aufbau 1.895 Anfragen und etwa 13 Stunden single-threaded für 1.842 Listings. Die Kaufseite ist eine Mindestbestellung vorbereiteter Datensätze. Diese 2 Zahlen sind nicht direkt vergleichbar, da die Aufbaukosten pro Shop anfallen und die Kaufkosten ein Minimum sind, das Sie auf Shops verteilen.

Abschließende Gedanken

Etsy blockiert gewöhnliches Fetching, während es schema.org JSON-LD auf denselben öffentlichen Seiten veröffentlicht, sodass Extraktion eine kurze Aufgabe ist und Zugang das meiste der Arbeit ist. Der Transport entscheidet nicht über den Zugang, da ein headed Browser eine Seite lud, die 9 abgestimmte HTTP-Clients nicht laden konnten, und Etsy denselben Browser bei seiner zweiten Anfrage ablehnte. Das Engineering-Problem ist also, wie man das Sammeln fortsetzt, nicht wie man wie ein Browser aussieht. Welchen Weg Sie auch nehmen, die Datenfehler kosten mehr als die Zugangs-Fehler, da eine gemischte Währungsspalte oder ein totes Listing zum vollen Preis sauber parst und man es Monate später findet. Holen Sie 1 Listing durch einen rohen Fetch und einen strukturierten Extraktor, vergleichen Sie die Felder, und lassen Sie das Ergebnis Aufbauen versus Kaufen entscheiden, bevor Sie den Crawler schreiben.

Häufig gestellte Fragen

Gibt es eine API für Etsy?

Ja. Die Etsy Open API v3 listete 76 Pfade, als wir zählten, von denen 31 GET-Operationen nur einen Anwendungsschlüssel benötigen. Sie gibt Listings, Shops, Bewertungen und Taxonomie zurück. Für Shops, die Sie nicht betreiben, lässt sie Verkäufe pro Listing, Keyword-Suchvolumen, Konversionsrate und Historie weg, die viele Datenprojekte benötigen.

Ist die Etsy-API kostenlos?

Die API selbst hat keinen veröffentlichten Preis, aber der Zugang hängt von der Genehmigung und von Rate-Limits ab, die pro Anwendungsschlüssel festgelegt werden. Etsy entscheidet, wer höhere Limits bekommt, und kann zusätzliche Bedingungen oder Gebühren anhängen. Die praktischen Kosten sind das Rate-Limit und keine Gebühr.

Warum wird mein Etsy-Scraper blockiert?

Etsy betreibt DataDome, das einen 403 mit einer kurzen Blockseite und einem X-DataDome-riskscore-Header zurückgibt, der Ihre Anfrage bewertet, wobei 1,0 der schlechteste Wert ist. Der Score folgt Ihrer aufrufenden IP und der Anfrage-Signatur. In unseren Tests senkten Header-Bearbeitungen den Score, und Chrome TLS-Impersonation erhöhte ihn, und keines von beidem gab eine Seite zurück.

Erlaubt Etsy Web-Scraping?

Nicht ohne ausdrückliche Genehmigung von Etsy. Die Nutzungsbedingungen sind das strengere Dokument. robots.txt verbietet die Keyword-Suche, die Verkaufshistorie und Favoriten in jeder Locale-Variante und lässt Listing- und Shop-Seiten offen. Etsy schreibt diese Datei ohne Ankündigung neu, also lesen Sie die Live-Kopie, bevor Sie aufbauen.

Kann man Etsy mit Python scrapen?

Ja. Listing-Seiten betten schema.org JSON-LD ein, sodass die Extraktion keine CSS-Selektoren benötigt. Wählen Sie den application/ld+json-Block, dessen @type Product ist, und vereinfachen Sie dann das Angebots-Objekt. Der Fetch ist die schwierige Hälfte, und das Sammeln im Volumen benötigt neue Browsing-Kontexte und rotierende Adressen.

Wie bekomme ich Etsy-Verkaufsdaten?

Verkäufe sind nur auf Shop-Ebene öffentlich. Der getShop-Endpunkt gibt transaction_sold_count zurück, eine Lifetime-Zahl für den gesamten Shop. Nichts teilt das pro Listing für einen Shop auf, den Sie nicht betreiben, also kommen die Pro-Listing-Zahlen eines Tools von woanders. Behandeln Sie diese als Schätzungen und prüfen Sie sie gegen Shop-Gesamtzahlen.

Hat Etsy eine Sitemap zum Crawlen?

Keine bei unserer letzten Prüfung. Die robots.txt-Datei deklariert null Sitemap:-Direktiven, und /sitemaps.xml gibt einen 403 mit leerem Body zurück. Ohne Sitemap müssen Sie URLs von Shop-Seiten, dem API-Endpunkt findAllListingsActive oder einem vorbereiteten Datensatz entdecken. Keines dieser 3 berührt den verbotenen Suchpfad.

Blockiert Etsy KI-Crawler wie GPTBot?

Nicht namentlich, als wir prüften. Die robots.txt-Datei deklariert nur 3 User-Agent-Gruppen, keine davon ein KI-Crawler, und Etsy veröffentlicht weder llms.txt noch ai.txt. DataDome lehnte GPTBot und ClaudeBot trotzdem ab, beide mit einem schlechteren Score von 0,9814 als die 0,923, die ein Chrome User-Agent erzielte. Sinnlose Strings erzielten denselben Score.