GetSellerList,GetMyeBaySellingで出品中の全商品を確実にPull・同期する

前回の記事はこちら

【連載#9】eBay Trading API:GetSellerList / GetMyeBaySellingで出品中の全商品を確実にPull・同期する

はじめに

本記事は、全42回にわたる「eBay API 実践ガイド」の第9回です。

前回(#8)までは、商品の出品から在庫・価格の高速同期といった「eBay へのデータ送信(Push)」をメインに解説してきました。今回からは、eBay 側の現在のステータスを正しくインポートする「データの取得(Pull)」フェーズに入ります。

この記事で得られること:

  • 出品一覧・販売状況を取得する 2 大 API GetSellerListGetMyeBaySelling の決定的な違いと使い分け。
  • GTC(長期出品)商品特有の「時間の罠」を回避するフィルタリング技術。
  • ネットワークの一時エラーに耐え、単一・バリエーション商品ともに網羅する堅牢な同期スクリプトの実装。

背景・なぜこれが重要か (Motivation)

自社システムやデータベース(DB)を構築して運用していると、必ず 「データの整合性の漂移(Data Drift)」 が発生します。

セラーが eBay の管理画面(Seller Hub)から手動で出品を取り下げたり、eBay 側のポリシー違反で出品が強制削除されたり、バリエーションの一部が売り切れたりした場合、自社 DB 側がその事実を検知できなければ、実在庫との不整合や二重販売の原因になります。

この同期ズレを防ぐためには、定期的に eBay 側から「現在のアクティブな出品一覧」を Pull して、ローカル DB を監査・一括更新(Upsert)するバッチ処理が不可欠です。

eBay Trading API にはそのための武器が 2 つ用意されていますが、特性を理解せずに使うと「全件取れていない」「レスポンスが重すぎてタイムアウトする」といった問題に直面します。

基本的な使い方(ベースライン):2大APIの徹底比較

まずは、これら 2 つの API の特性をマトリクスで理解しましょう。

機能・特性 GetMyeBaySelling GetSellerList
主な用途 現在のアカウント状況のクイックな同期、日次バッチ 出品データの全件一括ダウンロード、週次・月次のディープ監査
必須フィルタ 不要(ActiveList などのブロック単位で指定) 時間範囲(StartTime または EndTime)の指定が必須
時間指定の制約 なし 1 回のリクエストで指定できる期間幅が最大 120 日間(※過去や未来の日付上限自体はない)
データ軽量化 比較的軽量(必要なブロックのみを Include する) GranularityLevel で制御。軽量化なら Coarse を指定
バリエーション情報 <IncludeVariations>true</IncludeVariations> の明示が必要 GranularityLevel を調整することで詳細まで取得可能
【どちらを選ぶべきか?】
  • GetMyeBaySelling: 「今現在、アクティブな商品の一覧と在庫数をサクッと確認したい」という日次のクイックな在庫同期に最適です。
  • GetSellerList: 「新規システム導入時に、過去に出品した数万件の全データを一括インポートしたい」という初期同期や、時間ベースでの厳密な監査に必須です。

実務で躓く場面・深いポイント (Core)

1. GetSellerList における「GTC商品の罠」

GetSellerList を使う際、ほとんどのエンジニアが 「出品開始時間(StartTimeFrom / StartTimeTo)」 でフィルタをかけてしまいます。これが最大の罠です。

eBay の固定価格商品は基本的に GTC(自動再出品)です。3 年前に出品開始され、毎月自動更新されているアクティブな商品は、開始時間が 3 年前のままなので、直近 120 日間のフィルターには絶対にヒットしません。

【解決策】:
現在アクティブな GTC 商品を網羅したい場合は、開始時間ではなく 「出品終了時間(EndTimeFrom / EndTimeTo)」 でフィルターをかけます。GTC 商品は内部的に「30 日後に終了する設定」で毎月ロールオーバーしているため、“今から 30 日後までに終了する予定の商品” を検索すれば、現在動いているすべてのアクティブ商品を確実にキャッチできます。

2. ペイロード肥大化と DetailLevel / GranularityLevel の混同

Trading API にはレスポンスの細かさを制御するパラメータがありますが、API によって挙動が異なります。

GetSellerList で全件取得を試みる際、詳細なデータを求めすぎてタイムアウトを起こすケースが後を絶ちません。一覧の同期や監査が目的であれば、レスポンスを軽量化するために <GranularityLevel>Coarse</GranularityLevel> を指定するのが鉄則です。これにより、重い商品説明などのテキストを除外した必要最小限のフィールドのみが返却されます。

堅牢な実装:一括取得・パーススクリプト

実運用に耐えうるよう、以下の堅牢化を施した Python コードを実装します。

  • 古い実行環境にも配慮した Python 3.7+ 互換の型ヒント(typing.Tuple など)。
  • eBay の一時的なネットワークエラーや API 瞬断対策として、tenacity ライブラリを用いた指数バックオフ付きの自動リトライ。
  • 出品形式(固定価格 / オークション+BIN)に応じた価格フィールド(StartPrice / BuyItNowPrice)の安全なパース処理。
# fetch_listings.py
import requests
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any, Tuple, Optional
from tenacity import retry, stop_after_attempt, wait_exponential
from config import eBayConfig
from ebay_token_manager import eBayTokenManager

def _get_text(node: Optional[ET.Element], tag: str, ns: dict) -> str:
    """NoneType クラッシュを防ぐ安全なテキスト抽出ヘルパー"""
    if node is None: return ""
    el = node.find(tag, ns)
    return el.text if el is not None and el.text is not None else ""

# 【深いポイント①】: 一時的なAPIエラーに備え、tenacityによるリトライ機構を装備
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def _post_api_request(url: str, headers: dict, payload: str) -> str:
    response = requests.post(url, headers=headers, data=payload.encode('utf-8'), timeout=30)
    response.raise_for_status()
    return response.text

def fetch_active_list_myebay(config: eBayConfig, token: str, page_number: int = 1) -> Tuple[List[Dict[str, Any]], int]:
    """
    GetMyeBaySelling を使用した日次クイック同期用関数
    """
    headers = {
        "X-EBAY-API-CALL-NAME": "GetMyeBaySelling",
        "X-EBAY-API-SITEID": "0",
        "X-EBAY-API-COMPATIBILITY-LEVEL": "1323",
        "X-EBAY-API-IAF-TOKEN": token,
        "Content-Type": "text/xml"
    }

    # 【深いポイント②】: バリエーション情報を取得するために IncludeVariations を明示
    xml_payload = f"""<?xml version="1.0" encoding="utf-8"?>
    <GetMyeBaySellingRequest xmlns="urn:ebay:apis:eBLBaseComponents">
        <ErrorLanguage>en_US</ErrorLanguage>
        <WarningLevel>High</WarningLevel>
        <ActiveList>
            <Include>true</Include>
            <IncludeVariations>true</IncludeVariations>
            <Pagination>
                <EntriesPerPage>100</EntriesPerPage>
                <PageNumber>{page_number}</PageNumber>
            </Pagination>
        </ActiveList>
    </GetMyeBaySellingRequest>
    """

    res_text = _post_api_request(config.trading_api_url, headers, xml_payload)
    namespace = {'ns': 'urn:ebay:apis:eBLBaseComponents'}
    root = ET.fromstring(res_text)

    ack = _get_text(root, 'ns:Ack', namespace)
    if ack not in ['Success', 'Warning']:
        errors = [{"code": _get_text(e, 'ns:ErrorCode', namespace), "msg": _get_text(e, 'ns:LongMessage', namespace)} for e in root.findall('ns:Errors', namespace)]
        raise Exception(f"GetMyeBaySelling Error: {errors}")

    items_extracted = []
    total_pages_node = root.find('ns:ActiveList/ns:PaginationResult/ns:TotalNumberOfPages', namespace)
    total_pages = int(total_pages_node.text) if total_pages_node is not None and total_pages_node.text else 1

    active_list_node = root.find('ns:ActiveList/ns:ItemArray', namespace)
    if active_list_node is not None:
        for item_node in active_list_node.findall('ns:Item', namespace):
            item_id = _get_text(item_node, 'ns:ItemID', namespace)
            title = _get_text(item_node, 'ns:Title', namespace)
            
            variations_node = item_node.find('ns:Variations', namespace)
            if variations_node is not None:
                for var_node in variations_node.findall('ns:Variation', namespace):
                    items_extracted.append({
                        "item_id": item_id,
                        "title": f"{title} ({_get_text(var_node, 'ns:VariationTitle', namespace)})",
                        "sku": _get_text(var_node, 'ns:SKU', namespace),
                        "price": float(_get_text(var_node, 'ns:StartPrice', namespace) or 0.0),
                        "quantity": int(_get_text(var_node, 'ns:Quantity', namespace) or 0),
                        "is_variation": True
                    })
            else:
                # 【深いポイント③】: 出品形式(固定価格 / オークション+BIN)に応じた価格フィールドから安全に取得する
                price_val = _get_text(item_node, 'ns:BuyItNowPrice', namespace) or _get_text(item_node, 'ns:StartPrice', namespace) or "0.0"
                items_extracted.append({
                    "item_id": item_id,
                    "title": title,
                    "sku": _get_text(item_node, 'ns:SKU', namespace),
                    "price": float(price_val),
                    "quantity": int(_get_text(item_node, 'ns:Quantity', namespace) or 0),
                    "is_variation": False
                })

    return items_extracted, total_pages

def fetch_active_list_sellerlist(config: eBayConfig, token: str, page_number: int = 1) -> Tuple[List[Dict[str, Any]], int]:
    """
    GetSellerList を使用したディープ監査用関数(GTC商品の罠をEndTimeで回避)
    """
    headers = {
        "X-EBAY-API-CALL-NAME": "GetSellerList",
        "X-EBAY-API-SITEID": "0",
        "X-EBAY-API-COMPATIBILITY-LEVEL": "1323",
        "X-EBAY-API-IAF-TOKEN": token,
        "Content-Type": "text/xml"
    }

    # 【核心】: GTC商品を漏らさず取るため、現在から30日後までの「EndTime」でフィルタリング
    now = datetime.now(timezone.utc)
    end_time_from = now.strftime('%Y-%m-%dT%H:%M:%S.000Z')
    end_time_to = (now + timedelta(days=30)).strftime('%Y-%m-%dT%H:%M:%S.000Z')

    # 【軽量化】: 軽量化のために GranularityLevel=Coarse を指定
    xml_payload = f"""<?xml version="1.0" encoding="utf-8"?>
    <GetSellerListRequest xmlns="urn:ebay:apis:eBLBaseComponents">
        <ErrorLanguage>en_US</ErrorLanguage>
        <WarningLevel>High</WarningLevel>
        <EndTimeFrom>{end_time_from}</EndTimeFrom>
        <EndTimeTo>{end_time_to}</EndTimeTo>
        <GranularityLevel>Coarse</GranularityLevel>
        <Pagination>
            <EntriesPerPage>200</EntriesPerPage>
            <PageNumber>{page_number}</PageNumber>
        </Pagination>
    </GetSellerListRequest>
    """

    res_text = _post_api_request(config.trading_api_url, headers, xml_payload)
    namespace = {'ns': 'urn:ebay:apis:eBLBaseComponents'}
    root = ET.fromstring(res_text)

    ack = _get_text(root, 'ns:Ack', namespace)
    if ack not in ['Success', 'Warning']:
        errors = [{"code": _get_text(e, 'ns:ErrorCode', namespace), "msg": _get_text(e, 'ns:LongMessage', namespace)} for e in root.findall('ns:Errors', namespace)]
        raise Exception(f"GetSellerList Error: {errors}")

    items_extracted = []
    pagination_node = root.find('ns:PaginationResult', namespace)
    total_pages = int(_get_text(pagination_node, 'ns:TotalNumberOfPages', namespace) or 1)

    item_array_node = root.find('ns:ItemArray', namespace)
    if item_array_node is not None:
        for item_node in item_array_node.findall('ns:Item', namespace):
            # 【深いポイント④】: GranularityLevel=Coarse では BuyItNowPrice は返却されません。
            # 固定価格(Fixed Price)商品では StartPrice が出品価格そのものになります。
            price_val = _get_text(item_node, 'ns:StartPrice', namespace) or "0.0"
            items_extracted.append({
                "item_id": _get_text(item_node, 'ns:ItemID', namespace),
                "title": _get_text(item_node, 'ns:Title', namespace),
                "sku": _get_text(item_node, 'ns:SKU', namespace),
                "price": float(price_val),
                "quantity": int(_get_text(item_node, 'ns:Quantity', namespace) or 0),
                "is_variation": False
            })

    return items_extracted, total_pages

パフォーマンス・スケーリング視点 (深度)

1. API Call Limit (日次呼び出し制限) への厳格な配慮

数万〜数十万 SKU を持つ大規模運用の環境において、最も注意すべきは API Call Limit(日次制限) です。無策のまま全件ループを頻繁に回すと、上限に達してシステム全体が停止します。自社アカウントに割り当てられた上限値を My Account > API Call Limits で必ず確認し、バッチの実行頻度を調整してください。

2. ローカル DB 同期アーキテクチャの最適化

API 制限を回避するため、以下の「2段階ハイブリッド構成」で運用するのがベストプラクティスです。

【ハイブリッド同期の構成パターン】
  • フェーズ 1(高頻度・軽量 Pull): 普段の日次(または時間ごと)バッチでは、GetMyeBaySelling を使ってアクティブ一覧の差分や主要項目のみを素早く確認します。
  • フェーズ 2(週次・ディープ監査): 週に 1 回、アクセスが少ない夜間帯に GetSellerList を用いて、GTC 商品の更新漏れや、管理画面側で直接削除された「幽霊出品」の突合クレンジングを行います。

まとめ

本記事では、eBay 側から出品中のデータを安全かつ正確に Pull し、ローカル DB と同期するための設計を解説しました。

  • ベースライン: GetSellerList(監査・一括用)と GetMyeBaySelling(デイリー用)の役割の違い。
  • 深いポイント: GTC 商品に対する EndTime フィルタ適用の重要性。GranularityLevel=Coarse による軽量化と、tenacity を使ったエラーリトライ。
  • スケーリング: 日次の軽量 Pull と週次の完全 Pull を切り分ける、Call Limit を保護するハイブリッド同期設計。

次のステップ

出品状況が正確に把握できるようになると、次に必要になるのが「売れ残った古い在庫の整理」や「突発的なトラブルによる出品の一斉取り下げ」です。

次回(#10)は、「EndItemで出品を終了する / 複数商品の一括取り下げ(bulk)自動化」 について解説します。手動下架との違いや、大量の商品を安全に一括 End させる際の実務上の注意点を掘り下げます。お楽しみに!

技術的なサポートやご質問について

APIの実装や仕様に関してご不明な点がございましたら、以下のeBay Japan 技術サポート窓口までお気軽にお問い合わせください:
ebayjapan-techsupport@ebay.com

トップに戻る