プロキシを使ったAmazonキーワード順位追跡:開発者向け完全ガイド

Amazonの検索結果は独自のSERPを持ち、関連性と販売速度でランキングが決まります。本記事では、Pythonとプロキシを使ってASINごとのオーガニック順位を追跡する実装方法を解説します。

Amazon Keyword Rank Tracking with Proxies: A Developer's Guide

プロキシを使ったAmazonキーワード順位追跡とは

Amazonの検索結果ページ(SERP)はGoogleとは異なる独自のランキングアルゴリズムを持ちます。販売速度、レビュー数、在庫状況、価格競争力などが複合的に影響し、同じキーワードでも日単位で順位が変動します。Amazonセラーにとって、自社ASINがどのキーワードで何位に表示されているかを継続的に把握することは、リスティング最適化とPPC戦略の基盤となります。

プロキシを使ったAmazonキーワード順位追跡は、複数のマーケットプレイスに対してローカライズされた検索リクエストを送信し、HTMLを解析してASINごとのオーガニック表示位置を記録する手法です。本記事では、PythonとSERP追跡向けのプロキシインフラを組み合わせた実践的な実装を紹介します。

⚠️ 法的注意事項:本記事は公開検索データの収集のみを対象とします。米国ではComputer Fraud and Abuse Act (CFAA)がアクセス制限の迂回に該当する可能性があり、EUではGDPRが個人データの処理を制限します。robots.txtの尊重、利用規約の確認、適切なレート制限の遵守を行ってください。可能な場合はAmazon公式のSP-APIの使用を推奨します。

なぜAmazon SERPは独自なのか

AmazonのA9アルゴリズム(現在はA10とも呼ばれる)は、GoogleのPageRank系アルゴリズムとは根本的に異なります。販売速度コンバージョン率がランキングの主要因であり、テキストの関連性スコアは二次的な役割を果たします。これにより、以下の特徴が生まれます。

  • 同じキーワードでもマーケットプレイスごとに結果が大きく異なる(amazon.com vs amazon.de)
  • スポンサープロダクト(広告枠)がオーガニック結果の前に挿入される
  • 在庫切れや価格変更で数時間以内に順位が変動する
  • ページネーションによる位置変動が頻繁に発生する

セラーはキーワード、マーケットプレイス、ASINの3軸で順位を追跡する必要があります。これを手動で行うのは非現実的であり、自動化にはプロキシが不可欠です。

結果ページの解析:data-component-typeとdata-asin

Amazonの検索結果ページは、各商品カードを [data-component-type="s-search-result"] 属性で識別できます。各カード内には data-asin 属性でASINが格納されており、これを基にオーガニック位置を計算します。

重要なのは、スポンサープロダクトとオーガニック結果を区別することです。スポンサープロダクトにも同じ data-component-type が使われますが、カード内に「Sponsored」ラベルが含まれます。これを検出しないと、広告枠をオーガニック順位として誤記録してしまいます。

HTML構造の概要

<div data-component-type="s-search-result" data-asin="B08N5WRWNW">
  <div class="puis-card-container">
    <span class="a-color-secondary">Sponsored</span>
    <!-- オーガニック結果にはこのspanがない -->
    <h2 class="a-size-mini">
      <a class="a-link-normal" href="/dp/B08N5WRWNW/...">Product Title</a>
    </h2>
  </div>
</div>

以下のPython関数は、BeautifulSoupを使ってこの構造を解析し、スポンサープロダクトを除外したオーガニック順位を返します。

from bs4 import BeautifulSoup
import re

def parse_amazon_serps(html: str, target_asin: str) -> dict:
    """Amazon検索結果ページからASINのオーガニック位置を抽出する"""
    soup = BeautifulSoup(html, "html.parser")
    results = soup.find_all(attrs={"data-component-type": "s-search-result"})

    organic_position = None
    sponsored_positions = []
    total_results = 0

    for idx, card in enumerate(results, start=1):
        asin = card.get("data-asin", "")
        if not asin:
            continue

        # Sponsoredラベルの検出
        card_text = card.get_text(separator=" ", strip=True)
        is_sponsored = "Sponsored" in card_text

        if is_sponsored:
            sponsored_positions.append({"asin": asin, "position": idx})
            continue

        total_results += 1
        if asin == target_asin:
            organic_position = total_results

    return {
        "target_asin": target_asin,
        "organic_position": organic_position,
        "total_organic": total_results,
        "sponsored_count": len(sponsored_positions),
        "is_ranking": organic_position is not None,
    }

