なぜWalmartスクレイピングは難しいのか
Walmart.comは月間4億4,000万以上の訪問を持つ米国最大級のECプラットフォームです。CPGブランドやリテールインテリジェンスチームにとって、価格・在庫・レビュー・出品者データの継続的な取得は競合分析の生命線です。しかし、WalmartはAkamai Bot ManagerとPerimeterX(現HUMAN)の二重プロテクションを採用しており、素のHTTPリクエストや安価なデータセンタープロキシではすぐにブロックされます。
本記事では、Walmartの商品カタログ構造を理解し、__NEXT_DATA__という隠しJSONを活用する最短ルート、そしてresidential proxyを使った安定したデータ取得パイプラインの構築までを解説します。
Walmartの商品カタログ構造
商品ページ(Item Pages)
Walmartの商品ページは次のURLパターンを持ちます:
https://www.walmart.com/ip/{slug}/{itemId}
itemIdは9〜13桁の数値IDで、Walmart内部の主キーです。slug部分はSEO用で、省略してもリダイレクトされます。例:https://www.walmart.com/ip/Great-Value-Whole-Vitamin-D-Milk/10450109
カテゴリページ
カテゴリツリーは次のような構造です:
https://www.walmart.com/cp/food/976759
https://www.walmart.com/cp/household-essentials/1115193
各カテゴリページは最大40アイテムを表示し、ページネーションは?page=2形式です。ただし、カテゴリページのHTMLはJavaScriptレンダリングに依存する部分が多く、後述の__NEXT_DATA__の活用が必須になります。
検索ページ
検索URLパターン:
https://www.walmart.com/search?q=organic+milk&page=1&sort=price_low
主要クエリパラメータ:
q— 検索キーワードpage— ページ番号sort—price_low,price_high,rating,best_seller,relevanceaffinityOverride—defaultでパーソナライズ無効
APIエンドポイント vs HTML:トレードオフ
Walmartには内部APIがありますが、これらは認証付きで頻繁に変更されます。HTMLスクレイピングはフロントエンド変更のリスクがあるものの、__NEXT_DATA__を活用すればJSONとして構造化データを取得できるため、実はAPIよりも安定しています。
| アプローチ | 利点 | 欠点 | 推奨度 |
|---|---|---|---|
| 内部API直接呼び出し | 構造化済みJSON | 認証・エンドポイント変更リスク高 | ★★☆ |
| HTML + __NEXT_DATA__ | 構造化JSON・フロントエンドと同期 | HTML全体のDLが必要 | ★★★ |
| HTML + CSS/XPath | 柔軟なセレクタ | レイアウト変更に弱い | ★☆☆ |
| ヘッドレスブラウザ | JS実行済み | 遅い・コスト高 | ★★☆ |
Akamai + PerimeterX:Walmartの二重防御
Walmartは2つの主要なボット防御システムをデプロイしています:
Akamai Bot Manager
AkamaiはTLSフィンガープリント、HTTP/2フレーム順序、Sec-*ヘッダーの整合性を検査します。PythonのrequestsライブラリのデフォルトTLSフィンガープリントは即座に検出されます。対策としてcurl_cffiやtls-clientのようなブラウザTLSフィンガープリントを模倣するライブラリが必要です。
PerimeterX(HUMAN)
PerimeterXは主にクライアントサイドのチャレンジを配信します。_px3クッキーとJavaScriptチャレンジを通じて、ブラウザ環境の正当性を検証します。サーバーサイドのみのリクエストではこのクッキーを取得できないため、セッション初期化の工夫が必要です。
なぜresidential proxyが必要か
データセンタープロキシのIPはAkamaiのIPレピュテーションデータベースで即座にフラグが立ちます。residential proxyは実際のISP IPアドレスを使用するため、AkamaiのIPレピュテーションチェックを通過します。Walmartスクレイピングにおいて、residential proxyは「推奨」ではなく「必須」です。
重要:Walmartへのリクエストでは、米国のresidential IPを使用してください。海外IPはリダイレクトまたはブロックされる可能性が高いです。
__NEXT_DATA__:最も簡単なパースパス
Walmart.comはNext.jsで構築されており、各ページのHTMLに<script id="__NEXT_DATA__">タグとして完全な商品データJSONが埋め込まれています。これはAPIレスポンスと同等の構造化データを含んでいます。
CSSセレクタやXPathでHTMLをパースする必要はありません。__NEXT_DATA__を抽出するだけで、価格・在庫・レビュー・出品者情報がすべてJSONとして取得できます。
典型的な__NEXT_DATA__の構造(商品ページ):
{
"props": {
"pageProps": {
"initialData": {
"data": {
"product": {
"itemId": "10450109",
"name": "Great Value Whole Vitamin D Milk",
"priceInfo": {
"currentPrice": { "price": 3.36, "unitOfMeasure": "Each" },
"priceRangeString": "$3.36"
},
"availability": {
"available": true,
"maxQuantity": 12
},
"rating": { "averageRating": 4.2, "numberOfReviews": 1847 },
"sellerId": "0",
"sellerName": "Walmart"
}
}
}
}
}
}
主要フィールドのXPath / CSSセレクタは不要です。JSONキーをたどるだけ:
- 価格:
product.priceInfo.currentPrice.price - 在庫:
product.availability.available - レビュー平均:
product.rating.averageRating - レビュー数:
product.rating.numberOfReviews - 出品者ID:
product.sellerId("0"= Walmart直営)
Python実装:__NEXT_DATA__の取得とパース
以下のPythonスクリプトは、ProxyHatのresidential proxy経由でWalmart商品ページを取得し、__NEXT_DATA__から商品データを抽出します。
ステップ1:セッションの初期化
import json
import re
import time
import random
from curl_cffi import requests as curl_requests
# ProxyHat residential proxy(米国IP)
PROXY_URL = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY_URL, "https": PROXY_URL}
# ブラウザ風ヘッダー
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,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
}
session = curl_requests.Session(impersonate="chrome124")
ステップ2:商品データの取得とパース
def fetch_product(item_id: str) -> dict:
"""Walmart商品ページから__NEXT_DATA__を取得してパース"""
url = f"https://www.walmart.com/ip/{item_id}"
# ランダムな遅延でレート制限を回避
time.sleep(random.uniform(2.0, 5.0))
resp = session.get(url, headers=HEADERS, proxies=PROXIES, timeout=30)
resp.raise_for_status()
# __NEXT_DATA__の抽出
match = re.search(
r'<script id="__NEXT_DATA__"[^>]*>(.*?)</script>',
resp.text,
re.DOTALL
)
if not match:
raise ValueError(f"__NEXT_DATA__ not found for item {item_id}")
next_data = json.loads(match.group(1))
product = next_data["props"]["pageProps"]["initialData"]["data"]["product"]
return {
"item_id": product["itemId"],
"name": product["name"],
"price": product["priceInfo"]["currentPrice"]["price"],
"available": product["availability"]["available"],
"max_quantity": product["availability"].get("maxQuantity", None),
"avg_rating": product["rating"]["averageRating"],
"review_count": product["rating"]["numberOfReviews"],
"seller_id": product.get("sellerId", None),
"seller_name": product.get("sellerName", "Walmart"),
}
# 使用例
product = fetch_product("10450109")
print(json.dumps(product, indent=2))
ステップ3:検索結果の一括取得
def fetch_search(query: str, page: int = 1) -> list[dict]:
"""Walmart検索結果から商品一覧を取得"""
url = f"https://www.walmart.com/search?q={query}&page={page}"
time.sleep(random.uniform(3.0, 6.0))
resp = session.get(url, headers=HEADERS, proxies=PROXIES, timeout=30)
resp.raise_for_status()
match = re.search(
r'<script id="__NEXT_DATA__"[^>]*>(.*?)</script>',
resp.text,
re.DOTALL
)
if not match:
raise ValueError("__NEXT_DATA__ not found in search results")
next_data = json.loads(match.group(1))
# 検索結果の構造は商品ページと異なる
search_results = (
next_data["props"]["pageProps"]
["initialData"]["searchResult"]
["itemStacks"][0]["items"]
)
items = []
for item in search_results:
items.append({
"item_id": item.get("id"),
"name": item.get("name", ""),
"price": item.get("price", {}).get("current", None),
"avg_rating": item.get("rating", {}).get("averageRating"),
"review_count": item.get("rating", {}).get("numberOfReviews"),
})
return items
# 使用例:カテゴリ内の全商品を取得
all_items = []
for page in range(1, 6): # 最大5ページ
items = fetch_search("organic milk", page=page)
all_items.extend(items)
print(f"Page {page}: {len(items)} items fetched")
Walmart Marketplace:3P出品者 vs 1Pカタログ
Walmart.comの商品は、Walmart直営(1P)とMarketplace出品者(3P)が混在しています。__NEXT_DATA__ではsellerIdフィールドで判別できます:
- sellerId = "0" → Walmart直営(1P)
- sellerId = その他の数値 → Marketplace出品者(3P)
3P出品者データの抽出
Marketplace商品の場合、__NEXT_DATA__内のoffers配列に全出品者情報が含まれます:
def extract_offers(product_data: dict) -> list[dict]:
"""1P/3P出品者情報を抽出"""
offers = product_data.get("offers", [])
results = []
for offer in offers:
results.append({
"seller_id": offer.get("sellerId"),
"seller_name": offer.get("sellerName", ""),
"price": offer.get("priceInfo", {}).get("currentPrice", {}).get("price"),
"available": offer.get("availability", {}).get("available", False),
"shipping": offer.get("shipping", {}).get("fulfillmentPrice", None),
"is_walmart_fulfilled": offer.get("fulfillment", {}).get("isFulfilledByWalmart", False),
})
return results
# 3P出品者データは競合価格分析に直結
# 同一itemIdで複数出品者が存在する場合はoffers配列を確認
1Pと3Pの分析上の違い
| 項目 | 1P(Walmart直営) | 3P(Marketplace) |
|---|---|---|
| 価格戦略 | EDLP(Everyday Low Price) | 出品者ごとに変動 |
| 在庫データ | Walmart倉庫在庫 | 出品者在庫(リアルタイム性が低い) |
| 配送 | Walmart Fulfillment | 出品者配送またはWFS |
| 返品ポリシー | Walmart標準 | 出品者ごとに異なる |
| データポイント数 | 少ない(1SKU = 1価格) | 多い(1SKU = 複数価格) |
CPGブランドにとって、3P出品者の価格はMAP(Minimum Advertised Price)違反の監視に直結します。1P価格だけではなく、3P出品者の価格も継続的にモニタリングする必要があります。
レート制限対応とスケジューリング
Walmartのレート制限はAkamaiによって動的に管理されます。明示的な「1分間にXリクエスト」という公開値はありませんが、実測ベースでのガイドラインがあります:
実測ベースのレート制限しきい値
- 同一IP・1分間:10〜15リクエストが安全圏。20リクエストを超えるとCAPTCHAまたは403の確率が急増
- 同一IP・1時間:200〜300リクエストが上限目安
- 検索ページ:商品ページより厳しく、5リクエスト/分が安全圏
推奨スケジューリング戦略
大規模なデータ取得では、IPローテーションとリクエスト間隔の両方を管理します:
import time
import random
from collections import deque
class RateLimitedScraper:
"""Walmart向けレート制限対応スクレイパー"""
def __init__(self, max_per_minute=8, max_per_hour=180):
self.request_times = deque()
self.hour_times = deque()
self.max_per_minute = max_per_minute
self.max_per_hour = max_per_hour
def wait_if_needed(self):
"""レート制限内に収まるよう待機"""
now = time.time()
# 1分間の制限チェック
while self.request_times and now - self.request_times[0] > 60:
self.request_times.popleft()
if len(self.request_times) >= self.max_per_minute:
sleep_time = 60 - (now - self.request_times[0]) + random.uniform(1, 3)
print(f"Rate limit: sleeping {sleep_time:.1f}s")
time.sleep(sleep_time)
# 1時間の制限チェック
while self.hour_times and now - self.hour_times[0] > 3600:
self.hour_times.popleft()
if len(self.hour_times) >= self.max_per_hour:
sleep_time = 3600 - (now - self.hour_times[0]) + random.uniform(10, 30)
print(f"Hourly limit: sleeping {sleep_time:.1f}s")
time.sleep(sleep_time)
# ランダムな間隔を追加(ボット検知回避)
time.sleep(random.uniform(1.5, 4.0))
now = time.time()
self.request_times.append(now)
self.hour_times.append(now)
def fetch_product_safe(self, item_id: str) -> dict:
"""レート制限対応済みの商品取得"""
self.wait_if_needed()
return fetch_product(item_id) # 前述の関数を利用
# 大量商品のバッチ処理
scraper = RateLimitedScraper(max_per_minute=8, max_per_hour=180)
item_ids = ["10450109", "55864531", "34890857"] # 例
for item_id in item_ids:
try:
data = scraper.fetch_product_safe(item_id)
print(f"✓ {item_id}: ${data['price']}")
except Exception as e:
print(f"✗ {item_id}: {e}")
スティッキーセッションの活用
PerimeterXのボット検知は、セッション内での行動パターンも評価します。同じIPで複数リクエストを送る方が、リクエストごとにIPを変えるよりも自然に見えます。ProxyHatのスティッキーセッション機能を使うと、特定のIPを10〜30分間保持できます:
# スティッキーセッション付きプロキシ(10〜30分間同じIPを維持)
STICKY_PROXY = "http://user-country-US-session-mybatch01:PASSWORD@gate.proxyhat.com:8080"
# バッチごとにセッションIDを変更
# → 各バッチは異なるresidential IPを使用
# → バッチ内は同じIPで自然な閲覧行動をシミュレート
スティッキーセッションの使い分け:
- 商品ページの連続取得:スティッキーセッション(1セッション = 1カテゴリの全商品)
- 検索ページ:スティッキーセッション(1セッション = 1検索クエリの全ページ)
- 長時間のバッチ処理:セッションIDを30分ごとにローテーション
エラーハンドリングとCAPTCHA対応
Walmartスクレイピングで遭遇する主なエラー:
| ステータスコード | 原因 | 対応 |
|---|---|---|
| 403 | Akamaiボット検知 | IP/セッションを変更してリトライ |
| 200 + CAPTCHAページ | PerimeterXチャレンジ | IPを変更してリトライ |
| 200 + 空の__NEXT_DATA__ | 地域制限または在庫なし | US IPを確認、商品IDを検証 |
| 429 | レート制限 | 指数バックオフでリトライ |
| 503 | 一時的なサーバーエラー | 30秒〜1分後にリトライ |
CAPTCHAページの検出方法:
def is_captcha_page(html: str) -> bool:
"""PerimeterX CAPTCHAページかどうかを判定"""
captcha_signals = [
"px-captcha",
"_px",
"challenge-platform",
"cf-challenge", # Cloudflare(Walmartでは稀)
]
return any(signal in html.lower() for signal in captcha_signals)
Key Takeaways
Walmartスクレイピングの要点:
- __NEXT_DATA__を活用せよ:HTMLパースではなく、Next.jsの埋め込みJSONからデータを取得する。CSS/XPathセレクタの変更リスクを回避できる。
- residential proxyは必須:AkamaiのIPレピュテーションチェックにより、データセンタープロキシは即座にブロックされる。米国residential IPを使用すること。
- curl_cffiでTLSフィンガープリントを模倣:標準の
requestsライブラリはAkamaiに検出される。curl_cffiのimpersonate機能を使うこと。- 1Pと3Pを区別:
sellerId = "0"がWalmart直営。Marketplace商品はoffers配列に複数出品者が含まれる。- レート制限を意識:1IPあたり8リクエスト/分、180リクエスト/時間が安全圏。スティッキーセッションで自然な行動パターンをシミュレート。
Walmartの商品データを安定して取得するには、適切なプロキシインフラが不可欠です。ProxyHatのresidential proxyプランは、米国ISP IPでのWalmartアクセスに最適化されています。また、ウェブスクレイピングのユースケースページでは、他のECサイトのスクレイピング事例も確認できます。






