API endpoints vs HTML : le compromis fondamental sur Walmart
Quand on veut récupérer des données produit Walmart à grande échelle, la première décision est architecturale : faut-il cibler les endpoints API internes de Walmart ou parser le HTML rendu par le navigateur ?
- Endpoints API internes — Walmart expose plusieurs endpoints JSON non documentés (ex.
/mweb/graphql,/api/v1/items). Ces endpoints renvoient des données structurées, mais ils sont agressivement protégés par Akamai. Les signatures bot sont détectées en quelques requêtes, et les tokens de session expirent rapidement. Avantage : données propres. Inconvénient : maintenance constante des reverse-engineering de tokens. - Pages HTML rendues — Les pages produit, catégorie et recherche contiennent un objet JSON caché dans
__NEXT_DATA__. C'est le chemin le plus stable et le plus riche. Avantage : un seul endpoint par type de page, données complètes. Inconvénient : il faut télécharger le HTML complet (~200-400 Ko).
Notre recommandation pragmatique : commencez par __NEXT_DATA__. C'est le meilleur ratio effort/données. Les sections suivantes vous montrent exactement comment.
Structure du catalogue Walmart
Walmart organise son catalogue autour de trois types de pages, chacun avec un schéma d'URL prévisible.
Pages produit : /ip/{slug}/{itemId}
Chaque produit a un identifiant numérique unique (itemId). L'URL canonique ressemble à :
https://www.walmart.com/ip/Ozark-Trail-2-Person-Tent/46457687
Le slug est cosmétique — Walmart redirige vers le slug correct si vous utilisez un slug erroné. Seul le itemId (ici 46457687) compte. Vous pouvez même utiliser l'URL raccourcie https://www.walmart.com/ip/46457687.
C'est sur ces pages que vous trouverez le JSON __NEXT_DATA__ le plus riche : prix, inventaire par magasin, évaluations, vendeur Marketplace, variantes.
Pages catégorie
Les catégories suivent le pattern :
https://www.walmart.com/cp/sporting-goods/4128
Où 4128 est l'ID de catégorie. Les pages catégorie listent des produits avec pagination (paramètres ?page=2, ?page=3…).
Pages de recherche
L'URL de recherche est simple :
https://www.walmart.com/search?q=tent+4+person
La pagination fonctionne avec &page=2. Les résultats de recherche contiennent aussi un __NEXT_DATA__ avec une liste tronquée de produits (prix, thumbnail, itemId).
La stack anti-bot d'Akamai et PerimeterX
Walmart utilise une combinaison redoutable de deux technologies anti-bot :
- Akamai Bot Manager — Injecte des scripts JavaScript qui génèrent un cookie
_abcket un headerakamai_bypass. Akamai analyse les empreintes TLS, les headers HTTP, et les patterns de navigation. Si votre empreinte ne correspond pas à un navigateur réel, vous recevez un HTTP 403 avec une page de challenge. - PerimeterX (HUMAN) — Se concentre sur le comportement de la souris et du clavier. PerimeterX pose un challenge invisible (
_px3cookie) que seuls les vrais navigateurs résolvent naturellement.
En pratique, les datacenter IPs sont bloquées quasi immédiatement. Les services de résolution de challenges Akamai existent, mais elles coûtent cher et nécessitent une maintenance constante à chaque mise à jour de script.
Pourquoi les proxies résidentiels sont indispensables
Les IPs résidentielles proviennent de vrais FAI (Comcast, AT&T, Orange…). Akamai et PerimeterX ne peuvent pas les bloquer sans bloquer de vrais clients. C'est la seule approche qui fonctionne de manière fiable à long terme.
Avec des proxies résidentiels rotatifs, chaque requête sort d'une IP différente, ce qui empêche l'accumulation de patterns suspects sur une seule IP.
| Type de proxy | Taux de succès sur Walmart | Coût | Recommandation |
|---|---|---|---|
| Datacenter | 5-15 % | Low | ❌ Inutile pour Walmart |
| Résidentiel rotatif | 85-95 % | Medium | ✅ Idéal pour le scraping à grande échelle |
| Résidentiel sticky | 90-98 % | Medium | ✅ Meilleur pour les sessions multi-requêtes |
| Mobile | 95-99 % | High | ✅ Meilleur taux, pour les cas critiques |
Configurez votre proxy résidentiel ProxyHat pour le geo-targeting US :
# Format HTTP Proxy
http://user-country-US:password@gate.proxyhat.com:8080
# Exemple curl
curl -x http://user-country-US:password@gate.proxyhat.com:8080 \
"https://www.walmart.com/ip/46457687" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
-H "Accept: text/html,application/xhtml+xml" \
-H "Accept-Language: en-US,en;q=0.9"
__NEXT_DATA__ : le chemin le plus simple vers les données Walmart
Walmart utilise Next.js pour le rendu côté serveur. Next.js injecte un objet JSON complet dans chaque page via une balise <script id="__NEXT_DATA__">. Cet objet contient toutes les données que la page affiche : prix, inventaire, vendeur, évaluations, variantes, SEO metadata.
Le sélecteur CSS pour extraire cette balise :
script#__NEXT_DATA__
Le XPath équivalent :
//script[@id='__NEXT_DATA__']
Voici un extrait typique de la réponse JSON (tronqué pour la lisibilité) :
{
"props": {
"pageProps": {
"initialData": {
"data": {
"id": "46457687",
"name": "Ozark Trail 2-Person Tent",
"priceInfo": {
"currentPrice": { "price": 39.96, "currencyUnit": "USD" },
"priceRangeString": "$39.96"
},
"availability": {
"available": true,
"availableOnline": true
},
"averageRating": 4.2,
"numberOfReviews": 1847,
"sellerId": "0",
"sellerName": "Walmart.com",
"variantCategories": [...]
}
}
}
}
}
C'est un trésor de données structurées — pas besoin de parser le HTML avec des sélecteurs fragiles.
Python : récupérer et parser __NEXT_DATA__
Étape 1 — Récupérer la page via proxy résidentiel
import requests
from bs4 import BeautifulSoup
import json
import time
import random
PROXY = "http://user-country-US:password@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY, "https": PROXY}
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
}
def fetch_walmart_page(item_id: str, session: requests.Session = None) -> dict:
"""Télécharge une page produit Walmart et retourne __NEXT_DATA__."""
url = f"https://www.walmart.com/ip/{item_id}"
s = session or requests.Session()
# Délai aléatoire pour imiter un comportement humain
time.sleep(random.uniform(2, 5))
resp = s.get(url, headers=HEADERS, proxies=PROXIES, timeout=30)
if resp.status_code == 403:
raise Exception(f"Bloqué par anti-bot (403). Changez d'IP ou de session.")
if resp.status_code != 200:
raise Exception(f"HTTP {resp.status_code} pour {url}")
soup = BeautifulSoup(resp.text, "html.parser")
script_tag = soup.find("script", id="__NEXT_DATA__")
if not script_tag:
raise Exception("__NEXT_DATA__ introuvable dans la page.")
return json.loads(script_tag.string)
Étape 2 — Parser les données produit
def parse_product_data(next_data: dict) -> dict:
"""Extrait les données clés depuis __NEXT_DATA__."""
try:
product = next_data["props"]["pageProps"]["initialData"]["data"]
except (KeyError, TypeError):
# Parfois la structure diffère légèrement
product = next_data["props"]["pageProps"]["initialData"]
price_info = product.get("priceInfo", {})
current = price_info.get("currentPrice", {})
availability = product.get("availability", {})
result = {
"item_id": product.get("id", ""),
"name": product.get("name", ""),
"brand": product.get("brand", ""),
"price": current.get("price"),
"currency": current.get("currencyUnit", "USD"),
"price_string": price_info.get("priceRangeString", ""),
"available_online": availability.get("availableOnline", False),
"available_store": availability.get("availableInStore", False),
"average_rating": product.get("averageRating"),
"review_count": product.get("numberOfReviews", 0),
"seller_id": product.get("sellerId", ""),
"seller_name": product.get("sellerName", ""),
"is_marketplace": product.get("sellerId", "0") != "0",
}
# Variantes (tailles, couleurs, etc.)
variants = []
for vc in product.get("variantCategories", []):
for v in vc.get("variants", []):
variants.append({
"variant_id": v.get("id", ""),
"name": v.get("name", ""),
"availability": v.get("availability", {}).get("availableOnline", False),
})
result["variants"] = variants
return result
# Utilisation
next_data = fetch_walmart_page("46457687")
product = parse_product_data(next_data)
print(json.dumps(product, indent=2))
Résultat typique :
{
"item_id": "46457687",
"name": "Ozark Trail 2-Person Tent",
"brand": "Ozark Trail",
"price": 39.96,
"currency": "USD",
"price_string": "$39.96",
"available_online": true,
"available_store": true,
"average_rating": 4.2,
"review_count": 1847,
"seller_id": "0",
"seller_name": "Walmart.com",
"is_marketplace": false,
"variants": []
}
Marketplace Walmart : vendeurs 3P vs catalogue 1P
Walmart Marketplace permet à des vendeurs tiers (3P) de vendre sur walmart.com. C'est crucial pour les équipes d'intelligence retail : un même produit peut avoir des dizaines d'offres avec des prix différents.
Identifier 1P vs 3P
Dans __NEXT_DATA__, le champ sellerId est la clé :
sellerId = "0"→ Vendu par Walmart (1P).sellerNameaffiche « Walmart.com ».sellerId = "F55CBD31B7524..."→ Vendu par un vendeur Marketplace (3P).
Le flag is_marketplace dans notre parseur ci-dessus vous permet de filtrer facilement.
Récupérer toutes les offres 3P
Les offres alternatives sont dans product.get("offers", []) ou dans un bloc buyBox. Voici comment les extraire :
def parse_marketplace_offers(product: dict) -> list:
"""Extrait les offres Marketplace (3P sellers)."""
offers = product.get("offers", [])
results = []
for offer in offers:
seller_id = offer.get("sellerId", "")
if seller_id == "0":
continue # Ignorer l'offre 1P Walmart
offer_price = offer.get("priceInfo", {}).get("currentPrice", {})
results.append({
"seller_id": seller_id,
"seller_name": offer.get("sellerName", ""),
"price": offer_price.get("price"),
"shipping": offer.get("shippingPrice", {}).get("price", 0),
"delivery_date": offer.get("fulfillment", {}).get("deliveryDate", ""),
"available": offer.get("availability", {}).get("availableOnline", False),
})
return results
Pour les équipes CPG, cette distinction est essentielle : le prix 1P Walmart est souvent le prix de référence, tandis que les prix 3P révèlent la dynamique concurrentielle sur Marketplace.
Planification respectueuse des rate limits
Walmart n'affiche pas publiquement ses limites de débit, mais nos tests montrent les seuils approximatifs suivants :
- ~200 requêtes/heure/IP avant le premier challenge Akamai (datacenter).
- ~400-600 requêtes/heure/IP avant le premier challenge avec des IPs résidentielles.
- Au-delà de ces seuils, vous recevez des HTTP 403 avec une page de challenge JavaScript.
La stratégie optimale combine rotation d'IP, délais aléatoires et backoff exponentiel.
Scheduler avec backoff et rotation
import requests
import json
import time
import random
import logging
from datetime import datetime
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("walmart_scraper")
PROXY = "http://user-country-US:password@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY, "https": PROXY}
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0.0.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9",
"Accept-Language": "en-US,en;q=0.9",
}
class WalmartScraper:
def __init__(self, max_concurrent=5, requests_per_minute=8):
self.session = requests.Session()
self.max_concurrent = max_concurrent
self.rpm = requests_per_minute
self.min_delay = 60.0 / self.rpm # Délai minimum entre requêtes
self.last_request_time = 0
self.consecutive_403s = 0
def _wait(self):
"""Attend le délai minimum entre deux requêtes."""
elapsed = time.time() - self.last_request_time
jitter = random.uniform(0.5, 3.0)
wait = max(0, self.min_delay - elapsed) + jitter
time.sleep(wait)
def _backoff(self, attempt: int):
"""Backoff exponentiel après un 403."""
delay = min(300, (2 ** attempt) + random.uniform(1, 10))
logger.warning(f"403 détecté. Backoff de {delay:.1f}s (tentative {attempt})")
time.sleep(delay)
def fetch_item(self, item_id: str, max_retries: int = 3) -> dict | None:
"""Récupère un produit avec retry et backoff."""
for attempt in range(max_retries):
self._wait()
self.last_request_time = time.time()
try:
url = f"https://www.walmart.com/ip/{item_id}"
resp = self.session.get(url, headers=HEADERS, proxies=PROXIES, timeout=30)
if resp.status_code == 200:
self.consecutive_403s = 0
soup = BeautifulSoup(resp.text, "html.parser")
script = soup.find("script", id="__NEXT_DATA__")
if script:
return json.loads(script.string)
logger.error(f"__NEXT_DATA__ manquant pour {item_id}")
return None
elif resp.status_code == 403:
self.consecutive_403s += 1
self._backoff(attempt)
continue
else:
logger.error(f"HTTP {resp.status_code} pour {item_id}")
return None
except requests.RequestException as e:
logger.error(f"Erreur réseau pour {item_id}: {e}")
time.sleep(random.uniform(5, 15))
logger.error(f"Échec définitif pour {item_id} après {max_retries} tentatives")
return None
def scrape_items(self, item_ids: list[str]) -> list[dict]:
"""Scrape une liste d'item IDs avec rate limiting."""
results = []
for i, item_id in enumerate(item_ids):
logger.info(f"[{i+1}/{len(item_ids)}] Scraping {item_id}...")
next_data = self.fetch_item(item_id)
if next_data:
product = parse_product_data(next_data)
results.append(product)
return results
# Utilisation
scraper = WalmartScraper(requests_per_minute=8)
item_ids = ["46457687", "55352167", "191797462"]
products = scraper.scrape_items(item_ids)
for p in products:
print(f"{p['name']}: {p['price_string']} (3P: {p['is_marketplace']})")
Conseils de scheduling :
- Limitez à 8 requêtes/minute/IP pour rester sous le radar.
- Utilisez des sessions sticky ProxyHat pour les séquences multi-requêtes (ex. parcours catégorie → produit) :
http://user-country-US-session-abc123:password@gate.proxyhat.com:8080 - Variez les User-Agents et les headers Accept entre les sessions.
- Évitez de scraper entre 00h-06h EST — les mises à jour de prix sont fréquentes et les défenses plus agressives.
- Pour le monitoring de prix en continu, vérifiez chaque produit toutes les 2-4 heures plutôt qu'en temps réel.
Points clés à retenir
1.
__NEXT_DATA__est votre meilleure amie — un seul JSON structuré avec prix, inventaire, vendeur, évaluations.2. Les datacenter IPs ne fonctionnent pas sur Walmart. Les proxies résidentiels sont obligatoires.
3. Distinguez 1P (
sellerId = "0") et 3P Marketplace — les dynamiques de prix sont radicalement différentes.4. Respectez les rate limits : ~8 req/min/IP, backoff exponentiel sur 403, délais aléatoires.
5. Utilisez des sessions sticky ProxyHat pour les séquences multi-requêtes et la rotation pour le scaling.
Pour aller plus loin, consultez notre guide de scraping web ou explorez les tarifs ProxyHat pour choisir le plan adapté à votre volume.