なぜプロキシが必要なのか:アンチボットとローカライゼーション

Amazonは強力なアンチボットシステムを運用しており、同じIPからの高頻度リクエストを検出するとCAPTCHAチャレンジやIPブロックを実行します。また、検索結果はリクエスト元のIP地理位置に基づいてローカライズされるため、amazon.deの正確な順位を取得するにはドイツのIPが必要です。

プロキシロケーションの選択は、追跡対象のマーケットプレイスと一致させる必要があります。以下は主要マーケットプレイスと推奨geoタグの対応表です。

マーケットプレイスドメインProxyHat国コードユーザー名例
米国amazon.comUSuser-country-US
ドイツamazon.deDEuser-country-DE
英国amazon.co.ukGBuser-country-GB
日本amazon.co.jpJPuser-country-JP
フランスamazon.frFRuser-country-FR

さらに、ページネーション(1ページ目から5ページ目まで順次取得)を行う場合、同一セッションIDを使うことでAmazon側の一貫した検索コンテキストを維持できます。ProxyHatでは -session- フラグでスティッキーセッションを指定します。

# スティッキーセッションの例(ページネーション用)
# 同一session IDで5ページ分取得 → 同じIPが使われる
SESSION_ID = "amz-track-abc123"

# 米国マーケットプレイス用
proxy_url_us = "http://user-country-US-session-{sid}:pass@gate.proxyhat.com:8080".format(sid=SESSION_ID)

# ドイツマーケットプレイス用
proxy_url_de = "http://user-country-DE-session-{sid}:pass@gate.proxyhat.com:8080".format(sid=SESSION_ID)

実装例:curl_cffi + ProxyHatでキーワード順位を追跡

以下は、curl_cffi(TLSフィンガープリントを模倣するHTTPクライアント)とProxyHatプロキシを組み合わせて、指定キーワードの1〜5ページを取得し、ターゲットASINのオーガニック位置を記録する完全な例です。

import csv
import time
import logging
from datetime import datetime, timezone
from curl_cffi import requests as cffi_requests
from parse_module import parse_amazon_serps  # 上記で定義した関数

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

# ProxyHat接続設定
PROXYHAT_GATEWAY = "gate.proxyhat.com"
PROXYHAT_PORT = 8080
PROXYHAT_USER = "your_username"
PROXYHAT_PASS = "your_password"

def build_proxy_url(country: str, session_id: str) -> str:
    """国コードとセッションIDからプロキシURLを構築する"""
    username = "{user}-country-{country}-session-{sid}".format(
        user=PROXYHAT_USER, country=country, sid=session_id
    )
    return "http://{user}:{pass}@{host}:{port}".format(
        user=username, pass=PROXYHAT_PASS,
        host=PROXYHAT_GATEWAY, port=PROXYHAT_PORT
    )

def fetch_amazon_search(keyword: str, page: int, country: str, session_id: str) -> str:
    """Amazon検索結果ページのHTMLを取得する"""
    base_urls = {
        "US": "https://www.amazon.com",
        "DE": "https://www.amazon.de",
        "GB": "https://www.amazon.co.uk",
        "JP": "https://www.amazon.co.jp",
        "FR": "https://www.amazon.fr",
    }
    base = base_urls.get(country, "https://www.amazon.com")
    url = "{base}/s?k={kw}&page={pg}&ref=nb_sb_noss_{pg}".format(
        base=base, kw=keyword.replace(" ", "+"), pg=page
    )

    proxy = build_proxy_url(country, session_id)
    headers = {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9" if country == "US" else "de-DE,de;q=0.9",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    }

    response = cffi_requests.get(
        url,
        proxies={"http": proxy, "https": proxy},
        headers=headers,
        impersonate="chrome120",
        timeout=30,
    )
    response.raise_for_status()
    return response.text

def track_keyword_rank(keyword: str, target_asin: str, country: str, max_pages: int = 5):
    """キーワードに対するASINの順位を追跡し、CSVに保存する"""
    session_id = "amz-{ts}".format(ts=int(time.time()))
    rank_history = []

    for page in range(1, max_pages + 1):
        try:
            html = fetch_amazon_search(keyword, page, country, session_id)
            result = parse_amazon_serps(html, target_asin)

            # CAPTCHA検出
            if "api-services-support@amazon.com" in html or "Type the characters" in html:
                logger.warning("CAPTCHA detected on page %d — backing off", page)
                time.sleep(30)
                continue

            if result["is_ranking"]:
                position = (page - 1) * 50 + result["organic_position"]
                logger.info("ASIN %s found at page %d, position %d (global: %d)",
                            target_asin, page, result["organic_position"], position)
                rank_history.append({
                    "timestamp": datetime.now(timezone.utc).isoformat(),
                    "keyword": keyword,
                    "asin": target_asin,
                    "marketplace": country,
                    "page": page,
                    "organic_position": result["organic_position"],
                    "global_position": position,
                    "sponsored_count": result["sponsored_count"],
                })
                break  # 見つかったら終了
            else:
                logger.info("ASIN not found on page %d", page)
                time.sleep(2)  # ページ間の礼儀的な待機

        except Exception as e:
            logger.error("Error on page %d: %s", page, str(e))
            time.sleep(5)
            continue

    # CSVに追記
    if rank_history:
        with open("rank_history.csv", "a", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=rank_history[0].keys())
            if f.tell() == 0:
                writer.writeheader()
            writer.writerows(rank_history)

    return rank_history

# 実行例
if __name__ == "__main__":
    results = track_keyword_rank(
        keyword="wireless earbuds",
        target_asin="B08N5WRWNW",
        country="US",
        max_pages=5,
    )
    print("Tracked {n} entries".format(n=len(results)))

Playwrightを使った代替アプローチ

JavaScriptレンダリングが必要な場合や、より高度なアンチボット回避が必要な場合は、Playwrightが有効な選択肢です。以下はSOCKS5プロキシ(ポート1080)を使用する例です。

from playwright.sync_api import sync_playwright

def fetch_with_playwright(keyword: str, page_num: int, country: str, session_id: str) -> str:
    """Playwright + SOCKS5プロキシでAmazon検索結果を取得"""
    proxy_config = {
        "server": "socks5://gate.proxyhat.com:1080",
        "username": "your_username-country-{c}-session-{s}".format(c=country, s=session_id),
        "password": "your_password",
    }

    base_urls = {
        "US": "https://www.amazon.com",
        "DE": "https://www.amazon.de",
    }
    url = "{base}/s?k={kw}&page={pg}".format(
        base=base_urls.get(country, "https://www.amazon.com"),
        kw=keyword.replace(" ", "+"),
        pg=page_num,
    )

    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            proxy=proxy_config,
        )
        context = browser.new_context(
            locale="en-US" if country == "US" else "de-DE",
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        )
        page = context.new_page()
        page.goto(url, timeout=60000, wait_until="domcontentloaded")
        page.wait_for_selector('[data-component-type="s-search-result"]', timeout=15000)
        html = page.content()
        browser.close()
        return html

curlを使ったクイックテスト

Pythonスクリプトを書く前に、curlでプロキシ接続とHTML構造を素早く確認できます。

# 米国マーケットプレイスの検索結果を取得
# ページ1、キーワード "wireless earbuds"
curl -x "http://your_username-country-US-session-test1:your_password@gate.proxyhat.com:8080" \
  -H "Accept-Language: en-US,en;q=0.9" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://www.amazon.com/s?k=wireless+earbuds&page=1" \
  -o amazon_page1.html --silent --max-time 30

# ドイツマーケットプレイスの検索結果を取得
curl -x "http://your_username-country-DE-session-test2:your_password@gate.proxyhat.com:8080" \
  -H "Accept-Language: de-DE,de;q=0.9" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://www.amazon.de/s?k=bluetooth+kopfhorer&page=1" \
  -o amazon_de_page1.html --silent --max-time 30

プロダクション運用のベストプラクティス

日次スケジューリングとリトライ

順位追跡は日次で実行するのが一般的です。以下は sched と指数バックオフを組み合わせたスケジューラの例です。

import sched
import time
import random
from datetime import datetime, timezone

scheduler = sched.scheduler(time.time, time.sleep)

def run_daily_tracking():
    """日次の順位追跡タスク"""
    keywords = ["wireless earbuds", "bluetooth headphones", "noise cancelling earbuds"]
    target_asins = {"wireless earbuds": "B08N5WRWNW"}
    countries = ["US", "DE", "GB"]

    for kw in keywords:
        for country in countries:
            asin = target_asins.get(kw, "B08N5WRWNW")
            try:
                track_keyword_rank(kw, asin, country, max_pages=5)
            except Exception as e:
                logger.error("Tracking failed for %s/%s: %s", kw, country, e)
                # 指数バックオフ
                backoff = (2 ** 3) + random.uniform(0, 1)
                logger.info("Backing off for %.1f seconds", backoff)
                time.sleep(backoff)
            time.sleep(random.uniform(3, 8))  # リクエスト間のジッター

