eBayスクレイピング完全ガイド:プロキシ活用からオークション・出品者分析まで

eBayのAPI制限を回避し、検索結果・オークション・出品者データを安定して取得する実践ガイド。プロキシ選定、HTMLパース戦略、Python実装例まで網羅。

eBayスクレイピング完全ガイド:プロキシ活用からオークション・出品者分析まで

なぜeBayスクレイピングが難しいのか

リセラーインテリジェンスや市場調査チームにとって、eBayのデータは金鉱です。しかし、eBayはデータセンターIPを積極的にブロックし、APIには厳格なクォータがあり、HTML構造は頻繁に変更されます。このガイドでは、eBayプロキシの選定から、検索結果のパース、オークション情報の抽出、出品者分析まで、実運用で使えるアプローチを解説します。

API vs HTMLスクレイピング:eBayのトレードオフ

eBayは公式APIを提供していますが、大規模データ収集には致命的な制限があります。

観点eBay Finding/Browse APIHTMLスクレイピング
1日あたり制限5,000コール/日(Finding API)プロキシ次第で実質無制限
データ鮮度最大15分の遅延リアルタイム
オークション残時間非対応または不正確DOMから直接取得可能
出品者フィードバック詳細限定的フル取得可能
Geo別価格差APIは地域コンテキストを反映しない地域プロキシで正確な価格表示
初期コストApp Key申請+OAuth設定即座に開始可能
ブロックリスクなし(公式)あり(プロキシで軽減)

結論:APIは初期プロトタイプや低頻度モニタリングに最適。数千件以上のリスティングを日次で追跡するなら、eBayプロキシ経由のHTMLスクレイピングが現実的な選択です。

eBay APIのレート制限詳細

Finding APIのデフォルト制限は5,000 calls/日。Browse APIはアプリごとに異なりますが、実務上は1秒あたり2〜5リクエスト程度が上限です。1ページ50件としても、1日で25万件が限界——複数カテゴリ・地域を跨ぐ調査には到底足りません。APIが返すデータも、出品ページに表示される「残り時間」「入札数」「Buy It Nowフラグ」を含まないため、オークション追跡には不向きです。

eBayのHTMLターゲット構造

eBayの検索結果ページは比較的安定した構造を持っています。以下のセレクタを押さえておけば、大半のデータを抽出できます。

検索結果グリッド:.s-item

検索結果の各商品は.s-itemクラスの<li>要素に格納されています。

  • タイトル: .s-item__title — 商品名テキスト
  • 価格: .s-item__price — 「$129.99」形式。セール時は.s-item__price--originalに元価格
  • 送料: .s-item__shipping — 「Free shipping」「+$5.99 shipping」
  • URL: .s-item__linkhref属性
  • 画像: .s-item__image-imgsrc
  • 出品者: .s-item__seller-info-text
  • オークション情報: .s-item__bid-count.s-item__time-left
  • Buy It Now: .s-item__buy-it-now要素の有無

出品詳細ページ

個別出品ページ(/itm/ITEM_ID)には、より豊富なデータがあります。

  • 商品説明: #desc_div内のiframeまたはインラインHTML
  • Item Specifics: .ux-layout-section--itemDetails内のdlリスト
  • 入札履歴: .bid-historyテーブル(オークションのみ)
  • 残り時間: #vi-cdown_timeLeftまたは[data-testid="x-countdown-timer"]

出品者プロフィールページ

URLパターン:/str/SELLER_NAMEまたは/usr/SELLER_NAME

  • フィードバックスコア: .feedback-scoreまたは[data-testid="x-feedback-score"]
  • ポジティブ率: .feedback-percent
  • 出品カテゴリ: .seller-categories内のリスト
  • 他の出品一覧: .str-itemsグリッド

プロキシ選定:eBayでレジデンシャル一択の理由

eBayはデータセンターIPに対して非常に攻撃的なブロックを行います。CloudflareベースのWAFに加え、独自のボット検知が動いており、DC IPからのリクエストは数回でCAPTCHA(hCaptcha)または403を返します。

