Naver a mis fin à son API de recherche Shopping le 31 juillet 2026, sans remplacement officiel. Les équipes avaient toujours besoin des prix, classements et noms de vendeurs de Naver Shopping et SmartStore, leur principale option était donc de scraper les pages de Naver.
Dans nos tests, les clients HTTP classiques obtenaient des résultats complets depuis les pages de recherche de Naver. Les pages shopping de Naver, en revanche, renvoyaient une page de blocage, un CAPTCHA ou une redirection de connexion. Les données étaient toujours présentes, intégrées dans ces pages sous forme de JSON.
Ce guide vous montre comment lire ce JSON en Python, quel outil Bright Data récupère chaque type de page, et comment chaque outil est facturé.
TL;DR
- Scrapez les pages de recherche Naver avec l’API SERP de Bright Data. Lisez les prix depuis
discountedSalePrice, carsalePriceétait à 0 sur 6 des 8 cartes de comparaison de prix pour 아이폰 17. - Utilisez Web Unlocker pour les résultats Plus Store et Browser API pour la pagination. Signalez les publicités : aucune des 6 premières annonces pour 코카콜라 제로 n’était organique.
- Reconstruisez la plupart des champs de l’API Shopping à partir de la recherche de comparaison de prix. Web Unlocker a renvoyé le JSON de la page, avec
lowPrice,brandet les catégories, pour 10 requêtes sur 10. - Envoyez les URLs de produits SmartStore à l’API Naver Shopping Scraper. Un lot de 10 a renvoyé chaque annonce publique, avec des prix correspondant à chaque page.
La fin de l’API de recherche Shopping de Naver
L’API Open de Naver était la source de données officielle pour la surveillance des prix et du rayon digital. Son point de terminaison de recherche Shopping renvoyait jusqu’à 100 produits par appel et autorisait 25K appels par jour avec une clé gratuite.
L’avis du centre développeurs de Naver a annoncé que les API de recherche Shopping, Book et de documents de recherche prendraient fin le 31 juillet 2026. L’avis indiquait que les clés existantes cesseraient de fonctionner sans période de grâce. Il indiquait aussi que Naver ne fournirait aucune API de remplacement, pas même sur NAVER API HUB :

Avec une date de fin fixe et aucune période de grâce, les flux qui appelaient cette API avaient besoin d’une nouvelle source avant le 31 juillet 2026.
Le même avis indiquait que les autres API de recherche, y compris la recherche de blogs, d’actualités et de documents web, migreraient vers NAVER API HUB sur NAVER Cloud Platform. Les clés du centre développeurs émises avant le 31 juillet 2026 continueraient de fonctionner pour ces API jusqu’au 30 juin 2027.
Selon les conditions Open API de Naver, ces API n’étaient pas non plus destinées à être utilisées comme flux de surveillance. Les conditions interdisaient de copier, stocker ou mettre en cache les résultats, sauf mise en cache temporaire pour la continuité du service. Elles interdisaient aussi de fournir ou vendre les résultats à des tiers et de les utiliser comme entrée ou données d’entraînement pour l’IA.
L’API DataLab Shopping Insight de Naver renvoyait les tendances de clics par catégorie et mot-clé. Elle mettait chaque tendance à l’échelle pour que son point le plus haut soit 100. Ces tendances montraient si l’intérêt augmentait ou baissait, mais aucun prix. Les pages publiques de Naver affichaient les prix, classements et noms de vendeurs, il fallait donc les collecter là.
Comment chaque propriété Naver a réagi aux scrapers
Nous avons testé 4 clients HTTP classiques sur chacune des 4 propriétés Naver depuis une connexion résidentielle en Inde. Les pages de recherche et d’hôtels renvoyaient un HTML complet, mais les pages shopping et produit bloquaient ces clients. Pour les pages bloquées, le tableau inclut aussi les navigateurs et débloqueurs :
| Propriété Naver | Les clients HTTP classiques obtenaient | Les navigateurs et débloqueurs obtenaient |
|---|---|---|
Recherche (search.naver.com) |
La page de résultats complète, environ 1,5 Mo, même avec le User-Agent par défaut de python-requests | Pas nécessaire pour charger la page |
Recherche Shopping (search.shopping.naver.com) |
Une page de blocage HTTP 418, une page CAPTCHA HTTP 405, ou une redirection 307 vers la page de connexion de Naver | Chrome headless a obtenu le 418, Chrome avec interface a obtenu le CAPTCHA, et Web Unlocker a obtenu les résultats |
Produits SmartStore (smartstore.naver.com) |
Une page HTTP 429 intitulée « 시스템오류 » (« erreur système ») dès la première requête | Web Unlocker et Browser API ont obtenu la page produit |
Hôtels (hotels.naver.com) |
La page complète avec ses données Next.js | Pas nécessaire pour charger la page |
La recherche Naver a renvoyé des résultats pour les 500 requêtes de test, sans CAPTCHA ni blocage. Les 200 premières étaient séquentielles, espacées d’environ 2 secondes. Les 300 autres ont été envoyées en rafale à 4,9 demandes par seconde depuis 8 workers concurrents, et chaque requête en rafale utilisait une nouvelle session.
La page 418 sur la recherche shopping indiquait que l’accès était temporairement restreint. La page listait 4 déclencheurs : visites depuis des promotions externes sans rapport avec le shopping, rafales de requêtes depuis une seule IP, IPs VPN, et certaines extensions de navigateur. La page 405 chargeait le propre script CAPTCHA de Naver depuis ncpt.naver.com.
Dans Chrome avec interface, la page 405 chargeait un module WebAssembly depuis wtm.pstatic.net et envoyait une charge utile chiffrée à ncpt.naver.com/v2/tokens. La page affichait alors un CAPTCHA de reçu : un reçu de magasin généré avec une question à trous à son sujet.
Un client HTTP classique avec le User-Agent de Chrome a obtenu le CAPTCHA 405 au lieu du 418. Le CAPTCHA 405 apparaissait encore après que curl_cffi ait reproduit l’empreinte TLS et HTTP/2 de Chrome. Chrome headless a obtenu le 418 dès sa première requête de document, après 0,5 seconde et avant l’exécution de tout script de page. Aucun des 6 clients, de python-requests à Chrome avec interface, n’a obtenu de résultats shopping.
Le 429 de SmartStore apparaissait dès la première requête de chaque nouveau client, se comportant donc plus comme un blocage qu’une limitation de débit classique. Son texte d’erreur indiquait que la cause était un trafic élevé ou un réseau instable.
Collectez uniquement les pages qui se chargent sans connexion. Les données derrière une redirection de connexion ne sont pas publiques, car y accéder nécessite d’automatiser un compte Naver. La politique d’utilisation acceptable de Bright Data exclut également les données derrière une connexion.
Traitez chaque propriété comme une cible distincte.
Le JSON intégré dans chaque page Naver
Chaque page Naver testée avait ses données dans le HTML sous forme de JSON ou d’objet JavaScript, les parseurs n’ont donc besoin d’aucun sélecteur CSS. Sur la page de résultats de recherche, les données de chaque bloc shopping étaient assignées à sa propre variable JavaScript :

