4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PythonでAmazon PA-API 5.0を使って商品情報を検索する (SDK未使用)

Last updated at Posted at 2022-03-14

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

処理の流れ

  1. リクエストボディの作成(送信データの本体。検索したいキーワードなど)
  2. 署名の作成(一番長いコード。認証するためのハッシュ値の生成)
  3. リクエストヘッダの作成(送信データの補足情報。署名はここに入れる)
  4. POST メソッドでHTTPリクエスト
  5. レスポンスの確認(ステータスコードが 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 でどういった情報が欲しいのかを指定できます。わかりやすくするため、PartnerTagMarketPlace は省略せずに明記していますが、上記のサンプルコードでは 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 にすべきというのも把握していたのですが、規模の大きなコードでもないし自分にとっての見通しやすさを選びました。

次の記事もよかったら御覧ください。

4
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?