プロキシタイプ別のeBay対応力

プロキシタイプeBayブロック率推奨用途
データセンター90%以上(数リクエストでブロック)非推奨
モバイル(3G/4G/5G)5%未満高価値オークション監視
レジデンシャル10〜20%大規模リスティング収集

地域プロキシの重要性も見逃せません。eBay.deeBay.co.ukなどの地域ドメインは、アクセス元IPの国に基づいて価格・送料・出品visibilityを変えることがあります。ドイツの価格動向を正確に追跡するなら、ドイツのレジデンシャルIPが必要です。

ProxyHatでは、ユーザー名に国・都市フラグを追加するだけでジオターゲティングが可能です:

# ドイツIPでeBay.deにアクセス
http://user-country-DE:pass@gate.proxyhat.com:8080

# ロンドンIPでeBay.co.ukにアクセス
http://user-country-GB-city-london:pass@gate.proxyhat.com:8080

Python実装:eBay検索結果スクレイパー

以下は、ProxyHatのレジデンシャルプロキシ経由でeBay検索結果を取得し、.s-itemから構造化レコードを抽出する完全な例です。

import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass, field
from typing import Optional
import time, random

@dataclass
class EbayListing:
    title: str
    price: Optional[float]
    shipping: Optional[str]
    url: str
    item_id: str
    bid_count: Optional[int] = None
    time_left: Optional[str] = None
    is_buy_it_now: bool = False
    seller: Optional[str] = None

def get_proxy_url(country: str = "US", session: str = None) -> str:
    """ProxyHatレジデンシャルプロキシURLを生成"""
    user_part = f"user-country-{country}"
    if session:
        user_part += f"-session-{session}"
    return f"http://{user_part}:YOUR_PASSWORD@gate.proxyhat.com:8080"

def scrape_ebay_search(
    query: str,
    country: str = "US",
    max_pages: int = 5,
) -> list[EbayListing]:
    """eBay検索結果をスクレイピング"""
    listings = []
    proxy = get_proxy_url(country=country)
    proxies = {"http": proxy, "https": proxy}

    headers = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/124.0.0.0 Safari/537.36"
        ),
        "Accept-Language": "en-US,en;q=0.9",
    }

    base_domain = "ebay.com" if country == "US" else f"ebay.{country.lower()}"

    for page in range(1, max_pages + 1):
        url = (
            f"https://www.{base_domain}/sch/i.html"
            f"?_nkw={query}&_pgn={page}"
        )
        resp = requests.get(url, headers=headers, proxies=proxies, timeout=30)
        resp.raise_for_status()

        soup = BeautifulSoup(resp.text, "html.parser")
        items = soup.select(".s-item")

        if not items:
            break  # ページ終了またはブロック

        for item in items:
            title_el = item.select_one(".s-item__title")
            if not title_el or "Shop on eBay" in title_el.text:
                continue

            link_el = item.select_one(".s-item__link")
            price_el = item.select_one(".s-item__price")
            shipping_el = item.select_one(".s-item__shipping")
            bid_el = item.select_one(".s-item__bid-count")
            time_el = item.select_one(".s-item__time-left")
            bin_el = item.select_one(".s-item__buy-it-now")
            seller_el = item.select_one(".s-item__seller-info-text")

            # item_idをURLから抽出
            item_id = ""
            if link_el and link_el.get("href"):
                href = link_el["href"]
                if "/itm/" in href:
                    item_id = href.split("/itm/")[1].split("?")[0]

            # 価格をパース
            price = None
            if price_el:
                price_text = price_el.text.replace("$", "").replace(",", "").strip()
                try:
                    price = float(price_text.split(" to ")[0])
                except ValueError:
                    pass

            # 入札数をパース
            bid_count = None
            if bid_el:
                try:
                    bid_count = int("".join(filter(str.isdigit, bid_el.text)))
                except ValueError:
                    pass

            listings.append(EbayListing(
                title=title_el.text.strip() if title_el else "",
                price=price,
                shipping=shipping_el.text.strip() if shipping_el else None,
                url=link_el["href"] if link_el else "",
                item_id=item_id,
                bid_count=bid_count,
                time_left=time_el.text.strip() if time_el else None,
                is_buy_it_now=bin_el is not None,
                seller=seller_el.text.strip() if seller_el else None,
            ))

        # 礼儀的な遅延
        time.sleep(random.uniform(2, 5))

    return listings

