Scraping Walmart : guide pratique pour données produit, prix et inventaire

Apprenez à scraper Walmart en contournant Akamai et PerimeterX, à extraire __NEXT_DATA__ pour les prix et l'inventaire, et à gérer les vendeurs 3P avec des proxies résidentiels.

Scraping Walmart : guide pratique pour données produit, prix et inventaire

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

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 _abck et un header akamai_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 (_px3 cookie) 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 proxyTaux de succès sur WalmartCoûtRecommandation
Datacenter5-15 %Low❌ Inutile pour Walmart
Résidentiel rotatif85-95 %Medium✅ Idéal pour le scraping à grande échelle
Résidentiel sticky90-98 %Medium✅ Meilleur pour les sessions multi-requêtes
Mobile95-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). sellerName affiche « 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.

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog