Walmartスクレイピング完全ガイド:商品データ取得からAkamai回避まで

Walmartの商品データを安定して取得する実践ガイド。Akamai+PerimeterX対策、__NEXT_DATA__の活用、Marketplace対応、レート制限対応まで解説。

Walmartスクレイピング完全ガイド:商品データ取得からAkamai回避まで

なぜWalmartスクレイピングは難しいのか

Walmart.comは月間4億4,000万以上の訪問を持つ米国最大級のECプラットフォームです。CPGブランドやリテールインテリジェンスチームにとって、価格・在庫・レビュー・出品者データの継続的な取得は競合分析の生命線です。しかし、WalmartはAkamai Bot ManagerPerimeterX(現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 — ページ番号
  • sortprice_low, price_high, rating, best_seller, relevance
  • affinityOverridedefaultでパーソナライズ無効

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_cffitls-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スクレイピングで遭遇する主なエラー:

ステータスコード原因対応
403Akamaiボット検知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_cffiimpersonate機能を使うこと。
  • 1Pと3Pを区別:sellerId = "0"がWalmart直営。Marketplace商品はoffers配列に複数出品者が含まれる。
  • レート制限を意識:1IPあたり8リクエスト/分、180リクエスト/時間が安全圏。スティッキーセッションで自然な行動パターンをシミュレート。

Walmartの商品データを安定して取得するには、適切なプロキシインフラが不可欠です。ProxyHatのresidential proxyプランは、米国ISP IPでのWalmartアクセスに最適化されています。また、ウェブスクレイピングのユースケースページでは、他のECサイトのスクレイピング事例も確認できます。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る