【Python】Yahoo!ショッピングAPI備忘録

YahooはYJDN(Yahoo! JAPAN Developer Network)という名前でWeb APIを提供しており、YID連携(ログイン認証)が必要なものとそうでないものがある。

- YID連携必要 - ストア運営のサポート API
- YID連携不要 - ショッピング情報取得 API

今回は、ストア情報の取得・更新ができるYID連携が必要なAPIの利用の方法を紹介する。

![ストアクリエイタプロ3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483556/9a59ab7c-f01d-2141-25b6-af304c110305.png)


## 事前準備

下記公式サイトを参照して、準備する。

https://developer.yahoo.co.jp/webapi/shopping/introduction.html

1. [Yahoo! JAPAN IDを取得する。](https://account.edit.yahoo.co.jp/signup?.src=help&.done=https%3A%2F%2Fwww.yahoo.co.jp%2F)
1. [アプリケーションを登録する。](https://e.developer.yahoo.co.jp/shopping/register )
     - ID連携利用有無 = ID連携を利用する
     - アプリケーションの種類 = サーバーサイド（Yahoo! ID連携 v2）
     - 個人情報提供先としてユーザーへ開示することに同意しますか？ = 同意しない
     - どちらの用途で発行しますか？ = 本番環境用
     - ストアURL = https://www.yahoo.co.jp/
    ![YahooApplication1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483556/656597e8-f94e-1575-b046-2af562a2dda1.png)
    ![YahooApplication2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483556/930effc5-f55b-adbc-8dec-49193cda2fa1.png)
    ![YahooApplication3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483556/0a0cbace-cbc7-7bb1-fb4b-07063b3bed84.png)
    ![YahooApplication4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483556/568b0d17-24ea-aafe-f4d7-5e3271a08d6b.png)
    ![ストアクリエイタプロ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483556/56e0d4ec-b739-459b-4df7-ab48c19e2a41.png)
1. アプリケーションの管理を開き、編集ボタンを押して、以下の欄にURLを記載する
    コールバックURL = **ツールごとに決められたURL**
    例） https://www.yahoo.co.jp/
1. 従業員登録されているビジネスIDとの紐づける
    これらの Yahoo!ID に紐づく**ビジネス ID** を、***ストアクリエイタ Pro の権限管理設定***に登録する
     ***ストアクリエイタ Pro*** に「ツール管理権限（Yahoo!ビジネスマネージャー）」を持っている***Yahoo! JAPANビジネスID***でログインする
    設定->権限管理設定を押し、編集を押す
    Yahoo! JAPANビジネスIDと氏名、権限管理種別を追加する
    ![ストアクリエイタプロ1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483556/61f0c184-c424-8c4e-8845-8003d6acba3d.png)

## API認証

1. 下記コードを実行して、認可Code取得に必要なURLを取得する。client_idには上記手順で取得したClient IDを用いる。
    ```Python
    # Authorizationエンドポイントへリダイレクト
    client_id = *******************
    url = 'https://auth.login.yahoo.co.jp/yconnect/v2/authorization'
    params = {}
    params ['response_type'] = 'code'
    params ['client_id'] = client_id
    params ['redirect_uri'] = 'https://www.yahoo.co.jp/'
    params ['scope'] = 'openid'
    response = requests.get(url, params)
    print(response.url)
    ```
1. 表示されたURLをブラウザで開き、ログイン&二段階認証する
1. リダイレクト後のURL末尾の```code=***```の認可codeをコピーする
1. 下記コードを実行して、アクセストークンとリフレッシュトークンを取得する。auth_codeには取得した認可codeを、secret_keyには上記手順で取得したシークレットを用いる。
    ```Python
    #上記のURLへブラウザでアクセスして、二段階認証をすると、リダイレクト先のURLに認可codeが得られる
    auth_code = *******
    secret_key = *******************
    url = 'https://auth.login.yahoo.co.jp/yconnect/v2/token'
    
    string_to_encode = client_id + ':' + secret_key
    encoded_string = base64.b64encode(string_to_encode.encode("utf-8")).decode("utf-8")
    
    params = {}
    params ['client_id'] = client_id
    params ['client_secret'] = secret_key
    params ['code'] = auth_code
    params ['grant_type'] = 'authorization_code'
    params ['redirect_uri'] = 'https://www.yahoo.co.jp/'
    headers = {}
    headers ['Authorization'] = 'Basic ' + encoded_string
    response = requests.post(url,params,headers=headers)
    results = response.json()
    if 'access_token' in results.keys():
        print('Correctly Get Access Token & Refresh Token!')
        access_token = results['access_token']
        refresh_token = results['refresh_token']
        print(results['access_token'], '\n')
        print(results['refresh_token'], '\n')
    else:
        print('Error Access Token & Refresh Token...')
        print(results)
    ```
1. リフレッシュトークンでアクセストークンを更新する。
    ```Python
    # リフレッシュトークンは、12時間制限 -> 公開鍵を使えば、4週間に伸びる
    access_token = ********************
    refresh_token = ********************
    
    url = 'https://auth.login.yahoo.co.jp/yconnect/v2/token'
    string_to_encode = client_id + ':' + secret_key
    encoded_string = base64.b64encode(string_to_encode.encode("utf-8")).decode("utf-8")
    params = {}
    params ['refresh_token'] = refresh_token
    params ['grant_type'] = 'refresh_token'
    headers = {}
    headers ['Content-Type'] = 'application/x-www-form-urlencoded'
    headers ['Authorization'] = 'Basic ' + encoded_string
    response = requests.post(url,params, headers=headers)
    results = response.json()
    access_token = results['access_token']
    print(access_token)
    ```

ここで取得したアクセストークンを用いて各APIを実行する。

## 各API実行

下記公式サイトを参照して、実行する。

https://developer.yahoo.co.jp/webapi/shopping/

### 1.ストアアカウントの在庫情報取得(アイテムコードを指定)
アイテムコード（ショップで一意の商品コード）を指定して在庫情報を取得する

```Python
item_code = *********
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/getStock"

params = {}
params['appid'] = client_id
params['seller_id'] = seller_id
params['item_code'] = item_code

headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}

response = requests.post(YahooAPIURL, data=params, headers=headers)
print(response.status_code)
print(response.text)
```

### 2. ストアアカウントの全在庫情報取得
ストアアカウントが保有する全ての商品・在庫・ストアカテゴリーの情報を取得する。

```Python
target_type = 1
# 1 : 商品データ
# 2 : 在庫データ
# 3 : ストアカテゴリ
YahooAPIURL_request = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/downloadRequest"
YahooAPIURL_check = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/downloadList?seller_id={}".format(seller_id)
YahooAPIURL_download = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/downloadSubmit?seller_id={}&type={}".format(seller_id, target_type)

params = {}
params['appid'] = client_id
params['seller_id'] = seller_id
params['type'] = target_type

headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}

response = requests.post(YahooAPIURL_request, data=params, headers=headers)
print(response.status_code)
print(response.text)
response = requests.get(YahooAPIURL_check, headers=headers)
print(response.status_code)
print(response.text)
response = requests.get(YahooAPIURL_download, headers=headers)
print(response.status_code)

escaped_string = response.text.replace('\n', r'\n')
pattern = r'"(.*?)"'
quoted_strings = re.findall(pattern, escaped_string)
lines = [re.split(r'(?<!\\)\n', quoted_string)[0] for quoted_string in quoted_strings]
sl = 64
lines = [lines[elem*sl : elem*sl + sl] for elem in range(int(len(lines)/sl))]
print(lines)

f = open('ProductList.csv', 'w')
for item in lines:
    f.write(','.join(item) + '\n')
```

### 3. ストアアカウントの全在庫情報取得
ストアアカウントの全商品の指定した情報を取得する。

```Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/getRealStock?sellerId={}".format(seller_id)

params = {}
params['appid'] = client_id
params['seller_id'] = seller_id
params['type'] = target_type

headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}

response = requests.get(YahooAPIURL, headers=headers)
print(response.status_code)
print(response.text)
```

### 4. 店舗情報取得

```Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/getRealStore?sellerId={}".format(seller_id)

headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}

response = requests.get(YahooAPIURL, headers=headers)
print(response.status_code)
print(response.text)
```

### 5. 画像一覧取得
ストアアカウントが保有する全てのアップロードした画像を取得する。

```Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/itemImageList?seller_id={}&query=&start=1&results=100".format(seller_id)

headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}

response = requests.get(YahooAPIURL, headers=headers)
print(response.status_code)
print(response.text)

import xml.etree.ElementTree as ET 
root = ET.fromstring(response.text)
print(f"root.tag={root.tag} .attrib={root.attrib}")
image_dict = {}
for child in root:
    image_dict[child.find("Name").text] = child.find("Url").find("ModeI").text
    print(child.find("Name").text, child.find("Url").find("ModeI").text)
print(image_dict)
```

### 6. カテゴリID取得
指定した商品カテゴリーIDの子カテゴリーのカテゴリー名とIDを一覧で取得する。

```Python
YahooItemSearchURL = "https://shopping.yahooapis.jp/ShoppingWebService/V1/json/categorySearch"
params = {}
params ['appid'] = client_id
params ['category_id'] = 1
response = requests.get(YahooItemSearchURL,params)
print(response.status_code)
res = json.loads(response.text)
print(list(res['ResultSet']['0']['Result']['Categories']['Children'].keys()))
for index in list(res['ResultSet']['0']['Result']['Categories']['Children'].keys()):
    if index.isdecimal():
        item = res['ResultSet']['0']['Result']['Categories']['Children'][index]
        print('%s,%s'%(item['Id'],item['Title']))

YahooItemSearchURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/getShopCategory?seller_id={}".format(seller_id)
response = requests.get(YahooItemSearchURL, headers=headers)
print(response.status_code)
print(response.text)
root = ET.fromstring(response.text)
print(f"root.tag={root.tag} .attrib={root.attrib}")
image_dict = {}
for child in root:
    print(child.find("CategoryCode").text, child.find("CategoryName").text)
```

### 7. 商品登録
１つの商品をストアアカウントへ商品登録する。

```Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/editItem"

headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}

params = {}
params['appid'] = client_id
params['seller_id'] = seller_id
params['item_code'] = 'code01' # 商品コード
params['path'] = 'Category01' # ストアカテゴリのパス
params['name'] = 'sample product' # 商品名
params['product_category'] = 2505 # YahooショッピングカテゴリID
params['original_price'] = 100000 # 定価
params['price'] = 100000 # 通常販売価格
params['lead_time_instock'] = 2000 # 発送日情報管理番号（在庫があり時)
params['lead_time_outstock'] = 4000 # 発送日情報管理番号（在庫が切れ時)
params['postage_set'] = 1 # 商品に紐づく配送グループ管理番号

response = requests.post(YahooAPIURL, headers=headers, data=params)
print(response.status_code)
print(response.text)
```

### 8. 商品削除
１つの商品をストアアカウントから削除する。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/deleteItem"
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}
params = {}
params['appid'] = client_id
params['seller_id'] = seller_id
params['item_code'] = 'code01'
response = requests.post(YahooAPIURL, headers=headers, data=params)
print(response.status_code)
print(response.text)
```

### 9. カテゴリーリスト
ストアアカウントの全てのストアカテゴリーを一覧で取得する。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/stCategoryList?seller_id={}".format(seller_id)
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}
response = requests.get(YahooAPIURL, headers=headers)
print(response.status_code)
print(response.text)
escaped_string = response.text.replace('\n', r'\n')
pattern = r'<PageKey>(.*?)</PageKey>'
stcat_keys = re.findall(pattern, escaped_string)
print(stcat_keys)
pattern = r'<Name><!\[CDATA\[(.*?)\]\]></Name>'
Name_keys = re.findall(pattern, escaped_string)
print(Name_keys)
```

### 10. 商品リスト
指定したストアカテゴリーの全ての商品情報を取得する。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/myItemList?seller_id={}&start=1&results=100&stcat_key={}".format(seller_id,stcat_keys[1])

response = requests.get(YahooAPIURL, headers=headers)
print(response.status_code)
print(response.text)
escaped_string = response.text.replace('\n', r'\n')
pattern = r'<Name><!\[CDATA\[(.*?)\]\]></Name>'
ProductName_keys = re.findall(pattern, escaped_string)
print(ProductName_keys)
pattern = r'<ItemCode>(.*?)</ItemCode>'
ItemCode_keys = re.findall(pattern, escaped_string)
print(ItemCode_keys)
```

### 11. データチェック履歴一覧API V2
ストアに反映されていないデータを、商品・在庫・画像などの情報を指定して取得する。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V2/dataCheckHistorySummary?seller_id={}&offset=0&limit=100&dataType=STOCK".format(seller_id)
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}
response = requests.get(YahooAPIURL, headers=headers)
print(response.status_code)
print(response.text)
```

### 12. データチェック履歴一覧API V2
反映されていないデータを全て取得する。

``` Python
# publish_id=0で未反映項目のみ
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/publishHistorySummary?seller_id={}&publish_id=0&start=1&results=100".format(seller_id)
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}
response = requests.get(YahooAPIURL, headers=headers)
print(response.status_code)
print(response.text)
escaped_string = response.text.replace('\n', r'\n')
pattern = r'<PublishId>(.*?)</PublishId>'
PublishId_keys = re.findall(pattern, escaped_string)
print(PublishId_keys)
pattern = r'<ItemPageNum>(.*?)</ItemPageNum>'
ItemPageNum_keys = re.findall(pattern, escaped_string)
print(ItemPageNum_keys)
pattern = r'<CategoryPageNum>(.*?)</CategoryPageNum>'
CategoryPageNum_keys = re.findall(pattern, escaped_string)
print(CategoryPageNum_keys)
```

### 13. 全反映予約
反映されていないデータをすぐに全て反映する。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/reservePublish"
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}
params = {}
params['appid'] = client_id
params['seller_id'] = seller_id
params['mode'] = 1