# 実行例
results = scrape_ebay_search("vintage rolex", country="US", max_pages=3)
for r in results[:5]:
    print(f"{r.title[:50]}... ${r.price} | Bids: {r.bid_count} | BIN: {r.is_buy_it_now}")

このスクリプトは、各ページの.s-item要素を走査し、タイトル・価格・送料・入札数・残り時間・Buy It Nowフラグを構造化データとして返します。プロキシは国別に切り替え可能で、country="DE"を指定すればeBay.deのドイツ語結果を取得できます。

オークションデータの詳細抽出

オークション追跡では、残り時間と入札数のリアルタイム性が重要です。APIはこれらを提供しないため、HTMLスクレイピングが唯一の選択肢になります。

出品詳細ページからのオークション情報

def parse_auction_detail(html: str) -> dict:
    """個別出品ページからオークション詳細を抽出"""
    soup = BeautifulSoup(html, "html.parser")

    auction = {
        "time_left": None,
        "bid_count": None,
        "current_bid": None,
        "is_buy_it_now": False,
        "bin_price": None,
        "bid_history": [],
    }

    # 残り時間 — カウントダウンタイマー
    countdown = soup.select_one("[data-testid='x-countdown-timer']")
    if not countdown:
        countdown = soup.select_one("#vi-cdown_timeLeft")
    if countdown:
        auction["time_left"] = countdown.get_text(strip=True)

    # 入札数
    bid_el = soup.select_one(".bid-count")
    if not bid_el:
        bid_el = soup.select_one("[data-testid='x-bid-count']")
    if bid_el:
        try:
            auction["bid_count"] = int(
                "".join(filter(str.isdigit, bid_el.text))
            )
        except ValueError:
            pass

    # 現在価格
    price_el = soup.select_one("[data-testid='x-price-primary']")
    if price_el:
        auction["current_bid"] = price_el.text.strip()

    # Buy It Now
    bin_btn = soup.select_one("[data-testid='x-bin-btn']")
    if bin_btn:
        auction["is_buy_it_now"] = True
        bin_price_el = soup.select_one("[data-testid='x-bin-price']")
        if bin_price_el:
            auction["bin_price"] = bin_price_el.text.strip()

    # 入札履歴(ログイン不要で閲覧可能な範囲)
    bid_rows = soup.select(".bid-history tbody tr")
    for row in bid_rows:
        cells = row.select("td")
        if len(cells) >= 3:
            auction["bid_history"].append({
                "bidder": cells[0].text.strip(),
                "amount": cells[1].text.strip(),
                "time": cells[2].text.strip(),
            })

    return auction

# 使用例
proxy = get_proxy_url(country="US", session="auction-track-1")
proxies = {"http": proxy, "https": proxy}
resp = requests.get(
    "https://www.ebay.com/itm/1234567890",
    headers=headers,
    proxies=proxies,
    timeout=30,
)
auction_data = parse_auction_detail(resp.text)
print(auction_data)

実務ポイント:オークションの終了直前(残り5分以内)は価格変動が激しいため、スティッキーセッション(session-abc123)で同一IPから連続ポーリングするのが有効です。ProxyHatのセッションID指定で、同じIPを最大30分間維持できます。

出品者分析:フィードバック・カテゴリ・クロスリスティング

リセラーインテリジェンスでは、個別商品だけでなく出品者全体の動向を追跡することが重要です。出品者プロフィールページから、フィードバックスコア・取扱カテゴリ・出品パターンを抽出できます。