Les données du bloc de comparaison de prix formaient un seul objet JSON, un seul appel de décodeur lit donc chaque carte qu’il contient. Dans View Page Source, cette assignation tenait sur une seule ligne de 32 182 caractères.
Chaque page stockait son JSON à un endroit différent, et chaque page avait son propre problème d’analyse :
| Page | Emplacement du JSON | Problème d’analyse |
|---|---|---|
| Résultats de recherche | Un objet _INITIAL_STATE sur naver.search.ext.newshopping["shopping"], et un autre sur ["nstore"] |
salePrice était à 0 sur 6 des 8 cartes de comparaison de prix pour 아이폰 17. Le prix affiché se trouvait dans discountedSalePrice. |
Recherche Plus Store (/ns/search) |
Des fragments de React Server Component poussés avec self.__next_f.push, sous la clé initPagedCompositeCards |
Les guillemets du JSON étaient échappés dans le HTML, il faut donc joindre et décoder les fragments avant de les parcourir. |
Recherche de comparaison de prix (/search/all) |
__NEXT_DATA__, à props.pageProps.compositeList.list[].item |
price était une chaîne, tandis que lowPrice était un entier, convertissez donc price en nombre avant de l’utiliser. |
| Produit SmartStore | window.__PRELOADED_STATE__, à simpleProductForDetailPage |
L’objet n’était pas du JSON strict, et la clé de premier niveau product contenait des zéros. |
Lorsqu’un parseur commence à ne rien renvoyer, vérifiez d’abord ces chemins.
Analysez le JSON même lorsque la page rendue semble plus facile à lire. La page de Naver plafonnait le nombre d’avis affiché d’une annonce Apple à « 9 999+ ». Le JSON intégré pour la même annonce contenait le nombre exact, 13 971.
L’état de SmartStore nécessitait 3 vérifications supplémentaires. Sur une page produit, l’état était un littéral d’objet JavaScript avec 34 valeurs undefined nues, donc json.loads levait une erreur. L’id du produit correspondait au numéro dans l’URL, mais le productNo interne était un numéro différent, utilisez donc id comme identifiant de produit. La page comportait aussi 2 nombres d’avis, un pour le produit et un plus grand pour son groupe de produits.
La fonction ci-dessous normalise l’objet et sépare les annonces actives des annonces mortes :
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
L’URL d’un produit supprimé renvoyait toujours une page normale, intitulée « 상품이 존재하지 않습니다 » (« le produit n’existe pas »). Une simple vérification du code de statut traitait donc cette page comme une annonce active. La fonction renvoie None pour cette page et pour les annonces derrière une vérification d’autorisation. Traitez un résultat None comme le signal de retirer l’URL de votre liste de surveillance.
Configurer Bright Data pour Naver
Quatre scripts appellent les API REST de Bright Data avec requests, et le script de pagination pilote un navigateur cloud avec Playwright. Vous avez besoin de :
- Un compte Bright Data et une clé API depuis les paramètres du compte.
- Une zone API SERP, une zone Web Unlocker et une zone Browser API. Une zone est la configuration d’un produit dans le panneau de contrôle, et les scripts utilisent son nom. L’API Naver Shopping Scraper n’a besoin que de la clé API.
- Python 3.10 ou plus récent avec
requestsetplaywright. Le script de pagination connecte Playwright à un navigateur distant, vous n’avez donc pas besoin d’en télécharger un localement.
Pour des raisons de sécurité, une zone ne peut accepter des requêtes que depuis les adresses figurant sur sa liste blanche d’IP. Si les requêtes vers votre zone renvoient ip_forbidden, ajoutez l’IP de votre machine à cette liste.
Installez les paquets et exportez vos identifiants :
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-<id>-zone-<zone>-country-kr:<password>@brd.superproxy.io:9222"
La valeur BRIGHTDATA_BROWSER_WSS est l’URL WebSocket affichée sous Puppeteer / Playwright dans les Détails d’accès de votre zone :

L’URL intègre déjà le nom d’utilisateur et le mot de passe de la zone.
Ajoutez -country-kr après le nom de la zone pour router les requêtes via des IPs coréennes. Si vous préférez un SDK à des appels HTTP bruts, installez le SDK Python Bright Data, brightdata-sdk.
Pour vérifier une seule URL avant d’écrire du code, utilisez le CLI Bright Data, qui récupère l’URL via Web Unlocker :
bdata scrape "https://smartstore.naver.com/acnope/products/10444371682" --country kr -f html -o product.html
La commande a enregistré la page produit SmartStore complète, donc smartstore_product(open("product.html", encoding="utf-8").read()) renvoie ses données produit.
Scraper les résultats shopping de la recherche Naver avec l’API SERP
Pour 아이폰 17 (iPhone 17), la page de résultats desktop de Naver affichait 2 blocs shopping, l’un avec des cartes de comparaison de prix et l’autre avec des produits Naver Plus Store :