response = requests.post(YahooAPIURL, headers=headers, data=params)
print(response.status_code)
print(response.text)
escaped_string = response.text.replace('\n', r'\n')
pattern = r'<Status>(.*?)</Status>'
Status_keys = re.findall(pattern, escaped_string)
print(Status_keys)
```

### 14. 在庫更新API
指定したアイテムコードの在庫情報を更新する。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/setStock"
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
    'Content-Type': 'application/json'
}
params = {}
params['appid'] = client_id
params['seller_id'] = seller_id
params['item_code'] = 'code01'
params['quantity'] = 1
response = requests.post(YahooAPIURL, headers=headers, data=params)
print(response.status_code)
print(response.text)
```

### 15. 画像アップロードAPI
画像をアップロードする。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/uploadItemImage?seller_id={}".format(seller_id)
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
}
params = {}
params['appid'] = client_id
files = {'file': ('code01.jpg', open('code01.jpg', 'rb'))}

response = requests.post(YahooAPIURL, headers=headers, files=files, data=params)
print(response.status_code)
print(response.text)
```

### 16. SHPカテゴリ検索API
指定した検索キーワードから商品カテゴリーを推測する。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/getShopCategoryList?seller_id={}&query=PC&category=2498".format(seller_id)
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
}
response = requests.get(YahooAPIURL, headers=headers)
print(response.status_code)
print(response.text)
escaped_string = response.text.replace('\n', r'\n')
pattern = r'<CategoryCode>(.*?)</CategoryCode>'
Status_keys = re.findall(pattern, escaped_string)
print(Status_keys)
```

