Naver Shopping und SmartStore Daten scrapen

Erfahre, wie man Naver-Suche, Plus Store und SmartStore Seiten mit Python scrapt, und wähle das richtige Bright Data Tool für jeden Seitentyp.
6 min lesen
How to Scrape Naver

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 discountedSalePrice lesen, da salePrice bei 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, brand und 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:

Naver Developers Hinweis mit dem Titel 'Search API, Search Trend, Shopping Insight 서비스 종료 및 NAVER API HUB 이관 안내', datiert auf 2026-06-29, mit 3 markierten koreanischen Sätzen, nummeriert 1 bis 3, über einem englischen Übersetzungsfeld

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:

Chrome View Page Source von Naver Suchergebnissen für 아이폰 17, Zeilen 43 bis 46, wo ein Script-Tag ein JSON-Objekt der newshopping INITIAL_STATE Variable zuweist. Hervorhebungen markieren die Variable, sourceType AD, und den ersten Produktnamen

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 requests und playwright. 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:

Bright Data Control Panel, Zugriffsdetails-Abschnitt einer Browser API Zone, zeigt den Zonennamen, den Host brd.superproxy.io, und die Puppeteer / Playwright WebSocket-URL auf Port 9222, mit maskierter Kunden-ID und versteckten Passwort

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:

Naver Suchergebnisse für 아이폰 17 zeigen die 네이버 가격비교 und 네이버플러스 스토어 Shopping-Blöcke, beschriftet als shopping und nstore. Rote AD-Tags markieren 4 광고 Karten, und orangene Markierungen 1 und 2 umreißen einen 1.405.500원 Preis und eine 9.999+ Rezensionsanzahl

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:

Bright Data SERP API Zoneneinstellungen, mit Raw HTML ausgewählt in der Datenformat-Liste

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 Plus Store Suchergebnisse für 코카콜라 제로 in einem 4-Spalten-Raster, mit Tags auf Rang 1 bis 4 die AD lesen, Rang 5 und 6 SUPER_POINT, und Rang 7 und 8 ORGANIC. Karten 1 bis 6 zeigen 광고 Badges

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:

Sequenzdiagramm: eine echte Browser-Session lud /ns/search, ein WebAssembly-Modul von wtm.pstatic.net erstellte einen cipherText, der Browser sendete es an ncpt.naver.com/v2/tokens und erhielt eine tokenId, forderte dann /ns/v1/search/paged-composite-cards mit dem x-wtm-ncaptcha-token Header an und erhielt 50 Karten. Ein rotes Kästchen vermerkt, dass dieselbe Anfrage ohne neuen Token HTTP 418 erhielt

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:

Bright Data Control Panel zeigt den Naver Produkte Collect by URL Endpunkt, mit einer SmartStore Produkt-URL in der Inputs-Tabelle, ausgewähltem Synchronous-Modus, und Scraper-Einstellungen für Lieferung und Datensatzlimits

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_price war ein String, und final_price war eine Ganzzahl, in allen 930 Datensätzen eines Naver Produkte Dataset-Beispiels und in Echtzeit-Datensätzen. Beide vor dem Vergleich in Ganzzahlen umwandeln, wie validate es tut.
  • In diesem Batch bedeutete ein null original_price, dass es keinen Rabatt gab, und ein null total_reviews bedeutete, dass es noch keine Rezensionen gab.
  • total_reviews stimmte 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_id mit der Produktnummer in jeder Eingabe-URL überein, sodass sie zur Verknüpfung von Datensätzen mit Plus Store Ergebnissen verwendet werden kann.
  • custom_output_fields steuert, 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.