Amazon Product Advertising API 5.0 (商品情報API) で商品情報を検索するコードの Python 版です。
公式の API テスト実行ツールでは Java と PHP のみだったので、Lambda などで Python を使いたくなり書いてみました。公式ドキュメントを参考にしながら、SDK を使わずに SHA-256 での署名生成も行っています。(AWSのリソースにアクセスするときも、この方法が使えるようです)
PA-API 公式ドキュメント
https://webservices.amazon.co.jp/paapi5/documentation/sending-request.html
API を利用するために必要なもの
- Amazon アソシエイトのパートナータグ
xxxxxxxxxxxxxxx-22
-
PA-API の認証キー
-
Access Key
,Secret Key
-
- 利用したい場所 (Locale) の、アマゾンのホストとリージョン
- アマゾンジャパンなら (2022/03 時点)
- host:
webservices.amazon.co.jp
- region:
us-west-2
- host:
- アマゾンジャパンなら (2022/03 時点)
処理の流れ
- リクエストボディの作成(送信データの本体。検索したいキーワードなど)
- 署名の作成(一番長いコード。認証するためのハッシュ値の生成)
- リクエストヘッダの作成(送信データの補足情報。署名はここに入れる)
- POST メソッドでHTTPリクエスト
- レスポンスの確認(ステータスコードが 200 以外なら、ボディにエラーが含まれる)
全体的にはシンプルな HTTP リクエストのコードです。
こちらのサンプルコードを利用しています。
AWS 署名バージョン4 を作る Python サンプルコード
https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html#sig-v4-examples-post
コード
Python 3.9 で確認。クラス AmzAPi5
に一通りの処理をまとめてあります。
import json
import requests
import sys, os, base64, datetime, hashlib, hmac
# PA-APIで検索したい文字列をセット
keywords = 'iPhone'
# アクセスキーとシークレットキーをセット
amz_api = AmzApi5('Access Key', 'Secret Key')
# 利用したいアマゾンのホストとリージョンをセット
amz_api.set_region('us-west-2')
amz_api.set_market_place('www.amazon.co.jp')
# アソシエイトのパートナータグをセット
amz_api.set_partner_tag('xxxxxxxxxx-22')
# SHA-256 署名を生成して、POST メソッドで HTTP リクエスト
# Param: x-amz-target, endpoint, host, path, request_parameters(without PartnerTag & MarketPlace)
# ※request_parameters でPOST送信のボディを渡しているが、ParterTagとMarketPlaceはここでは省略。クラス内で処理
r = amz_api.request_post(
'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems',
'https://webservices.amazon.co.jp/paapi5/searchitems',
'webservices.amazon.co.jp',
'/paapi5/searchitems',
{
'Keywords': keywords,
'PartnerType': 'Associates',
'Operation': 'SearchItems',
'Resources': [
'BrowseNodeInfo.BrowseNodes',
'BrowseNodeInfo.BrowseNodes.Ancestor',
'BrowseNodeInfo.BrowseNodes.SalesRank',
'BrowseNodeInfo.WebsiteSalesRank',
..... (省略)
]
}
)
# レスポンスを確認
return {
'status_code': r.status_code,
'body': r.text
}
class AmzApi5():
__config = {
'content_encoding': 'amz-1.0',
'content_type': 'application/json; charset=utf-8',
'service': 'ProductAdvertisingAPI'
}
def __init__(self, access_key, secret_key):
self.__access_key = access_key
self.__secret_key = secret_key
def set_region(self, region):
self.__region = region
def set_market_place(self, market_place):
self.__market_place = market_place
def set_partner_tag(self, partner_tag):
self.__partner_tag = partner_tag
def request_post(self, amz_target, endpoint, host, path, request_parameters):
# Set Marketplace and PartnerTag
request_parameters['Marketplace'] = self.__market_place
request_parameters['PartnerTag'] = self.__partner_tag
# ********************************************
# Create Signature (AWS4-HMAC-SHA256)
# ********************************************
# Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# This file is licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License. A copy of the
# License is located at
#
# http://aws.amazon.com/apache2.0/
#
# This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
# OF ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
#
# Original Code : Example Using POST (Python). See:
# https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html#sig-v4-examples-post
# ********************************************
# Key derivation functions. See:
# http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python
def sign(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
def getSignatureKey(key, dateStamp, regionName, serviceName):
kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp)
kRegion = sign(kDate, regionName)
kService = sign(kRegion, serviceName)
kSigning = sign(kService, 'aws4_request')
return kSigning
# Read AWS access key from env. variables or configuration file. Best practice is NOT
# to embed credentials in code.
access_key = self.__access_key
secret_key = self.__secret_key
if access_key is None or secret_key is None:
print('No access key is available.')
sys.exit()
# Create a date for headers and the credential string
t = datetime.datetime.utcnow()
amz_date = t.strftime('%Y%m%dT%H%M%SZ')
date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope
# ************* TASK 1: CREATE A CANONICAL REQUEST *************
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
# Step 1 is to define the verb (GET, POST, etc.)--already done.
# Step 2: Create canonical URI--the part of the URI from domain to query
# string (use '/' if no path)
canonical_uri = path
## Step 3: Create the canonical query string. In this example, request
# parameters are passed in the body of the request and the query string
# is blank.
canonical_querystring = ''
# Step 4: Create the canonical headers and signed headers. Header names
# must be trimmed and lowercase, and sorted in code point order from
# low to high. Note that there is a trailing \n.
# Required fields : content-type, host, x-amz-date, x-amz-target,
# https://webservices.amazon.co.jp/paapi5/documentation/sending-request.html#signing
canonical_headers = 'content-type:' + self.__config['content_type'] + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' + 'x-amz-target:' + amz_target + '\n'
# Step 5: Create the list of signed headers. This lists the headers
# in the canonical_headers list, delimited with ";" and in alpha order.
# Note: The request can include any headers; canonical_headers and
# signed_headers lists those that you want to be included in the
# hash of the request. "Host" and "x-amz-date" are always required.
signed_headers = 'content-type;host;x-amz-date;x-amz-target'
# Step 6: Create payload hash. In this example, the payload (body of
# the request) contains the request parameters.
request_parameters_dump = json.dumps(request_parameters)
payload_hash = hashlib.sha256(request_parameters_dump.encode('utf-8')).hexdigest()
# Step 7: Combine elements to create canonical request
canonical_request = 'POST' + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash
# ************* TASK 2: CREATE THE STRING TO SIGN*************
# Match the algorithm to the hashing algorithm you use, either SHA-1 or
# SHA-256 (recommended)
algorithm = 'AWS4-HMAC-SHA256'
credential_scope = date_stamp + '/' + self.__region + '/' + self.__config['service'] + '/' + 'aws4_request'
string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
# ************* TASK 3: CALCULATE THE SIGNATURE *************
# Create the signing key using the function defined above.
signing_key = getSignatureKey(self.__secret_key, date_stamp, self.__region, self.__config['service'])
# Sign the string_to_sign using the signing_key
signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest()
# ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
# The signing information can be either in a query string value or in
# a header named Authorization. This code shows how to use a header.
# Create authorization header and add to request headers
authorization_header = algorithm + ' ' + 'Credential=' + self.__access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature
# ********************************************
# Create Header
# Python note: The 'host' header is added automatically by the Python 'requests' library.
headers = {
'content-encoding': self.__config['content_encoding'],
'content-type': self.__config['content_type'],
'x-amz-date': amz_date,
'x-amz-target': amz_target,
'Authorization': authorization_header
}
# ************* SEND THE REQUEST *************
r = requests.post(endpoint, data=json.dumps(request_parameters), headers=headers)
return r
上記は Apache License 2.0 http://aws.amazon.com/apache2.0/ のコードが含まれています。
リクエストボディのサンプル
ここは特に難しいところはないですね。 Resources
でどういった情報が欲しいのかを指定できます。わかりやすくするため、PartnerTag
と MarketPlace
は省略せずに明記していますが、上記のサンプルコードでは AmzAPi5
クラス内で処理させています。
'Keywords': 検索したいキーワード,
'PartnerTag': パートナータグ,
'Marketplace': 'www.amazon.co.jp'
'PartnerType': 'Associates',
'Operation': 'SearchItems',
'Resources': [
'BrowseNodeInfo.BrowseNodes',
'BrowseNodeInfo.BrowseNodes.Ancestor',
'BrowseNodeInfo.BrowseNodes.SalesRank',
'BrowseNodeInfo.WebsiteSalesRank',
..... (省略)
]
※このリクエストのボディも署名の素材に使われます。
リクエストヘッダのサンプル
POSTリクエストでは次のようなヘッダを送信します。 ここには署名が含まれます。
POST / HTTP/1.1
host: webservices.amazon.co.jp
content-type: application/json; charset=utf-8
content-encoding: amz-1.0
x-amz-date: 20160925T120000Z
x-amz-target: com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems
Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20160925/us-west-2/ProductAdvertisingAPI/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=&5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7;
x-amz-date
は署名生成時の日時。x-amz-target
はリクエストするサービス (今回はPA-APIの検索) を指定しています。書式は省略しますが、とりあえずこの値を指定すると思ってください。Authorization
が認証情報であり署名です。正規ユーザかを Amazon が確認するためのものです。
SHA-256 署名を作るにあたって
署名はアクセスキーなどを含む単一の文字列で、前項の通り、リクエストヘッダにキー Authorization
で含めます。構成は次のとおり。
Authorization: AWS4-HMAC-SHA256 Credential=[アクセスキー]/[YYYYMMDD]/[リージョン]/[サービス名]/aws4_request, SignedHeaders=[署名に含まれる内容], Signature=[署名本体。AWS署名バージョン4];
AWS4-HMAC-SHA256 Credential=[アクセスキー]
までは簡単ですね。この署名のアルゴリズムが AWS4-HMAC-SHA256 であることを示し、API のアクセスキー(平文)を伝えています。その後は [署名の年月日]
/[リージョン]
/[サービス名]
/aws4_request
が続きます。
次に、SignedHeaders=content-type;host;x-amz-date;x-amz-target
。これは後ろに続く署名本体 Signature
に含まれるヘッダの内容を示しています(セミコロンで区切る)。SignedHeaders
の記述の順番には注意してください。(署名の本体は Signature
ですが、 ハッシュ化に用いるヘッダ情報の文字列と順番が異なると、認証が通りません)
PA-API のサンプルコードによると、 content-type
host
x-amz-date
x-amz-target
を署名に含めることを要求しています。(試したところ、content-type はなくてもエラーは出ませんでしたが)
ハッシュ化の前後
具体的な作り方はコードを見てもらうとして、ハッシュ化の前後でどのようになるかを見てみます。
まず次のようなヘッダ情報の文字列を用意します。これがハッシュ化する前です。
content-type:'application/json; charset=utf-8' \n
host:'webservices.amazon.co.jp' \n
x-amz-date:'20160925T120000Z' \n
x-amz-target:'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems' \n
上記を、諸々の処理を経て、下記のような感じのランダムな文字列(ハッシュ値)にします。
これが署名の本体となります。
&5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
ヘッダ情報の文字列に、さらにシークレットキー・リクエストボディ・メソッド形式・リクエストするホスト・パス・リージョン・日時・利用するサービス名・ヘッダ情報の構成などを加えて、SHA-256 (256ビット) のハッシュにしています。
ハッシュ化とは不可逆な変換で、暗号化(可逆的な変換)よりセキュリティ的に強固とされています。リクエストを受ける Amazon 側 (AWS) でも同様にハッシュ値の生成を行い、ユーザが送ったハッシュ値と一致するかどうかで認証しています。ハッシュは、ユーザと Amazon しか知らないシークレットキーを用いて作られるため、正しいハッシュは両者しか知りえません。
AWS の公式ドキュメントには、手順について更に詳細な解説があります。興味がある方は覗いてみてください。
ハッシュ値(署名)ができたら、Authorization: AWS4-HMAC-SHA256 Credential=[アクセスキー]/[YYYYMMDD]/[リージョン]/[サービス名]/aws4_request, SignedHeaders=[署名に含まれるヘッダの内容], Signature=[署名本体。AWS署名バージョン4];
の Signature
に当てはめます。あとはリクエストヘッダに含めて送信するだけです。
あとがき
Python に慣らしつつ書いていたので、Pythoner (パイソナー) 的にはアレな書き方があるかもしれません。 getter / setter は不要論や @property
にすべきというのも把握していたのですが、規模の大きなコードでもないし自分にとっての見通しやすさを選びました。
次の記事もよかったら御覧ください。