### 17. 価格更新API
指定したアイテムコードの価格情報を更新する。

``` Python
YahooAPIURL = "https://circus.shopping.yahooapis.jp/ShoppingWebService/V1/updateItems"
headers = {
    'Host': 'circus.shopping.yahooapis.jp',
    'Authorization': 'Bearer ' +  access_token,
}
params = {}
params['appid'] = client_id
params['seller_id'] = seller_id
params['item1'] = 'item_code=code01&price=10000&sale_price='.encode('utf-8')

response = requests.post(YahooAPIURL, headers=headers, data=params)
print(response.status_code)
print(response.text)
escaped_string = response.text.replace('\n', r'\n')
pattern = r'<Status>(.*?)</Status>'
Status_keys = re.findall(pattern, escaped_string)
print(Status_keys)
```

## 公開鍵の取得・使用方法

公開鍵なしでは下記APIは発行から12時間しかアクセストークンが有効ではない。
 - 注文検索API
 - 注文詳細API
 - 注文内容変更API
 - メッセージ投稿API
 - 質問詳細API
 - 質問一覧API
 - 定期購入親注文詳細取得API
 - 定期購入情報変更API
 - 定期購入情報変更履歴詳細API

公開鍵を使用することで、これを4週間まで延ばすことができる。
公開鍵認証は、Yahoo! ショッピング出店ストアのみ申請が可能。
発行した月より「1年間」有効。