def scrape_seller_profile(
    seller_name: str,
    country: str = "US",
) -> dict:
    """出品者プロフィールから分析データを抽出"""
    proxy = get_proxy_url(country=country, session=f"seller-{seller_name}")
    proxies = {"http": proxy, "https": proxy}

    url = f"https://www.ebay.com/str/{seller_name}"
    resp = requests.get(url, headers=headers, proxies=proxies, timeout=30)
    soup = BeautifulSoup(resp.text, "html.parser")

    seller = {
        "name": seller_name,
        "feedback_score": None,
        "positive_pct": None,
        "categories": [],
        "active_listings_count": None,
    }

    # フィードバックスコア
    fb_score = soup.select_one(".feedback-score")
    if fb_score:
        try:
            seller["feedback_score"] = int(
                "".join(filter(str.isdigit, fb_score.text))
            )
        except ValueError:
            pass

    # ポジティブ率
    fb_pct = soup.select_one(".feedback-percent")
    if fb_pct:
        seller["positive_pct"] = fb_pct.text.strip()

    # 取扱カテゴリ
    cat_els = soup.select(".seller-categories a")
    seller["categories"] = [
        el.text.strip() for el in cat_els if el.text.strip()
    ]

    # アクティブ出品数
    count_el = soup.select_one(".str-items-count")
    if count_el:
        try:
            seller["active_listings_count"] = int(
                "".join(filter(str.isdigit, count_el.text))
            )
        except ValueError:
            pass

    return seller

# クロスリスティング検出:複数出品者が同一商品を出品しているか確認
def find_cross_listings(
    item_title: str,
    country: str = "US",
) -> list[dict]:
    """同一商品を複数出品している出品者を検出"""
    results = scrape_ebay_search(item_title, country=country, max_pages=2)
    # タイトルの類似度でグルーピング(簡易実装)
    from collections import defaultdict
    title_groups = defaultdict(list)
    for r in results:
        # 最初の30文字で大まかにグループ化
        key = r.title[:30].lower()
        title_groups[key].append(r)
    # 2件以上出品されているグループを返す
    return [
        {"title_key": k, "listings": v}
        for k, v in title_groups.items()
        if len(v) >= 2
    ]

# 実行例
seller_info = scrape_seller_profile("top-rated-seller-1")
print(f"Feedback: {seller_info['feedback_score']} ({seller_info['positive_pct']})")
print(f"Categories: {', '.join(seller_info['categories'][:5])}")

# Node.js + axios の場合
// const axios = require("axios");
// const resp = await axios.get("https://www.ebay.com/sch/i.html?_nkw=vintage+rolex", {
//   proxy: {
//     host: "gate.proxyhat.com",
//     port: 8080,
//     auth: { username: "user-country-US", password: "YOUR_PASSWORD" }
//   }
// });
// console.log(resp.data.length);

eBayのアンチボット対策と回避戦略

eBayは以下の技術でスクレイパーを検知します:

  • Cloudflare WAF:データセンターIP範囲をブロック。ASNベースのフィルタリングも行う
  • hCaptcha:疑わしいトラフィックにCAPTCHAを提示。解決には$1.5〜3/1000回のコストがかかる
  • レートリミット:同一IPから1分間に30リクエスト以上で429を返す
  • ブラウザフィンガープリント:TLSフィンガープリント・HTTP/2フレーム順序でボットを識別

これらに対する実務的な対策:

  1. レジデンシャルプロキシのローテーション:ProxyHatのデフォルト設定ではリクエストごとにIPが変わるため、ASNブロックを回避
  2. 自然なリクエスト間隔:2〜5秒のランダム遅延を入れる
  3. 適切なHTTPヘッダー:最新のChrome User-Agent、Accept-Language、Refererを設定
  4. セッション管理:出品詳細ページへの連続アクセスにはスティッキーセッションを使用

curlによるクイックテスト