Les étiquettes rouges marquent les cartes que Naver a étiquetées 광고 (publicité), que le parseur signale comme is_ad. Au repère 1, la carte affichait 1 405 500₩, tandis que son champ salePrice était à 0. Au repère 2, le nombre d’avis affichait 9 999+, tandis que le JSON contenait 13 971.
Utilisez l’API SERP pour les pages de résultats de moteurs de recherche (SERP), Naver inclus, et Web Unlocker pour les autres sites. L’API SERP facture par requête réussie.
Naver a géolocalisé les requêtes de l’API SERP en Corée. Son JSON intégré enregistrait KR pour ces requêtes, et il enregistrait IN avec une ville indienne pour les requêtes directes depuis l’Inde.
Pour Naver, définissez le format de données de la zone sur HTML brut, ou passez "data_format": "html" dans chaque requête :

HTML brut renvoyait la page de résultats complète, incluant le JSON shopping intégré de Naver.
Le script ci-dessous récupère une page de résultats et renvoie chaque carte des deux blocs shopping, avec les publicités signalées :
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]}')
Pour 아이폰 17, le script a affiché 8 cartes de comparaison de prix et 4 produits Plus Store :
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 자급제
Les 3 premières cartes étaient des publicités, et la troisième affichait un prix qui nécessitait un contrat d’opérateur, pas le prix du téléphone débloqué. Les cartes de produits de catalogue n’avaient pas de valeur mall et un channel_product_id vide, car elles pointaient vers les pages de comparaison de prix de Naver plutôt que vers une seule boutique. Sur 10 requêtes, l’API SERP a renvoyé une page avec le JSON shopping 10 fois sur 10.
Entre 2 exécutions le même jour, 2 positions Plus Store se sont échangées. Stockez un horodatage et le drapeau de publicité avec chaque ligne, et échantillonnez un mot-clé plus d’une fois avant que votre suivi de classement ne signale un changement.
Scraper les résultats de recherche Naver Plus Store avec Web Unlocker
Naver a lancé Plus Store, une plateforme de shopping, en 2024. Sur sa page de recherche, les clients HTTP classiques obtenaient la page de blocage 418, le CAPTCHA 405, ou la redirection de connexion.
Web Unlocker, le débloqueur web automatisé de Bright Data, récupère une URL et facture par requête réussie. Avec des IPs coréennes, il a renvoyé la page de recherche avec ses 50 premières cartes intégrées. Sur 10 requêtes, Web Unlocker a renvoyé une page Plus Store contenant les données de résultat 10 fois sur 10.
La fonction de récupération ci-dessous demande des IPs coréennes, confirme que la réponse contient les données de résultat, et retente avec un délai croissant :
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\((\[.*?\])\)</script>", 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))
Pour 코카콜라 제로 (Coke Zero), la page 1 comptait 47 produits parmi ses 50 cartes, et la première annonce organique était au 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"}
Sur la page elle-même, aucun des 6 premiers rangs n’était une annonce organique :

Naver a marqué les rangs 1 à 6 avec des étiquettes 광고 (publicité), et le parseur a défini leur type sur AD ou SUPER_POINT.
Conservez type avec chaque ligne, afin qu’un rapport de part de rayon sépare les publicités et promotions des annonces organiques. Utilisez le numéro de produit dans l’URL de chaque annonce comme identifiant de produit. Les noms sont des identifiants peu fiables, car sur cette page, les 8 cartes liées à un catalogue Naver affichaient catalogName, pas le productName stocké.
Paginer les résultats de recherche Plus Store avec Browser API
La page 1 des résultats Plus Store était intégrée dans le HTML, et le site chargeait les pages 2 et suivantes avec un défilement infini. C’est la différence entre Web Unlocker et Browser API : Web Unlocker renvoie une page, et Browser API peut la faire défiler. Lorsqu’un acheteur faisait défiler, la page demandait /ns/v1/search/paged-composite-cards avec un curseur, et chaque requête portait un en-tête x-wtm-ncaptcha-token. La page générait un nouveau jeton pour chaque appel :