下記リンクから取得することができる。

https://store-info.yahoo.co.jp/shopping/toolmanual/setup/i/1329.html


https://developer.yahoo.co.jp/webapi/shopping/help/#aboutapipublickey

公開鍵を用いたリクエスト
```Python
import time
import base64
import requests
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

# 設定
seller_id = "{ストアアカウント}"
public_key_path = "./public.key"  # 公開鍵のパス (例: "C:\\Users\\store\\public.key")
key_version = "1"  # 公開鍵のバージョン
access_token = "{アクセストークン}"  # アクセストークン
api_url = "{APIのURL}"  # APIのURL

# 認証情報作成
authentication_value = f"{seller_id}:{int(time.time())}"

# 公開鍵の読み込み
with open(public_key_path, 'r') as f:
    public_key = RSA.import_key(f.read())

# 認証情報の暗号化
cipher = PKCS1_v1_5.new(public_key)
encrypted_authentication_value = cipher.encrypt(authentication_value.encode('utf-8'))

# 認証情報のBase64エンコード
encoded_authentication_value = base64.b64encode(encrypted_authentication_value).decode('utf-8')

# リクエストヘッダーの設定
request_headers = {
    "Authorization": f"Bearer {access_token}",
    "X-sws-signature": encoded_authentication_value,
    "X-sws-signature-version": key_version
}

# HTTP通信実行
response = requests.get(api_url, headers=request_headers)

# 結果表示
print(response.text)
```


## 参考文献

https://s.yimg.jp/dl/developer_network/image/download/shopping/yjdnshpapi.pdf