Naver hat die Shopping Search API am 31. Juli 2026 beendet, ohne offiziellen Ersatz. Teams brauchten weiterhin Preise, Rankings und Verkäufernamen von Naver Shopping und SmartStore, daher war ihre Hauptoption, Naver’s Seiten zu scrapen.
In unseren Tests erhielten einfache HTTP-Clients vollständige Ergebnisse von Naver’s Suchseiten. Naver’s Shopping-Seiten lieferten jedoch eine Blockseite, ein CAPTCHA oder eine Login-Weiterleitung. Die Daten waren trotzdem vorhanden, eingebettet in diesen Seiten als JSON.
Dieser Leitfaden zeigt, wie man dieses JSON in Python liest, welches Bright Data Tool jeden Seitentyp abruft und wie jedes Tool abgerechnet wird.
TL;DR
- Naver Suchseiten mit Bright Data’s SERP API scrapen. Preise aus
discountedSalePricelesen, dasalePricebei 6 von 8 Preisvergleichskarten für 아이폰 17 den Wert 0 hatte. - Web Unlocker für Plus Store Ergebnisse und Browser API für Paginierung verwenden. Anzeigen markieren: keine der ersten 6 Listings für 코카콜라 제로 war organisch.
- Die meisten Shopping API Felder aus der Preisvergleichssuche rekonstruieren. Web Unlocker lieferte das JSON der Seite, mit
lowPrice,brandund Kategorien, bei 10 von 10 Abfragen. - SmartStore Produkt-URLs an die Naver Shopping Scraper API senden. Ein Batch von 10 lieferte jedes öffentliche Listing, mit Preisen, die jeder Seite entsprachen.
Das Ende von Naver’s Shopping Search API
Naver’s Open API war die offizielle Datenquelle für Preis- und Digital-Shelf-Monitoring. Ihr Shopping Search Endpunkt lieferte bis zu 100 Produkte pro Aufruf und erlaubte 25K Aufrufe pro Tag mit einem kostenlosen Schlüssel.
Naver’s Hinweis im Developer Center kündigte an, dass die Shopping-, Book- und Forschungsdokument-Suche APIs am 31. Juli 2026 enden würden. Der Hinweis besagte, dass bestehende Schlüssel ohne Übergangsfrist nicht mehr funktionieren würden. Er besagte auch, dass Naver keine Ersatz-API bereitstellen würde, nicht einmal über NAVER API HUB:

Mit einem festen Enddatum und keiner Übergangsfrist brauchten Feeds, die diese API aufriefen, bis zum 31. Juli 2026 eine neue Quelle.
Derselbe Hinweis besagte, dass die anderen Such-APIs, einschließlich Blog-, News- und Webdokumentensuche, zu NAVER API HUB auf der NAVER Cloud Platform wechseln würden. Developer-Center-Schlüssel, die vor dem 31. Juli 2026 ausgestellt wurden, würden für diese APIs bis zum 30. Juni 2027 weiter funktionieren.
Nach Naver’s Open API Bedingungen waren diese APIs auch nicht als Monitoring-Feed gedacht. Die Bedingungen verboten das Kopieren, Speichern oder Zwischenspeichern von Ergebnissen, außer temporäres Caching für Servicekontinuität. Sie verboten auch, Ergebnisse an Dritte bereitzustellen oder zu verkaufen und sie als KI-Input oder Trainingsdaten zu verwenden.
Naver’s DataLab Shopping Insight API lieferte Klick-Trends nach Kategorie und Keyword. Sie skalierte jeden Trend so, dass sein Höhepunkt 100 war. Diese Trends zeigten, ob das Interesse stieg oder fiel, aber keine Preise. Naver’s öffentliche Seiten zeigten die Preise, Rankings und Verkäufernamen, also sammle sie dort.
Wie jede Naver-Property auf Scraper reagierte
Wir testeten 4 einfache HTTP-Clients auf jeder von 4 Naver-Properties von einer Residential-Verbindung in Indien. Such- und Hotelseiten lieferten vollständiges HTML, aber Shopping- und Produktseiten blockierten diese Clients. Für die blockierten Seiten enthält die Tabelle auch Browser und Unblocker:
| Naver Property | Einfache HTTP-Clients erhielten | Browser und Unblocker erhielten |
|---|---|---|
Suche (search.naver.com) |
Die vollständige Ergebnisseite, etwa 1,5 MB, sogar mit dem Standard python-requests User-Agent | Nicht erforderlich zum Laden der Seite |
Shopping-Suche (search.shopping.naver.com) |
Eine HTTP 418 Blockseite, eine HTTP 405 CAPTCHA-Seite oder eine 307-Weiterleitung zu Naver’s Login-Seite | Headless Chrome erhielt die 418, Headful Chrome erhielt das CAPTCHA, und Web Unlocker erhielt die Ergebnisse |
SmartStore Produkte (smartstore.naver.com) |
Eine HTTP 429 Seite mit dem Titel “시스템오류” (“Systemfehler”) bei der ersten Anfrage | Web Unlocker und Browser API erhielten die Produktseite |
Hotels (hotels.naver.com) |
Die vollständige Seite mit ihren Next.js-Daten | Nicht erforderlich zum Laden der Seite |
Naver-Suche lieferte Ergebnisse für alle 500 Testanfragen, ohne CAPTCHA oder Block. Die ersten 200 waren sequenziell, etwa 2 Sekunden auseinander. Die anderen 300 wurden als Burst mit 4,9 Anfragen pro Sekunde von 8 gleichzeitigen Workern gesendet, und jede Burst-Anfrage verwendete eine neue Session.
Die 418-Seite bei der Shopping-Suche besagte, dass der Zugriff vorübergehend eingeschränkt sei. Die Seite listete 4 Auslöser auf: Besuche von externen Werbeaktionen, die nicht mit Shopping zusammenhängen, Anfragebursts von einer einzelnen IP, VPN-IPs und bestimmte Browser-Erweiterungen. Die 405-Seite lud Naver’s eigenes CAPTCHA-Skript von ncpt.naver.com.
In Headful Chrome lud die 405-Seite ein WebAssembly-Modul von wtm.pstatic.net und sendete eine verschlüsselte Payload an ncpt.naver.com/v2/tokens. Die Seite zeigte dann ein Kassenbon-CAPTCHA: ein generierter Kassenbon mit einer Lückentext-Frage dazu.
Ein einfacher HTTP-Client mit Chrome’s User-Agent erhielt das 405-CAPTCHA statt der 418. Das 405-CAPTCHA erschien weiterhin, nachdem curl_cffi Chrome’s TLS- und HTTP/2-Fingerprint angepasst hatte. Headless Chrome erhielt die 418 bei seiner ersten Dokumentanfrage, nach 0,5 Sekunden und bevor irgendein Seitenskript lief. Keiner der 6 Clients, von python-requests bis Headful Chrome, erhielt Shopping-Ergebnisse.
SmartStore’s 429 erschien bei der ersten Anfrage von jedem neuen Client, sodass es sich eher wie ein Block als ein typisches Rate Limit verhielt. Der Fehlertext besagte, die Ursache sei hoher Traffic oder ein instabiles Netzwerk.
Sammle nur Seiten, die ohne Login geladen werden. Daten hinter einer Login-Weiterleitung sind nicht öffentlich, da der Zugriff darauf die Automatisierung eines Naver-Kontos erfordert. Bright Data’s Richtlinie zur akzeptablen Nutzung schließt auch Daten hinter einem Login aus.
Behandle jede Property als separates Ziel.
Das in jeder Naver-Seite eingebettete JSON
Jede getestete Naver-Seite hatte ihre Daten im HTML als JSON oder ein JavaScript-Objekt, sodass die Parser keine CSS-Selektoren benötigen. Auf der Suchergebnisseite wurden die Daten jedes Shopping-Blocks einer eigenen JavaScript-Variable zugewiesen:

Die Daten des Preisvergleichsblocks waren ein einziges JSON-Objekt, sodass ein einzelner Decoder-Aufruf jede Karte darin liest. In View Page Source war diese Zuweisung eine einzelne Zeile mit 32.182 Zeichen.
Jede Seite speicherte ihr JSON an einem anderen Ort, und jede Seite hatte ihr eigenes Parsing-Problem:
| Seite | JSON-Speicherort | Parsing-Problem |
|---|---|---|
| Suchergebnisse | Ein _INITIAL_STATE-Objekt unter naver.search.ext.newshopping["shopping"], und ein weiteres unter ["nstore"] |
salePrice war 0 bei 6 der 8 Preisvergleichskarten für 아이폰 17. Der angezeigte Preis war in discountedSalePrice. |
Plus Store Suche (/ns/search) |
React Server Component Chunks, gepusht mit self.__next_f.push, unter dem Schlüssel initPagedCompositeCards |
Die Anführungszeichen des JSON waren innerhalb des HTML escaped, also müssen die Chunks vor dem Durchsuchen zusammengefügt und dekodiert werden. |
Preisvergleichssuche (/search/all) |
__NEXT_DATA__, bei props.pageProps.compositeList.list[].item |
price war ein String, während lowPrice eine Ganzzahl war, also price vor der Verwendung in eine Zahl umwandeln. |
| SmartStore Produkt | window.__PRELOADED_STATE__, bei simpleProductForDetailPage |
Das Objekt war kein striktes JSON, und der oberste product-Schlüssel enthielt Nullen. |
Wenn ein Parser anfängt, nichts zurückzugeben, prüfe zuerst diese Pfade.
Parse das JSON auch, wenn die gerenderte Seite einfacher zu lesen aussieht. Naver’s Seite deckelte die angezeigte Rezensionsanzahl eines Apple-Listings bei “9.999+”. Das eingebettete JSON für dasselbe Listing enthielt die genaue Anzahl, 13.971.
SmartStore’s State benötigte 3 zusätzliche Prüfungen. Auf einer Produktseite war der State ein JavaScript-Objektliteral mit 34 bloßen undefined-Werten, sodass json.loads einen Fehler auslöste. Die id des Produkts stimmte mit der Nummer in der URL überein, aber die interne productNo war eine andere Nummer, also id als Produkt-ID verwenden. Die Seite hatte auch 2 Rezensionsanzahlen, eine für das Produkt und eine größere für seine Produktgruppe.
Die folgende Funktion normalisiert das Objekt und trennt aktive von toten Listings:
import json
import re
MARKER = "window.__PRELOADED_STATE__="
def smartstore_product(html: str) -> dict | None:
start = html.index(MARKER) + len(MARKER)
# The state is a JavaScript object literal, and bare undefined values break json
text = re.sub(r"(?<=[:\[,])undefined(?=[,\]}])", "null", html[start:])
state, _ = json.JSONDecoder().raw_decode(text)
product = state["simpleProductForDetailPage"]
# None for a deleted listing, an empty id for one behind an authorization check
return product if product and product.get("id") else None
Die URL eines gelöschten Produkts lieferte immer noch eine normale Seite mit dem Titel “상품이 존재하지 않습니다” (“das Produkt existiert nicht”). Eine Statuscode-Prüfung hätte diese Seite also als aktives Listing behandelt. Die Funktion gibt None für diese Seite und für Listings hinter einer Autorisierungsprüfung zurück. Behandle ein None-Ergebnis als Signal, die URL aus deiner Watchlist zu entfernen.
Bright Data für Naver einrichten
Vier Skripte rufen Bright Data’s REST APIs mit requests auf, und das Paginierungsskript steuert einen Cloud-Browser mit Playwright. Du benötigst:
- Ein Bright Data Konto und einen API-Schlüssel aus den Kontoeinstellungen.
- Eine SERP API Zone, eine Web Unlocker Zone, und eine Browser API Zone. Eine Zone ist die Konfiguration eines Produkts im Control Panel, und die Skripte verwenden ihren Namen. Die Naver Shopping Scraper API benötigt nur den API-Schlüssel.
- Python 3.10 oder neuer mit
requestsundplaywright. Das Paginierungsskript verbindet Playwright mit einem entfernten Browser, sodass kein lokaler heruntergeladen werden muss.
Aus Sicherheitsgründen kann eine Zone Anfragen nur von den Adressen auf ihrer IP Allowlist akzeptieren. Wenn Anfragen an deine Zone ip_forbidden zurückgeben, füge die IP deines Rechners zu dieser Liste hinzu.
Installiere die Pakete und exportiere deine Zugangsdaten:
pip install requests playwright
export BRIGHTDATA_API_KEY="your_api_key"
export BRIGHTDATA_SERP_ZONE="your_serp_zone"
export BRIGHTDATA_UNLOCKER_ZONE="your_unlocker_zone"
export BRIGHTDATA_BROWSER_WSS="wss://brd-customer--zone--country-kr:@brd.superproxy.io:9222"
Der Wert BRIGHTDATA_BROWSER_WSS ist die WebSocket-URL, die unter Puppeteer / Playwright in den Zugriffsdetails deiner Zone angezeigt wird:

Die URL enthält bereits den Benutzernamen und das Passwort der Zone.
Füge -country-kr nach dem Zonennamen hinzu, um Anfragen über koreanische IPs zu leiten. Wenn du ein SDK gegenüber rohem HTTP bevorzugst, installiere das Bright Data Python SDK, brightdata-sdk.
Um eine einzelne URL zu prüfen, bevor du Code schreibst, verwende das Bright Data CLI, das die URL über Web Unlocker abruft:
bdata scrape "https://smartstore.naver.com/acnope/products/10444371682" --country kr -f html -o product.html
Der Befehl speicherte die vollständige SmartStore Produktseite, sodass smartstore_product(open("product.html", encoding="utf-8").read()) die Produktdaten zurückgibt.
Shopping-Ergebnisse aus der Naver-Suche mit der SERP API scrapen
Für 아이폰 17 (iPhone 17) zeigte Naver’s Desktop-Ergebnisseite 2 Shopping-Blöcke, einen mit Preisvergleichskarten und einen mit Naver Plus Store Produkten:

Rote Tags markieren die Karten, die Naver als 광고 (Werbung) kennzeichnete, die der Parser als is_ad markiert. Bei Marker 1 zeigte die Karte 1.405.500원, während ihr salePrice-Feld 0 war. Bei Marker 2 zeigte die Rezensionsanzahl 9.999+, während das JSON 13.971 enthielt.
Verwende die SERP API für Suchmaschinen-Ergebnisseiten (SERPs), Naver eingeschlossen, und Web Unlocker für andere Seiten. Die SERP API rechnet pro erfolgreicher Anfrage ab.
Naver lokalisierte SERP API Anfragen nach Korea. Das eingebettete JSON erfasste KR für diese Anfragen, und es erfasste IN mit einer indischen Stadt für direkte Anfragen aus Indien.
Für Naver, setze das Datenformat der Zone auf Raw HTML, oder übergib "data_format": "html" in jeder Anfrage:

Raw HTML lieferte die vollständige Ergebnisseite, einschließlich Naver’s eingebettetem Shopping-JSON.
Das folgende Skript ruft eine Ergebnisseite ab und gibt jede Karte aus beiden Shopping-Blöcken zurück, mit markierten Anzeigen:
import json
import os
import re
from urllib.parse import quote
import requests
API_URL = "https://api.brightdata.com/request"
HEADERS = {"Authorization": f"Bearer {os.environ['BRIGHTDATA_API_KEY']}"}
SERP_ZONE = os.environ["BRIGHTDATA_SERP_ZONE"]
# Each shopping block on the results page assigns its data to a JS global
STATE = re.compile(r'naver\.search\.ext\.newshopping\["(\w+)"\]\._INITIAL_STATE\s*=\s*')
def fetch_naver_serp(query: str) -> str:
url = f"https://search.naver.com/search.naver?where=nexearch&query={quote(query)}"
resp = requests.post(
API_URL,
headers=HEADERS,
# Request the raw page, which contains Naver's embedded shopping JSON
json={"zone": SERP_ZONE, "url": url, "format": "raw", "data_format": "html"},
timeout=120,
)
resp.raise_for_status()
if resp.headers.get("x-brd-error") or "_INITIAL_STATE" not in resp.text:
raise RuntimeError(f"No shopping state for {query!r}: {resp.headers.get('x-brd-error')}")
return resp.text
def parse_shopping_blocks(html: str) -> list[dict]:
decoder = json.JSONDecoder()
rows = []
for match in STATE.finditer(html):
block = match.group(1) # "shopping" = price comparison, "nstore" = Plus Store
state, _ = decoder.raw_decode(html, match.end())
props = state["initProps"]
if block == "shopping":
items = [slot["data"] for page in props["pagedSlot"] for slot in page["slots"]]
else:
items = props["products"]
for position, item in enumerate(items, start=1):
rows.append({
"block": block,
"position": position,
"is_ad": item.get("sourceType") == "AD",
"name": re.sub(r"<[^>]+>", "", item.get("productName", "")),
# discountedSalePrice holds the displayed price, and salePrice can be 0
"price": item.get("discountedSalePrice") or item.get("salePrice"),
"mall": item.get("mallName") or None,
"reviews": item.get("totalReviewCount"),
"nv_mid": item.get("nvMid"),
"channel_product_id": item.get("channelProductId"),
})
return rows
if __name__ == "__main__":
for row in parse_shopping_blocks(fetch_naver_serp("아이폰 17")):
label = "ad" if row["is_ad"] else " "
print(f'{row["block"]:<8} {row["position"]} {label} {row["price"]:>9,} KRW '
f'{row["mall"] or "(catalog)":<12} {row["name"][:24]}')
Für 아이폰 17 druckte das Skript 8 Preisvergleichskarten und 4 Plus Store Produkte:
shopping 1 ad 1,405,500 KRW 쿠팡 Apple 아이폰 17 자급제 256GB 화
shopping 2 ad 1,405,500 KRW 쿠팡 Apple 아이폰 17 자급제 256GB 블
shopping 3 ad 535,000 KRW 유플투게더몰 애플 아이폰 17 256GB LGU+ 기기변
shopping 4 1,405,500 KRW (catalog) 아이폰 17 256GB [자급제]
shopping 5 1,713,000 KRW (catalog) 아이폰 17 프로 256GB [자급제]
shopping 6 2,006,400 KRW (catalog) 아이폰 17 프로 512GB [자급제]
shopping 7 29,600 KRW 로트박스 [대여] 애플 아이폰 17 프로 대여 렌탈
shopping 8 2,294,400 KRW (catalog) 아이폰 17 프로 1TB [자급제]
nstore 1 ad 1,600,000 KRW 이투고 미국 아이폰 17 iPhone 17 직구 무
nstore 2 1,406,500 KRW 애플 공식 브랜드스토어 애플 아이폰 17 자급제
nstore 3 1,790,000 KRW 애플 공식 브랜드스토어 애플 아이폰 17 프로 자급제
nstore 4 1,697,500 KRW 애플 공식 브랜드스토어 애플 아이폰 17 자급제
Die ersten 3 Karten waren Anzeigen, und die dritte zeigte einen Preis, der einen Mobilfunkvertrag erforderte, nicht den Preis des entsperrten Telefons. Katalog-Produktkarten hatten keinen mall-Wert und eine leere channel_product_id, da sie auf Naver’s Preisvergleichsseiten statt auf einen einzelnen Shop verwiesen. Über 10 Abfragen lieferte die SERP API bei 10 von 10 eine Seite mit dem Shopping-JSON.
Zwischen 2 Durchläufen am selben Tag tauschten 2 Plus Store Positionen. Speichere einen Zeitstempel und das Anzeigen-Flag bei jeder Zeile, und probiere ein Keyword mehr als einmal, bevor dein Rank Tracking eine Änderung meldet.
Naver Plus Store Suchergebnisse mit Web Unlocker scrapen
Naver startete Plus Store, eine Shopping-Plattform, im Jahr 2024. Auf dessen Suchseite erhielten einfache HTTP-Clients die 418-Blockseite, das 405-CAPTCHA oder die Login-Weiterleitung.
Web Unlocker, Bright Data’s automatisierter Web Unlocker, ruft eine URL ab und rechnet pro erfolgreicher Anfrage ab. Mit koreanischen IPs lieferte er die Suchseite mit ihren ersten 50 eingebetteten Karten. Über 10 Abfragen lieferte Web Unlocker bei 10 von 10 eine Plus Store Seite mit den Ergebnisdaten.
Die folgende Fetch-Funktion fordert koreanische IPs an, bestätigt, dass die Antwort die Ergebnisdaten enthält, und versucht es erneut mit Backoff:
import json
import os
import re
import time
from collections import Counter
from urllib.parse import quote
import requests
API_URL = "https://api.brightdata.com/request"
HEADERS = {"Authorization": f"Bearer {os.environ['BRIGHTDATA_API_KEY']}"}
UNLOCKER_ZONE = os.environ["BRIGHTDATA_UNLOCKER_ZONE"]
FLIGHT = re.compile(r"self\.__next_f\.push\((\[.*?\])\)", re.S)
KEY = '"initPagedCompositeCards":'
MARKER = "initPagedCompositeCards" # quotes are escaped inside the raw HTML
def fetch_plus_store_search(query: str, attempts: int = 3) -> str:
url = f"https://search.shopping.naver.com/ns/search?query={quote(query)}"
for attempt in range(1, attempts + 1):
resp = requests.post(
API_URL,
headers=HEADERS,
json={"zone": UNLOCKER_ZONE, "url": url, "format": "raw", "country": "kr"},
timeout=180,
)
# Naver can redirect to a login page, so check for the result data, not the status
if resp.ok and MARKER in resp.text:
return resp.text
reason = resp.headers.get("x-brd-error") or ("login wall" if "nidlogin" in resp.text else "no data")
print(f"attempt {attempt} failed: {reason}")
time.sleep(5 * attempt)
raise RuntimeError(f"Plus Store search failed for {query!r}")
def parse_search(html: str) -> dict:
# The page streams React Server Component data in string chunks
chunks = [json.loads(m.group(1)) for m in FLIGHT.finditer(html)]
flight = "".join(c[1] for c in chunks if len(c) == 2 and isinstance(c[1], str))
start = flight.index(KEY) + len(KEY)
result, _ = json.JSONDecoder().raw_decode(flight, start)
products = []
for card in result["data"]:
item = card.get("card", {}).get("product")
if not item:
continue # banners and promotion slots have no product
products.append({
"rank": len(products) + 1,
"type": item.get("cardType") or "ORGANIC",
"name": item.get("productName"),
"price": item.get("discountedSalePrice"),
"list_price": item.get("salePrice"),
"mall": item.get("mallName"),
"reviews": item.get("totalReviewCount"),
"url": (item.get("productUrl") or {}).get("pcUrl"),
})
return {"total": result["total"], "next_cursor": result["cursor"], "products": products}
if __name__ == "__main__":
page = parse_search(fetch_plus_store_search("코카콜라 제로"))
mix = Counter(p["type"] for p in page["products"])
print("total results:", page["total"], "| next cursor:", page["next_cursor"], "| page 1 mix:", dict(mix))
for p in page["products"][:3] + [p for p in page["products"] if p["type"] == "ORGANIC"][:2]:
print(json.dumps(p, ensure_ascii=False))
Für 코카콜라 제로 (Coke Zero) hatte Seite 1 47 Produkte unter ihren 50 Karten, und das erste organische Listing war auf Rang 7:
total results: 7746 | next cursor: 33 | page 1 mix: {'AD': 12, 'SUPER_POINT': 2, 'ORGANIC': 31, 'SME_SLOT': 2}
{"rank": 1, "type": "AD", "name": "코카-콜라 제로 CAN 350ml 24개", "price": 21600, "list_price": 35290, "mall": "코카콜라직영몰", "reviews": 96956, "url": "https://smartstore.naver.com/main/products/4660954096"}
{"rank": 2, "type": "AD", "name": "코카-콜라 제로 CAN 190ml 30개", "price": 18000, "list_price": 24790, "mall": "코카콜라직영몰", "reviews": 46451, "url": "https://smartstore.naver.com/main/products/4609271837"}
{"rank": 3, "type": "AD", "name": "코카-콜라 제로 레몬라임 CAN 350ml 48개(2x24입) + 코크 큐브 블럭캘린더 (옵션선택)", "price": 38720, "list_price": 70580, "mall": "코카콜라직영몰", "reviews": 440, "url": "https://smartstore.naver.com/main/products/13689352448"}
{"rank": 7, "type": "ORGANIC", "name": "코카-콜라 제로 라벨프리 PET 370ml 24개", "price": 22880, "list_price": 35300, "mall": "코카콜라직영몰", "reviews": 25197, "url": "https://smartstore.naver.com/main/products/6006925665"}
{"rank": 8, "type": "ORGANIC", "name": "코카-콜라 제로 제로 라벨프리 PET 370ml 18개", "price": 16720, "list_price": 20900, "mall": "코카콜라직영몰", "reviews": 3676, "url": "https://smartstore.naver.com/main/products/11574172085"}
Auf der Seite selbst war keiner der ersten 6 Ränge ein organisches Listing:

Naver markierte Ränge 1 bis 6 mit 광고 (Werbung) Labels, und der Parser setzte deren type auf AD oder SUPER_POINT.
Behalte type bei jeder Zeile bei, sodass ein Share-of-Shelf-Bericht Anzeigen und Werbeaktionen von organischen Listings trennt. Verwende die Produktnummer in der URL jedes Listings als Produkt-ID. Namen sind unzuverlässige IDs, da auf dieser Seite alle 8 Karten, die mit einem Naver-Katalog verlinkt waren, catalogName anzeigten, nicht den gespeicherten productName.
Plus Store Suchergebnisse mit Browser API paginieren
Seite 1 der Plus Store Ergebnisse war im HTML eingebettet, und die Seite lud Seiten 2 und darüber hinaus mit endlosem Scrollen. Das ist der Unterschied zwischen Web Unlocker und Browser API: Web Unlocker liefert eine Seite, und Browser API kann sie scrollen. Als ein Käufer scrollte, forderte die Seite /ns/v1/search/paged-composite-cards mit einem Cursor an, und jede Anfrage trug einen x-wtm-ncaptcha-token-Header. Die Seite generierte einen neuen Token für jeden Aufruf:

Eine direkte Anfrage an den Endpunkt ohne neuen Token erhielt HTTP 418. Das Ausführen der Seite in einem echten Browser ließ ihre eigenen Skripte jeden Token generieren.
Browser API betreibt Chrome in Bright Data’s Cloud und rechnet nach Bandbreite ab. In diesem Browser generierte die eigene WebAssembly der Seite jeden Token, und der Code musste nur die JSON-Antworten lesen. Das folgende Skript verbindet sich über das Chrome DevTools Protocol (CDP), scrollt in ungleichmäßigen Schritten mit Pausen und erfasst jede Ergebnisseite:
import asyncio
import os
import random
import time
from urllib.parse import quote
from playwright.async_api import async_playwright
# wss://brd-customer--zone--country-kr:@brd.superproxy.io:9222
BROWSER_WSS = os.environ["BRIGHTDATA_BROWSER_WSS"]
PAGED_API = "/ns/v1/search/paged-composite-cards"
async def plus_store_pages(query: str, extra_pages: int = 5, budget_s: int = 120) -> list[dict]:
"""Return result pages 2..N as JSON. The page generates each request's token itself."""
pages = []
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(BROWSER_WSS)
page = await browser.new_page()
async def on_response(response):
# The page fetches each new set of cards as JSON while it scrolls
if PAGED_API in response.url and response.ok:
pages.append((await response.json())["data"])
page.on("response", on_response)
first = await page.goto(f"https://search.shopping.naver.com/ns/search?query={quote(query)}",
wait_until="domcontentloaded", timeout=120_000)
if first.status != 200:
raise RuntimeError(f"landing page returned {first.status}")
deadline = time.time() + budget_s
while len(pages) < extra_pages and time.time() < deadline:
if pages and not pages[-1]["hasMore"]:
break
# Scroll in uneven steps with pauses
await page.mouse.wheel(0, random.randint(900, 1500))
await page.wait_for_timeout(random.randint(2500, 4500))
await browser.close()
return pages
if __name__ == "__main__":
result = asyncio.run(plus_store_pages("캠핑의자"))
for data in result:
products = [c["card"]["product"] for c in data["data"] if c.get("card", {}).get("product")]
print(f"page {data['data'][0]['page']}: {len(products)} products, next cursor {data['cursor']}")
Für 캠핑의자 (Campingstuhl) sammelte die Session Seiten 2 bis 6:
page 2: 50 products, next cursor 74
page 3: 50 products, next cursor 112
page 4: 50 products, next cursor 151
page 5: 50 products, next cursor 189
page 6: 50 products, next cursor 239
In 3 weiteren Sessions mit diesem Scroll-Muster sammelte jede alle 5 zusätzlichen Seiten. Behalte die ungleichmäßigen Schritte und Pausen bei, wenn du das Skript anpasst, damit deine Sessions dem getesteten Muster entsprechen.
Die Felder der Shopping API aus der Preisvergleichssuche rekonstruieren
Von den von uns getesteten Seiten war Naver’s Preisvergleichssuche unter /search/all die ähnlichste zur eingestellten API. Ihr __NEXT_DATA__ enthielt 12 der 14 Felder, die die Shopping Search API lieferte, sodass ein defekter Feed die meisten seines Schemas behalten kann:
| Shopping Search API Feld | Preisvergleichssuche Feld |
|---|---|
productId |
id |
title |
productTitle |
link |
mallProductUrl für Einzelshop-Produkte, oder crUrl, Naver’s Tracking-Link, für Katalogprodukte |
image |
imageUrl |
lprice |
lowPrice, der niedrigste Preis über Shops hinweg für ein Katalogprodukt |
hprice |
Nicht zuordnen: highPrice war in jedem geprüften Ergebnis leer oder “0” |
mallName |
mallName, leer für Katalogprodukte, da viele Shops sie verkaufen |
productType |
Teilweise aus mallCount: über 0 markierte ein Katalogprodukt |
brand und maker |
brand und maker |
category1 bis category4 |
category1Name bis category4Name |
Web Unlocker lieferte diese Seite mit ihrer Ergebnisliste bei 10 von 10 Testabfragen. Das folgende Skript ruft die Seite über Web Unlocker ab, versucht es bis zu 3 Mal, und benennt die Felder jedes Ergebnisses nach den Namen der API um:
import json
import os
import re
import time
from urllib.parse import quote
import requests
API_URL = "https://api.brightdata.com/request"
HEADERS = {"Authorization": f"Bearer {os.environ['BRIGHTDATA_API_KEY']}"}
UNLOCKER_ZONE = os.environ["BRIGHTDATA_UNLOCKER_ZONE"]
NEXT_DATA = re.compile(r'', re.S)
def fetch_price_comparison(query: str, attempts: int = 3) -> str:
url = f"https://search.shopping.naver.com/search/all?query={quote(query)}"
for attempt in range(1, attempts + 1):
resp = requests.post(
API_URL,
headers=HEADERS,
json={"zone": UNLOCKER_ZONE, "url": url, "format": "raw", "country": "kr"},
timeout=180,
)
# A login page also returns 200, so check for the result list
if resp.ok and "compositeList" in resp.text:
return resp.text
print(f"attempt {attempt} failed: {resp.headers.get('x-brd-error') or 'no result list'}")
time.sleep(5 * attempt)
raise RuntimeError(f"Price comparison search failed for {query!r}")
def as_api_items(html: str) -> list[dict]:
"""Rename each result's fields to the ones the Shopping search API returned."""
data = json.loads(NEXT_DATA.search(html).group(1))
rows = []
for entry in data["props"]["pageProps"]["compositeList"]["list"]:
item = entry["item"]
rows.append({
"productId": item.get("id"),
"title": item.get("productTitle"),
"link": item.get("mallProductUrl") or item.get("crUrl"), # catalog products have no store URL
"lprice": item.get("lowPrice"), # the lowest price across stores for a catalog product
"mallName": item.get("mallName") or None,
"mallCount": item.get("mallCount"), # above 0 marks a price comparison catalog product
"brand": item.get("brand"),
"maker": item.get("maker"),
**{f"category{i}": item.get(f"category{i}Name") for i in range(1, 5)},
# adId marks an ad, and entry types other than "product" are promoted slots
"is_ad": bool(item.get("adId")) or entry["type"] != "product",
})
return rows
if __name__ == "__main__":
rows = as_api_items(fetch_price_comparison("코카콜라 제로"))
first = next(i for i, row in enumerate(rows) if not row["is_ad"])
print(f"{len(rows)} results, {sum(r['is_ad'] for r in rows)} ads or promoted slots, first organic at rank {first + 1}")
print(json.dumps({k: v for k, v in rows[first].items() if k != "link"}, ensure_ascii=False))
Für 코카콜라 제로 waren die ersten 8 Ergebnisse Anzeigen oder beworbene Slots, und das erste organische Ergebnis war ein Katalogprodukt:
56 results, 16 ads or promoted slots, first organic at rank 9
{"productId": "54166199731", "title": "코카콜라 콜라 , 190ml, 30개", "lprice": 13400, "mallName": null, "mallCount": 8702, "brand": "코카콜라", "maker": "코카콜라", "category1": "식품", "category2": "음료", "category3": "청량/탄산음료", "category4": "콜라", "is_ad": false}
Das JSON der Seite markiert auch Anzeigen, ein Signal, das die eingestellte API nicht lieferte, also nach is_ad filtern, bevor alte und neue Rankings verglichen werden.
SmartStore Produktdatensätze mit der Naver Shopping Scraper API abrufen
Für Produktdetails fordere strukturierte Datensätze an, statt HTML. Die Naver Shopping Scraper API, Teil von Bright Data’s Web Scraper API, führt einen SmartStore Scraper für jede gesendete URL aus. Im Control Panel nimmt ihr Collect by URL Endpunkt eine Liste von Produkt-URLs entgegen:

Derselbe Endpunkt ist über die REST API verfügbar, mit der Dataset-ID gd_m9qqjxxr1hab7okefj.
Das Skript stützt sich auf 3 Details der API. Wenn ein Job länger als das synchrone Timeout-Limit dauert, liefert der /scrape-Endpunkt HTTP 202 mit einer Snapshot-ID, die du abfragst. Verwende /scrape für kleine Batches, und sende größere Jobs an den asynchronen /trigger-Endpunkt, dessen Dokumentation das aktuelle Batch-Größenlimit angibt. Und custom_output_fields liefert nur die aufgelisteten Felder, also füge input, error, und error_code hinzu, um zu sehen, warum eine URL kein Produkt lieferte.
Das folgende Skript sendet einen Batch, wechselt bei Bedarf zu Snapshot-Polling, und validiert jeden Datensatz:
import json
import os
import sys
import time
import requests
API = "https://api.brightdata.com/datasets/v3"
HEADERS = {"Authorization": f"Bearer {os.environ['BRIGHTDATA_API_KEY']}"}
NAVER_PRODUCTS = "gd_m9qqjxxr1hab7okefj"
# custom_output_fields returns only what you list, so include input and the error fields
FIELDS = ("input|error|error_code|url|product_id|title|original_price|final_price"
"|discount_rate|total_reviews|sold_out|breadcrumbs|timestamp")
def collect_products(urls: list[str]) -> list[dict]:
params = {"dataset_id": NAVER_PRODUCTS, "format": "json", # a JSON array instead of the default NDJSON
"include_errors": "true", "custom_output_fields": FIELDS}
body = {"input": [{"url": u} for u in urls]}
resp = requests.post(f"{API}/scrape", params=params, json=body, headers=HEADERS, timeout=180)
if resp.status_code == 202: # the job took longer than the synchronous timeout limit
snapshot_id = resp.json()["snapshot_id"]
while True:
status = requests.get(f"{API}/progress/{snapshot_id}", headers=HEADERS, timeout=60).json()["status"]
if status == "ready":
break
if status == "failed":
raise RuntimeError(f"snapshot {snapshot_id} failed")
time.sleep(10)
while True: # the snapshot endpoint returns 202 until the file is built
resp = requests.get(f"{API}/snapshot/{snapshot_id}", params={"format": "json"},
headers=HEADERS, timeout=120)
if resp.status_code != 202:
break
time.sleep(10)
resp.raise_for_status()
records = resp.json()
# Keep a list shape even if the API returns a single object
return records if isinstance(records, list) else [records]
def validate(record: dict) -> dict:
if record.get("error_code"):
# dead_page means the listing is gone: drop it from the watch list, don't retry
return {"url": record["input"]["url"], "ok": False, "reason": record["error_code"]}
list_price = int(record["original_price"]) if record.get("original_price") else None # int() also accepts the string form
price = record.get("final_price")
ok = isinstance(price, int) and price > 0 and (list_price is None or price <= list_price)
return {"product_id": record.get("product_id"), "ok": ok, "price": price, "list_price": list_price,
"reviews": record.get("total_reviews"), "title": (record.get("title") or "")[:40]}
if __name__ == "__main__":
urls = json.load(open(sys.argv[1])) if len(sys.argv) > 1 else [
"https://smartstore.naver.com/acnope/products/10444371682"]
records = collect_products(urls)
print(f"{len(records)} records")
for row in map(validate, records):
print(json.dumps(row, ensure_ascii=False))
Ein Batch von 10 SmartStore URLs lieferte Datensätze für alle 7 öffentlichen Listings und markierte die anderen 3 mit Fehlercodes:
10 records
{"url": "https://smartstore.naver.com/bestymall/products/9824862314", "ok": false, "reason": "dead_page"}
{"url": "https://smartstore.naver.com/wonliveshopm/products/12177447319", "ok": false, "reason": "dead_page"}
{"url": "https://smartstore.naver.com/dajunemall/products/12725077707", "ok": false, "reason": "wait_element_timeout"}
{"product_id": "11219337734", "ok": true, "price": 16890, "list_price": null, "reviews": null, "title": "TROLLI 트롤리 올인원 젤리 1kg 사우어 구미캔디"}
{"product_id": "11499596414", "ok": true, "price": 615340, "list_price": null, "reviews": null, "title": "LG전자 2도어 344L 몽블랑 네이처 (M344MB14) 2도어 냉장고"}
{"product_id": "12465433420", "ok": true, "price": 17900, "list_price": 35000, "reviews": 41, "title": "[국산 100%] 꿈에그린X알파플러스 KF94 새부리형 마스크 100매("}
{"product_id": "12332341407", "ok": true, "price": 299000, "list_price": 608000, "reviews": 12, "title": "마이크로소프트 MS office 2024 Home & Business "}
{"product_id": "12385071329", "ok": true, "price": 277000, "list_price": null, "reviews": null, "title": "-ku- 일렉트로룩스 WELL Q8 무선스틱청소기 WQ81-3OMBF"}
{"product_id": "5494837526", "ok": true, "price": 31490, "list_price": null, "reviews": 1, "title": "로슈 아큐첵 인스턴트 혈당계+시험지50매"}
{"product_id": "9280411528", "ok": true, "price": 32200, "list_price": 32250, "reviews": 33, "title": "오딧세이 남성2종 로맨틱 레드 스페셜세트+아이오페 포케터블 토트백 증 정"}
Jeder Preis, jede Rezensionsanzahl und jedes Flag stimmte mit derselben Produktseite überein, die über Web Unlocker abgerufen wurde. Die 3 markierten URLs waren 2 gelöschte Listings und 1 Listing hinter Naver’s Autorisierungsprüfung. Entferne dead_page URLs nach jedem Durchlauf aus deiner Watchlist. Rufe jede andere markierte URL über Web Unlocker ab, und parse jede Seite mit smartstore_product.
Jede URL im Batch begann mit der URL des Shops, wie die Standard-URL des Skripts smartstore.naver.com/acnope/products/10444371682. Um eine aus einer Plus Store Karte zu erstellen, lies die Shop-URL aus dem url-Parameter von mallUrl.pcUrl und hänge /products/ an.
Prüfe den Datenvertrag, bevor du Datensätze in ein Data Warehouse lädst:
original_pricewar ein String, undfinal_pricewar eine Ganzzahl, in allen 930 Datensätzen eines Naver Produkte Dataset-Beispiels und in Echtzeit-Datensätzen. Beide vor dem Vergleich in Ganzzahlen umwandeln, wievalidatees tut.- In diesem Batch bedeutete ein null
original_price, dass es keinen Rabatt gab, und ein nulltotal_reviewsbedeutete, dass es noch keine Rezensionen gab. total_reviewsstimmte mit der Rezensionsanzahl auf der Produktseite überein, 19.581 für die Standard-URL des Skripts. Diese Anzahl war für die gesamte Produktgruppe, und der Seitenstatus enthielt auch 15.422 für das einzelne Produkt.- In diesem Batch stimmte
product_idmit der Produktnummer in jeder Eingabe-URL überein, sodass sie zur Verknüpfung von Datensätzen mit Plus Store Ergebnissen verwendet werden kann. custom_output_fieldssteuert, welche Felder du speicherst, also behalte nur die Felder, die du überwachst, und speichere keine persönlichen Verkäuferdaten.
Für Historie oder Massenabdeckung statt Echtzeiterfassung, hol dir das Naver Produkte Dataset, abgerechnet pro Datensatz. Teams, die Preis- und Werbeeinblicke statt Rohdaten wollen, können den Naver Shopping Price Tracker verwenden.
Bright Data hat auch dedizierte Scraper für SmartStore Bewertungen, Naver Shopping Bewertungen, und Naver Hotels.
Tools und Kosten für jeden Naver Seitentyp vergleichen
Jeder Job im Naver Preis- und Digital-Shelf-Monitoring passt zu einem Bright Data Produkt:
| Job | Bright Data Produkt | Testergebnis | Abrechnung |
|---|---|---|---|
| Shopping-Ergebnisse in der Naver-Suche für ein Keyword | SERP API mit Raw HTML | 10 von 10 Abfragen | Pro erfolgreicher Anfrage (SERP API Preise) |
| Erste Seite der Plus Store Ergebnisse, 50 Karten | Web Unlocker | 10 von 10 Abfragen | Pro erfolgreicher Anfrage (Web Unlocker Preise) |
| Plus Store Seiten 2 und darüber hinaus | Browser API | 5 von 5 zusätzlichen Seiten in jeder von 4 Sessions mit Scroll-Pausen | Pro GB Bandbreite (Browser API Preise) |
| Shopping API Felder für ein Keyword, aus der Preisvergleichssuche | Web Unlocker | 10 von 10 Abfragen | Pro erfolgreicher Anfrage |
| Vollständige Datensätze für bekannte Produkt-URLs | Naver Shopping Scraper API | 7 von 7 öffentlichen Listings, mit Preisen und Bewertungen übereinstimmend mit jeder Seite | Pro Datensatz (Web Scraper API Preise) |
| Historie oder Massenproduktdaten | Naver Produkte Dataset | Preisfeldtypen in einer Stichprobe von 930 Datensätzen stimmten mit Echtzeit-Datensätzen überein | Pro Datensatz (Dataset Preise) |
Über Web Unlocker wird eine Plus Store Ergebnisseite als 1 Anfrage abgerechnet. Die Naver Shopping Scraper API rechnet pro Datensatz ab, sodass die Kosten mit der Anzahl der gesendeten Produkt-URLs skalieren. In unserem Test verwendete eine Browser API Session, die 5 zusätzliche Seiten mit 50 Produkten lud, etwa 18 MB. Multipliziere diese Einheiten mit den aktuellen Raten auf jeder Preisseite, um dein Budget zu schätzen.
Verwende die Suchseiten, um Produkte zu finden und ihren Suchrang zu verfolgen. Sende dann die überwachten URLs an die Naver Shopping Scraper API, sodass du nur für Datensätze zu Produkten bezahlst, die du verfolgst.
Abschließende Gedanken
Naver’s Shopping- und Produktseiten blockierten einfache HTTP-Clients, aber ihre Preisvergleichssuche enthielt immer noch die meisten Felder der eingestellten API. Lies das eingebettete JSON und prüfe dessen Inhalt, nicht den Statuscode, da Naver’s Login-Weiterleitungen und gelöschte Listings als gewöhnliche Seiten zurückkamen.
Verwende die SERP API und Web Unlocker für die Entdeckung, Browser API für Paginierung, und die Naver Shopping Scraper API für strukturierte Produktdatensätze. Echtzeiterfassung zeigt aktuelle Preise und Ränge, und das Naver Produkte Dataset liefert Historie oder Massenproduktdaten. Beginne damit, das Preisvergleichsskript auf die 5 am häufigsten überwachten Keywords anzuwenden, und lade dessen Zeilen in deine bestehenden Feed-Tabellen.
FAQ
Hat Naver noch eine Shopping API?
Naver hat die Shopping Search API am 31. Juli 2026 beendet, ohne Ersatz auf NAVER API HUB. Eine ähnliche Alternative war ihre Preisvergleichssuche, deren eingebettete Daten die meisten Felder der API enthielten, einschließlich niedrigster Preis, Marke und Kategorien. Bright Data’s Web Unlocker lieferte sie bei 10 von 10 Abfragen.
Warum liefert Naver Shopping einen 418-Fehler?
HTTP 418 war Naver Shopping’s Blockseite für vorübergehend eingeschränkten Zugriff. Sie listete 4 Auslöser auf: Besuche von externen Werbeaktionen, die nicht mit Shopping zusammenhängen, Anfragebursts von einer IP, VPN-IPs und einige Browser-Erweiterungen. Python’s requests und Headless Chrome erhielten sie, während Bright Data’s Web Unlocker die Ergebnisse erhielt.
Kann man Naver von außerhalb Koreas scrapen?
Ja, mit einem Unblocker für die Shopping-Seiten. Aus Indien erhielten einfache HTTP-Clients vollständige Seiten von der Naver-Suche, während Naver Shopping Blockseiten, CAPTCHAs oder Login-Weiterleitungen lieferte. Bright Data’s Web Unlocker lieferte dort 10 von 10 Plus Store Seiten. Verwende koreanische IPs, da Naver das Land jedes Anfragestellers erfasste.
Wie scrapt man Naver mit Python?
Rufe die Seite ab, parse dann das im HTML eingebettete JSON statt CSS-Selektoren zu verwenden. Suchseiten speicherten Shopping-Daten in _INITIAL_STATE Objekten, und SmartStore verwendete window.__PRELOADED_STATE__. Naver Shopping blockierte Python HTTP-Clients, also leite diese Anfragen über Bright Data’s Web Unlocker.
Wie viel kostet es, Naver zu scrapen?
Bright Data’s SERP API und Web Unlocker rechnen pro erfolgreicher Anfrage ab, die Naver Shopping Scraper API rechnet pro Datensatz ab, und Browser API rechnet pro GB ab. In unseren Tests war eine Ergebnisseite 1 Anfrage, und 5 zusätzliche Seiten über Browser API verwendeten etwa 18 MB. Aktuelle Raten finden sich auf der Preisseite.