UploadSiteHostedPicturesで画像をEPSに確実・高速にアップロードする

前回の記事はこちら

eBay Trading API:UploadSiteHostedPicturesで画像をEPSに確実・高速にアップロードする

はじめに

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

前回は出品に必須となるメタデータを取得しました。今回はいよいよ出品データを組み立てる直前の最終準備として、「商品画像のアップロード」 に焦点を当てます。

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

  • eBay Picture Services (EPS) の役割と、事前アップロードアーキテクチャの利点。
  • 実務で最もハマりやすい multipart/form-data を用いたローカル画像のバイナリアップロードの正確な実装(順序保証と動的MIMEタイプ判定)。
  • ThreadPoolExecutor を用いた、複数画像の並列アップロードと順序保持(スケーリング手法)。

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

eBay に商品を出品する際、画像を指定する方法は主に2つあります。

  1. 自己ホスト型 (Self-Hosted): AddFixedPriceItem リクエストに自分のサーバーや Amazon S3 の URL を直接渡す。
  2. EPS ホスト型 (eBay Picture Services): 事前に eBay のサーバー(EPS)に画像をアップロードし、返ってきた i.ebayimg.com の URL を出品リクエストに渡す。

一見すると、1の「URLを直接渡す」方が簡単に見えます。しかし、実務においてこの方法は出品エラーの最大の温床になります。

なぜなら、出品 API 呼び出しの同期処理中に eBay のクローラーがあなたの画像 URL にアクセスして取得を試みるため、少しでもネットワーク遅延や SSL 証明書のエラー、CDN のブロックが発生すると、「画像が取得できない」という理由で出品リクエスト全体が失敗(Drop)してしまうからです。

大規模かつ安定したシステムを構築するなら、事前に UploadSiteHostedPictures を叩いて EPS に画像をキャッシュさせ、確実に生成された EPS の URL を使って出品を行うアーキテクチャ(2の手法) が必須となります。

基本的な使い方(ベースライン):外部URLからの取得

UploadSiteHostedPictures には「ローカルファイルのアップロード」と「外部 URL を渡して eBay に取りに行かせる」の2パターンがあります。

まずは簡単な「外部 URL」パターンのベースラインを見てみましょう。

<?xml version="1.0" encoding="utf-8"?> <UploadSiteHostedPicturesRequest xmlns="urn:ebay:apis:eBLBaseComponents"> <ExternalPictureURL>https://your-domain.com/images/item1.jpg</ExternalPictureURL> <PictureSet>Supersize</PictureSet> <ExtensionInDays>30</ExtensionInDays> </UploadSiteHostedPicturesRequest> 