Une requête directe vers le point de terminaison sans nouveau jeton a obtenu HTTP 418. Exécuter la page dans un vrai navigateur permettait à ses propres scripts de générer chaque jeton.
Browser API exécute Chrome dans le cloud de Bright Data et facture par bande passante. Dans ce navigateur, le propre WebAssembly de la page générait chaque jeton, et le code n’avait plus qu’à lire les réponses JSON. Le script ci-dessous se connecte via le protocole Chrome DevTools (CDP), fait défiler par étapes irrégulières avec des pauses, et capture chaque page de résultats :
import asyncio
import os
import random
import time
from urllib.parse import quote
from playwright.async_api import async_playwright
# wss://brd-customer-<id>-zone-<zone>-country-kr:<password>@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']}")
Pour 캠핑의자 (chaise de camping), la session a collecté les pages 2 à 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
Lors de 3 autres sessions avec ce schéma de défilement, chacune a collecté les 5 pages supplémentaires. Conservez les étapes irrégulières et les pauses lorsque vous adaptez le script, afin que vos sessions correspondent au schéma testé.
Reconstruire les champs de l’API Shopping à partir de la recherche de comparaison de prix
Parmi les pages testées, la recherche de comparaison de prix de Naver, sur /search/all, était la plus proche de l’API retirée. Son __NEXT_DATA__ contenait 12 des 14 champs renvoyés par l’API de recherche Shopping, un flux cassé peut donc conserver la plupart de son schéma :
| Champ de l’API de recherche Shopping | Champ de recherche de comparaison de prix |
|---|---|
productId |
id |
title |
productTitle |
link |
mallProductUrl pour les produits d’une seule boutique, ou crUrl, le lien de suivi de Naver, pour les produits de catalogue |
image |
imageUrl |
lprice |
lowPrice, le prix le plus bas entre boutiques pour un produit de catalogue |
hprice |
Laissez non mappé : highPrice était vide ou « 0 » dans chaque résultat vérifié |
mallName |
mallName, vide pour les produits de catalogue car plusieurs boutiques les vendent |
productType |
Partiellement à partir de mallCount : au-dessus de 0 indiquait un produit de catalogue |
brand et maker |
brand et maker |
category1 à category4 |
category1Name à category4Name |
Web Unlocker a renvoyé cette page avec sa liste de résultats pour 10 requêtes de test sur 10. Le script ci-dessous récupère la page via Web Unlocker, essaie jusqu’à 3 fois, et renomme les champs de chaque résultat selon les noms de l’API :
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'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', 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))
Pour 코카콜라 제로, les 8 premiers résultats étaient des publicités ou des emplacements promus, et le premier résultat organique était un produit de catalogue :
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}
Le JSON de la page marque aussi les publicités, un signal que l’API retirée ne renvoyait pas, filtrez donc sur is_ad avant de comparer les anciens et nouveaux classements.
Obtenir les fiches produit SmartStore avec l’API Naver Shopping Scraper
Pour les détails produit, demandez des enregistrements structurés plutôt que du HTML. L’API Naver Shopping Scraper, partie de l’API Web Scraper de Bright Data, exécute un scraper SmartStore sur chaque URL que vous envoyez. Dans le panneau de contrôle, son point de terminaison Collecter par URL prend une liste d’URLs de produits :