プロキシが正しく動作するか確認するには、curlでテストします:

# 米国IPでeBay検索結果を取得
curl -x "http://user-country-US:YOUR_PASSWORD@gate.proxyhat.com:8080" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  -H "Accept-Language: en-US,en;q=0.9" \
  "https://www.ebay.com/sch/i.html?_nkw=iphone+15+pro" \
  -o ebay_search.html

# ドイツIPでeBay.deをテスト
curl -x "http://user-country-DE:YOUR_PASSWORD@gate.proxyhat.com:8080" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  -H "Accept-Language: de-DE,de;q=0.9" \
  "https://www.ebay.de/sch/i.html?_nkw=vintage+uhr" \
  -o ebay_de_search.html

サンプルレスポンス(検索結果HTMLの構造)

eBay検索結果のHTMLは概ね以下の構造を持ちます(簡略化):

<ul class="srp-results srp-list clearfix">
  <li class="s-item s-item__pl-on-bottom">
    <div class="s-item__wrapper clearfix">
      <a class="s-item__link" href="https://www.ebay.com/itm/1234567890">
        <div class="s-item__image-section">...</div>
      </a>
      <div class="s-item__info clearfix">
        <a class="s-item__link">
          <h3 class="s-item__title">Vintage Rolex Submariner 1680</h3>
        </a>
        <div class="s-item__details clearfix">
          <span class="s-item__price">$8,500.00</span>
          <span class="s-item__shipping">Free shipping</span>
          <span class="s-item__bid-count">12 bids</span>
          <span class="s-item__time-left">2h 15m left</span>
          <span class="s-item__buy-it-now">Buy It Now</span>
        </div>
        <div class="s-item__seller-info">
          <span class="s-item__seller-info-text">luxury_watches_nyc (15,234) 99.7%</span>
        </div>
      </div>
    </div>
  </li>
  <!-- more .s-item entries... -->
</ul>

この構造はeBayがA/Bテストを実施するため、時々微変化があります。.s-item自体は安定していますが、サブクラス名(__details__detail-sectionなど)が変わることがあるため、定期的なセレクタ検証が必要です。

倫理的・法的考慮事項

  • robots.txt:eBayのrobots.txtは/sch/(検索)と/itm/(出品詳細)のクローリングを許可していません。実務上はプロキシ経由でアクセスしますが、これはeBayのToSに違反する可能性があります
  • ToS:eBayの利用規約は自動データ収集を禁止しています。大規模スクレイピングはアカウント停止リスクがあります
  • GDPR/CCPA:出品者名・フィードバックなど個人データを扱う場合、適切な取り扱いが必要
  • レート制限の尊重:eBayのサーバーに負荷をかけないよう、適切な遅延を入れる

推奨アプローチ:APIで取得できるデータはAPIを使い、APIでカバーできない部分のみスクレイピングに頼るハイブリッド戦略が、リスクと効率のバランスが最も良いです。

Key Takeaways

  • eBay Finding APIは1日5,000コール制限があり、オークションデータを含まない——大規模追跡にはeBayプロキシ経由のHTMLスクレイピングが必須
  • 検索結果は.s-itemセレクタで安定してパース可能。サブセレクタは定期的に検証すること
  • データセンターIPはeBayでほぼ確実にブロックされる——レジデンシャルプロキシが必須
  • 地域プロキシでeBay.de・eBay.co.ukの地域固有価格・送料を正確に取得
  • オークション追跡にはスティッキーセッション(同一IP維持)が効果的
  • 出品者分析はフィードバックスコア・カテゴリ・クロスリスティングパターンの3軸で行う
  • API+スクレイピングのハイブリッド戦略が、リスクとカバレッジの最適バランス

ProxyHatのレジデンシャルプロキシでeBayスクレイピングを開始するには、料金プランを確認し、対応ロケーションから必要な地域を選択してください。大規模なウェブスクレイピングのユースケースについては、ウェブスクレイピングのユースケースも参照してください。

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

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

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