これを送信すると、<SiteHostedPictureDetails><FullURL> の中に、EPS 上の新しい URL(https://i.ebayimg.com/...)が返ってきます。

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

実務において「画像の元データが S3 などのパブリック URL になく、ローカルディスクや非公開ストレージにある」というケースは多々あります。

この場合、画像を直接バイナリとして送信する必要がありますが、これが Python 開発者が最も躓くポイント です。

eBay の API は、1つのリクエスト内に「XML のメタデータ」と「画像のバイナリデータ」を混在させる multipart/form-data 形式を要求します。これを requests ライブラリで正しく構築するには、少し特殊な書き方が必要です。

堅牢な実装:マルチパート・バイナリアップロード

ここでは、ローカルの画像ファイルを読み込み、動的にMIMEタイプを判定した上で、XML と一緒に安全に送信するクラスメソッドを実装します。

# upload_pictures.py import requests import xml.etree.ElementTree as ET import os import mimetypes from config import eBayConfig from ebay_token_manager import eBayTokenManager def upload_picture_to_eps(config: eBayConfig, token: str, file_path: str) -> str: """
    ローカルの画像ファイルをeBay Picture Services (EPS) にアップロードし、URLを返す
    """ if not os.path.exists(file_path): raise FileNotFoundError(f"Image not found: {file_path}")

    headers = { "X-EBAY-API-CALL-NAME": "UploadSiteHostedPictures", "X-EBAY-API-SITEID": "0", "X-EBAY-API-COMPATIBILITY-LEVEL": "1323", "X-EBAY-API-IAF-TOKEN": token, # 【注意】: Content-Type は requests ライブラリが自動生成・境界(boundary)設定するためここでは指定しない! } # XML ペイロード(バイナリ送信時は ExternalPictureURL は不要) xml_payload = """<?xml version="1.0" encoding="utf-8"?>
    <UploadSiteHostedPicturesRequest xmlns="urn:ebay:apis:eBLBaseComponents">
        <ErrorLanguage>en_US</ErrorLanguage>
        <WarningLevel>High</WarningLevel>
        <PictureSet>Supersize</PictureSet>
        <ExtensionInDays>30</ExtensionInDays>
    </UploadSiteHostedPicturesRequest>
    """ file_name = os.path.basename(file_path) # 【深いポイント①】: 拡張子からMIMEタイプを動的判定する mime_type, _ = mimetypes.guess_type(file_path)
    mime_type = mime_type or 'image/jpeg' with open(file_path, 'rb') as img_file: # 【深いポイント②】: マルチパートの順序保証 # eBay は「XMLが最初のパート、画像が次のパート」という順序を厳格に要求します。 # Python 3.7以降は辞書(dict)でも挿入順が保証されますが、意図を明示するためにリスト形式(タプルのリスト)を推奨します。 files = [ # 第1パート: XMLデータ(ファイル名は空文字を指定) ('XML Payload', ('', xml_payload, 'text/xml')), # 第2パート: 画像データ ('file', (file_name, img_file, mime_type))
        ]

        response = requests.post(config.trading_api_url, headers=headers, files=files)
        response.raise_for_status() # XMLパースとエラーハンドリング namespace = {'ns': 'urn:ebay:apis:eBLBaseComponents'}
    root = ET.fromstring(response.text)

    ack_node = root.find('ns:Ack', namespace)
    ack = ack_node.text if ack_node is not None else "" if ack not in ['Success', 'Warning']:
        errors_node = root.find('ns:Errors/ns:LongMessage', namespace)
        error_msg = errors_node.text if errors_node is not None else "Upload Failed" raise Exception(f"API Error ({file_name}): {error_msg}") # EPSのURLを抽出 url_node = root.find('ns:SiteHostedPictureDetails/ns:FullURL', namespace) if url_node is None or not url_node.text: raise Exception(f"Failed to extract FullURL for {file_name}") return url_node.text
【エッジケース: 画像フォーマットとサイズ】

eBay は JPEG, PNG, TIFF, BMP, GIF をサポートしていますが、実務ではJPEG または PNG に統一することを強く推奨します。また、画像サイズが小さすぎると(長辺が500px未満など)PictureSet=Supersize(ズーム機能)が有効にならず、エラーまたは警告が返るため、最低でも 500x500、理想は 1600x1600 ピクセルの画像を用意してください。

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

eBay では1商品につき最大24枚の画像を登録できます。

上記のスクリプトで24枚の画像を for ループで順次アップロード(Sequential Upload)すると、1枚1秒かかったとして 24秒 もブロックされてしまいます。大量の出品処理を行うシステムでは致命的なボトルネックです。

ネットワーク I/O が主体の処理なので、Python の concurrent.futures.ThreadPoolExecutor を用いて並列アップロード(Concurrent Upload)を実装します。

ここで重要なのが 「画像の順序とインデックス」 です。eBay の出品リクエストでは、渡したURLリストの1枚目がメイン画像(検索結果に表示される画像)となります。そのため、並列処理を行いつつも、結果のURLリストは元のファイルリストの順序を厳密に維持しなければなりません。

import concurrent.futures from typing import List, Optional def upload_multiple_pictures(config: eBayConfig, token: str, file_paths: List[str]) -> List[Optional[str]]: """
    複数画像をマルチスレッドでEPSへ一括アップロードし、入力順序を維持したURLのリストを返す。
    失敗した画像は None として返し、インデックスのズレ(メイン画像の意図せぬ入れ替わり)を防ぐ。
    """ # 内部エラーキャッチ用のラッパー関数 def safe_upload(path): try:
            url = upload_picture_to_eps(config, token, path) print(f"[Success]: {path} -> {url}") return url except Exception as exc: print(f"[Error] uploading {path}: {exc}") return None # max_workers は環境に合わせて調整(eBayのRate Limitに配慮して5〜10程度が安全) with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: # 【深いポイント③】: 順序とインデックスの保持 # as_completed ではなく map を使うことで、入力した file_paths の順序通りに結果が返却されます。 # 失敗時(None)を除外してしまうと「何枚目が何の画像か」が分からなくなるため、そのまま返却します。 results = list(executor.map(safe_upload, file_paths)) return results # 使い方の例 if __name__ == "__main__":
    config = eBayConfig()
    manager = eBayTokenManager(config.client_id, config.client_secret, config.refresh_token, config.env)
    token = manager.get_token() # アップロードしたいローカル画像のリスト(1枚目がメイン画像になる前提) images_to_upload = ["item_front.jpg", "item_back.jpg", "item_detail.jpg"]
    
    eps_urls = upload_multiple_pictures(config, token, images_to_upload) # エラーハンドリング:欠落がある場合は呼び出し元で検知する if None in eps_urls: print("【警告】: 一部の画像アップロードに失敗しました。対応するインデックスを確認してリトライしてください。") print("Final EPS URLs to use in AddFixedPriceItem:", eps_urls)

この実装により、画像アップロードの所要時間を大幅に(数分の一に)短縮しつつ、メイン画像が意図せず入れ替わってしまう事故を完全に防ぐことができます。

まとめ

本記事では、出品プロセスを安定させるための「画像の事前アップロード」について解説しました。

  • ベースライン: EPS (eBay Picture Services) を介することで、出品 API (AddFixedPriceItem) の失敗リスクを極限まで減らすアーキテクチャ。
  • 深いポイント: requestsfiles をリストで定義し順序を保証する実装。拡張子からの MIME タイプの動的判定による堅牢化。
  • スケーリング: ThreadPoolExecutor.map を用いた、順序とインデックス(メイン画像の対応関係)を維持した安全で高速な並列アップロード。

これで「メタデータ」と「EPS 画像URL」という、出品に必要な全ての材料が手元に揃いました。

次のステップ

準備はすべて整いました。次回(#5)は、いよいよ本丸となる 「AddFixedPriceItemで商品を出品する」 です。

これまでに取得したメタデータと画像 URL を組み合わせ、バリエーションを持たない単一商品(Single-SKU)の出品ペイロードを構築し、Sandbox 環境に実際に商品を並べてみます。お楽しみに!

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

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

トップに戻る