Le même point de terminaison est disponible via l’API REST, avec l’identifiant de jeu de données gd_m9qqjxxr1hab7okefj.
Le script s’appuie sur 3 détails de l’API. Si une tâche prend plus de temps que la limite de délai synchrone, le point de terminaison /scrape renvoie HTTP 202 avec un identifiant de snapshot que vous interrogez. Utilisez /scrape pour les petits lots, et envoyez les tâches plus importantes au point de terminaison asynchrone /trigger, dont la documentation indique la limite de taille de lot actuelle. Et custom_output_fields ne renvoie que les champs que vous listez, ajoutez donc input, error et error_code pour voir pourquoi une URL n’a renvoyé aucun produit.
Le script ci-dessous envoie un lot, bascule vers l’interrogation de snapshot si nécessaire, et valide chaque enregistrement :
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))
Un lot de 10 URLs SmartStore a renvoyé des enregistrements pour les 7 annonces publiques et a signalé les 3 autres avec des codes d’erreur :
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종 로맨틱 레드 스페셜세트+아이오페 포케터블 토트백 증 정"}
Chaque prix, nombre d’avis et drapeau correspondait à la même page produit récupérée via Web Unlocker. Les 3 URLs signalées étaient 2 annonces supprimées et 1 annonce derrière la vérification d’autorisation de Naver. Retirez les URLs dead_page de votre liste de surveillance après chaque exécution. Récupérez toute autre URL signalée via Web Unlocker, et analysez chaque page avec smartstore_product.
Chaque URL du lot commençait par l’URL de la boutique, comme l’URL par défaut du script smartstore.naver.com/acnope/products/10444371682. Pour en construire une à partir d’une carte Plus Store, lisez l’URL de la boutique depuis le paramètre url de mallUrl.pcUrl et ajoutez /products/<channelProductId>.
Vérifiez le contrat de données avant de charger les enregistrements dans un entrepôt :
original_priceétait une chaîne, etfinal_priceétait un entier, dans les 930 enregistrements d’un échantillon de jeu de données de produits Naver et dans les enregistrements en temps réel. Convertissez les deux en entiers avant de les comparer, comme le faitvalidate.- Dans ce lot, un
original_pricenul signifiait aucune remise, et untotal_reviewsnul signifiait aucun avis pour l’instant. total_reviewscorrespondait au nombre d’avis sur la page produit, 19 581 pour l’URL par défaut du script. Ce nombre concernait l’ensemble du groupe de produits, et l’état de la page contenait aussi 15 422 pour le produit unique.- Dans ce lot,
product_idcorrespondait au numéro de produit dans chaque URL d’entrée, vous pouvez donc l’utiliser pour joindre les enregistrements aux résultats Plus Store. custom_output_fieldscontrôle les champs que vous stockez, ne conservez donc que les champs que vous surveillez et ne stockez pas les détails personnels des vendeurs.
Pour l’historique ou une couverture en masse plutôt qu’une collecte en temps réel, obtenez le jeu de données de produits Naver, facturé par enregistrement. Les équipes qui souhaitent des informations sur les prix et les promotions plutôt que des enregistrements bruts peuvent utiliser le Naver Shopping Price Tracker.
Bright Data dispose aussi de scrapers dédiés pour les avis SmartStore, les avis Naver Shopping, et Naver Hotels.
Comparer les outils et les coûts pour chaque type de page Naver
Chaque tâche de surveillance des prix et du rayon digital Naver s’associe à un produit Bright Data :
| Tâche | Produit Bright Data | Résultat du test | Mode de facturation |
|---|---|---|---|
| Résultats shopping sur la recherche Naver pour un mot-clé | API SERP avec HTML brut | 10 requêtes sur 10 | Par requête réussie (tarification de l’API SERP) |
| Première page de résultats Plus Store, 50 cartes | Web Unlocker | 10 requêtes sur 10 | Par requête réussie (tarification de Web Unlocker) |
| Pages Plus Store 2 et suivantes | Browser API | 5 pages supplémentaires sur 5 dans chacune des 4 sessions avec pauses de défilement | Par Go de bande passante (tarification de Browser API) |
| Champs de l’API Shopping pour un mot-clé, depuis la recherche de comparaison de prix | Web Unlocker | 10 requêtes sur 10 | Par requête réussie |
| Enregistrements complets pour des URLs de produits connues | API Naver Shopping Scraper | 7 annonces publiques sur 7, avec prix et avis correspondant à chaque page | Par enregistrement (tarification de l’API Web Scraper) |
| Historique ou données produit en masse | Jeu de données de produits Naver | Les types de champs de prix dans un échantillon de 930 enregistrements correspondaient aux enregistrements en temps réel | Par enregistrement (tarification des jeux de données) |
Via Web Unlocker, une page de résultats Plus Store est facturée comme 1 requête. L’API Naver Shopping Scraper facture par enregistrement, le coût évolue donc avec le nombre d’URLs de produits envoyées. Dans notre test, une session Browser API qui a chargé 5 pages supplémentaires de 50 produits a utilisé environ 18 Mo. Multipliez ces unités par les tarifs actuels sur chaque page de tarification pour estimer votre budget.
Utilisez les pages de recherche pour trouver des produits et suivre leur classement de recherche. Envoyez ensuite les URLs que vous surveillez à l’API Naver Shopping Scraper, afin de ne payer des enregistrements que pour les produits que vous suivez.
Réflexions finales
Les pages shopping et produit de Naver bloquaient les clients HTTP classiques, mais sa recherche de comparaison de prix contenait encore la plupart des champs de l’API retirée. Lisez le JSON intégré et vérifiez son contenu, pas le code de statut, car les redirections de connexion et les annonces supprimées de Naver revenaient comme des pages ordinaires.
Utilisez l’API SERP et Web Unlocker pour la découverte, Browser API pour la pagination, et l’API Naver Shopping Scraper pour les enregistrements produit structurés. La collecte en temps réel affiche les prix et classements actuels, et le jeu de données de produits Naver fournit l’historique ou des données produit en masse. Commencez par exécuter le script de comparaison de prix sur les 5 mots-clés que vous surveillez le plus, et chargez ses lignes dans vos tables de flux existantes.
FAQ
Naver a-t-il encore une API shopping ?
Naver a mis fin à son API de recherche Shopping le 31 juillet 2026, sans remplacement sur NAVER API HUB. Une alternative similaire était sa recherche de comparaison de prix, dont les données intégrées contenaient la plupart des champs de l’API, dont le prix le plus bas, la marque et les catégories. Web Unlocker de Bright Data l’a renvoyée pour 10 requêtes sur 10.
Pourquoi Naver Shopping renvoie-t-il une erreur 418 ?
HTTP 418 était la page de blocage de Naver Shopping pour un accès temporairement restreint. Elle listait 4 déclencheurs : visites depuis des promotions externes sans rapport avec le shopping, rafales de requêtes depuis une IP, IPs VPN, et certaines extensions de navigateur. Python requests et Chrome headless l’obtenaient, tandis que Web Unlocker de Bright Data obtenait les résultats.
Peut-on scraper Naver depuis l’extérieur de la Corée ?
Oui, avec un débloqueur pour les pages shopping. Depuis l’Inde, les clients HTTP classiques obtenaient des pages complètes depuis la recherche Naver, tandis que Naver Shopping renvoyait des pages de blocage, des CAPTCHA ou des redirections de connexion. Web Unlocker de Bright Data a renvoyé 10 pages Plus Store sur 10 là-bas. Utilisez des IPs coréennes, car Naver enregistrait le pays de chaque demandeur.
Comment scraper Naver avec Python ?
Récupérez la page, puis analysez le JSON intégré dans le HTML au lieu d’utiliser des sélecteurs CSS. Les pages de recherche stockaient les données shopping dans des objets _INITIAL_STATE, et SmartStore utilisait window.__PRELOADED_STATE__. Naver Shopping bloquait les clients HTTP Python, routez donc ces requêtes via Web Unlocker de Bright Data.
Combien coûte le scraping de Naver ?
L’API SERP et Web Unlocker de Bright Data facturent par requête réussie, l’API Naver Shopping Scraper facture par enregistrement, et Browser API facture par Go. Dans nos tests, une page de résultats représentait 1 requête, et 5 pages supplémentaires via Browser API utilisaient environ 18 Mo. Les tarifs actuels figurent sur la page de tarification.