def schedule_next_run():
    """翌日同時刻に次回実行をスケジュール"""
    now = datetime.now(timezone.utc)
    tomorrow = now.replace(hour=6, minute=0, second=0, microsecond=0)
    delay = (tomorrow - now).total_seconds() + 86400
    scheduler.enter(delay, 1, run_daily_tracking, ())
    scheduler.enter(delay + 1, 2, schedule_next_run, ())

schedule_next_run()
scheduler.run()

CAPTCHA検出とインデックス確認

CAPTCHAページの検出は、以下の文字列をHTML内で探すことで実装できます。

  • api-services-support@amazon.com — CAPTCHAページのフッター
  • Type the characters you see in this image — CAPTCHAプロンプト
  • Robot Check — ボット検出ページのタイトル

CAPTCHAを検出した場合は、30〜60秒の待機後にリトライし、プロキシのセッションIDを変更して別IPで再試行します。連続して3回CAPTCHAに遭遇した場合は、その日の追跡をスキップし、翌日再試行するのが安全な運用方針です。

インデックス確認も重要です。5ページすべてを取得してもASINが見つからない場合、そのキーワードでインデックスされていない可能性があります。これはAmazonの検索インデックスから除外されたことを意味し、リスティングの最適化や在庫状況の確認が必要です。

def check_indexation(keyword: str, target_asin: str, country: str) -> dict:
    """ASINがキーワードでインデックスされているか確認"""
    session_id = "idx-check-{ts}".format(ts=int(time.time()))
    found = False

    for page in range(1, 21):  # 最大20ページ(約1000件)まで確認
        try:
            html = fetch_amazon_search(keyword, page, country, session_id)
            result = parse_amazon_serps(html, target_asin)
            if result["is_ranking"]:
                found = True
                return {
                    "indexed": True,
                    "page": page,
                    "position": result["organic_position"],
                }
            if result["total_organic"] == 0:
                break  # 結果がなくなった
            time.sleep(2)
        except Exception:
            time.sleep(5)
            continue

    return {"indexed": False, "page": None, "position": None}

プロキシタイプの選択

Amazonキーワード順位追跡には、以下のプロキシタイプの比較を参考にしてください。

プロキシタイプ検出リスクローカライゼーション推奨用途
データセンタープロキシ高(AWS/GCP IP範囲はブロックされやすい)不可非推奨
モバイルプロキシ可能(キャリアIP)高頻度追跡
レジデンシャルプロキシ低〜中可能(国・都市)推奨

レジデンシャルプロキシはISPから発行された住宅IPを使用するため、AmazonのアンチボットシステムがデータセンターIP範囲でフィルタリングするのを回避できます。ProxyHatのレジデンシャルプロキシは国レベルのジオターゲティングに対応しており、料金プランは使用量に応じて選択できます。

エシックスと遵守事項

  • 自社のASINと公開リスティングのみを追跡する
  • 1リクエストあたり2〜5秒の間隔を空ける
  • robots.txtを確認し、disallowパスを尊重する
  • Amazon SP-APIで取得可能なデータ(カタログ、価格など)はAPI経由で取得する
  • 取得したデータを個人データとして保存しない(GDPR準拠)
  • 商用利用前にAmazonの利用規約を確認する

詳細なプロキシ設定についてはProxyHat公式ドキュメントを参照してください。Webスクレイピングのユースケースページにも関連情報があります。

Key Takeaways

  • Amazon SERPはGoogleとは独立したアルゴリズムで、販売速度とコンバージョンが主要因
  • [data-component-type="s-search-result"]data-asinでオーガニック位置を抽出し、「Sponsored」ラベルで広告を除外
  • マーケットプレイスごとに国コードを指定したレジデンシャルプロキシが必須(gate.proxyhat.com:8080)
  • ページネーションには-session-IDでスティッキーセッションを使用し、一貫した検索コンテキストを維持
  • CAPTCHA検出、インデックス確認、指数バックオフを本番環境に組み込む
  • 可能な場合はAmazon SP-APIを優先し、公開データのみをスクレイピング対象とする

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

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

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