Amazon Product Advertising APIを使って、「Flex × Python」からAmazonの商品データの検索するアプリを作ってみた

こんにちわ!兄貴とTwitter上でマジレス兄弟喧嘩をしたRyoAbeです。


今回は何を作ったかと言いますと表題にある通り、
AmazonAPI(Product Advertising API)を使った商品データの検索出来るアプリです。
(ほんとただそれだけですw)
かるーくAPI触ってどんな感じか探るだけだったんですが、
意外に手こずっちゃって、おかげでけっこう勉強になったんで
Blogにまとめがてら載せることにしました!


◎そもそもProduct Advertising APIってなんぞやって話から

Product Advertising API は、Amazon の商品情報や関連コンテンツをプログラムを通してアクセスできるサービスを提供することで、Web 開発者の皆様が、ご自分の Web サイトでAmazon の商品を紹介することによる紹介料の獲得を可能とします。

そもそもは、ブログとかのアフィリエイトとかに使うことが目的っぽい。
WordPressプラグインAmazonLinkとかもこのAPIを使ってるんだって。
まぁー、勉強ついでとはいえ今回の僕のAPIの使い方ちょっと間違ってますw

◎まずはアカウントの作成

まー上のリンク先(Product Advertising API)にも書いてあるんですが、アカウントを作らなきゃAPIは使えないんですわ。
アカウント作成が完了すると、"AWS アクセスキー ID"*1ってのを貰えて、それを使ってAPIにアクセスするわけです。
アカウントの作成手順については、下記のリンクを参考にして下さい
Product Advertising API アカウント作成 ヘルプ


◎アカウントも作成できたし、作り始めるか

今回使う言語はPythonです。
作り始めるって言ってもPythonのモジュールにPyAWSってのがあるんで、
大してプログラムは組まないんですが。。(そのつもりだった。。)

◎よーし、PyAWS使ってみよー

対話モードでとりあえず使ってみる。
(このときは、のん気なもんだった。。)

$ python

# まずはモジュールのインポート
>>> from pyaws import ecs

# アカウントを作成後にもらったキーをセット
>>> ecs.setLicenseKey('自分のアクセスキー')

# アクセス先は日本なので、setLocaleにjpをセット
>>> ecs.setLocale('jp')

# いざ、検索!! 試しに"Python"って調べてみる
>>> books = ecs.ItemSearch('Python', SearchIndex='Books', ResponseGroup='Images')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Python/2.6/site-packages/pyaws/ecs.py", line 339, in ItemSearch
    return pagedIterator(XMLItemSearch, argv, "ItemPage", 'Items', plugins)
  File "/Library/Python/2.6/site-packages/pyaws/ecs.py", line 216, in __init__
    dom = self.__search(** self.__arguments)
  File "/Library/Python/2.6/site-packages/pyaws/ecs.py", line 348, in XMLItemSearch
    return query(buildRequest(argv))
  File "/Library/Python/2.6/site-packages/pyaws/ecs.py", line 174, in query
    e = buildException(errors)
  File "/Library/Python/2.6/site-packages/pyaws/ecs.py", line 159, in buildException
    e = globals()[ class_name ](msg)
KeyError: u'ingParameter'

うげぇーーーー、エラー発生。。
APIから返却値(XML)にErrorが返ってきてるため、
PyAWS内のqueryメソッドでエラーが起きてしまっている様子。。
なんでErrorで返ってくるんだぁーーー!?!?!?!?

早速、調べてみた!!





◎なにやら、去年(2009年08月15日)APIの仕様が変わったらしい

前までは"Access Key ID"のみでAPIの使用は出来たんだが、認証方法が変わったらしい。

名称変更にともない、Product Advertising API にリクエストを送信いただく都度、認証のための電子署名を含めていただくことが必要になります。この変更は、2009年5月11日より3ヶ月の間の移行期間の後、2009年8月15日には、Product Advertising API へ送信されるリクエストは全て認証されることとなり、認証されない場合、リクエストは処理されなくなります。Product Advertising API へのリクエストに署名認証を含めるための簡単な方法については、こちらの開発者向けガイドをご覧ください。
Amazon アソシエイト Web サービスの名称変更および署名認証についてのお知らせ より

PyAMFのリリースを見てみると、2007/04/08で終わってる。。
そりゃ対応してないわ。。
どうしよっかな。。。





◎じゃあ、自分で作っちゃえ。

対応してないなら、自分で作っちゃえ。
PyAWSを直接修正するってのも手ではあったが、一から作った方が勉強になるしねb


# だったら初めから一から作れって話だし、
# 探せば新しい仕様に対応したモジュールがあるような気もするけど無視!


■プログラム概要

■処理フローの説明

 ----------------------------
 | Product Advertising API  |
 ----------------------------
    ↑②③         ↓④
 =========================================
 | [Python]サーバサイド(main.py, aws.py) |
 =========================================
    ↑①           ↓⑤
 ======================================
 | [Flex]クライアントサイド(main.swf) |
 ======================================
  1. テキストボックスに入力した検索文字を自作のPythonプログラムにAMF*3通信で投げる
  2. Product Advertising APIへのREST URLをアクセスキーやシークレットアクセスキーを元に生成する*4
    1. パラメータ(アクセスキーや検索したワード、タイムスタンプなど)をURLエンコード
    2. シークレットアクセスキーをHMAC-SHA256形式でハッシュ化
    3. 最後にBase64エンコードをし、REST URLの生成完了
  3. 生成したREST URLからリクエストを投げる
  4. urlopenXMLを取得する
  5. XMLをクライアントサイド(Flex)に返し、DataGridに表示する(バインディング*5使用)

■実際に返ってくるXMLはこんな感じ

  • keyword
  • ResponseGroup
    • ItemAttributes
  • SearchIndex
    • Books

で検索した場合のXML↓↓

<ItemSearchResponse>
	<OperationRequest>
		<HTTPHeaders>
			<Header Name="UserAgent" Value="Python-urllib/1.17 AppEngine-Google; (+http://code.google.com/appengine)"/>
		</HTTPHeaders>
		<RequestId>11111111-2222-3333-4444-555555555555</RequestId>
		<Arguments>
			<Argument Name="Operation" Value="ItemSearch"/>
			<Argument Name="Service" Value="AWSECommerceService"/>
			<Argument Name="Signature" Value="1111111111111111111111111111"/>
			<Argument Name="Version" Value="2009-01-06"/>
			<Argument Name="Keywords" Value="python"/>
			<Argument Name="AWSAccessKeyId" Value="AAAAAAAAAAAAAAAAAAAAAAA"/>
			<Argument Name="Timestamp" Value="2010-04-16T05:17:19Z"/>
			<Argument Name="ResponseGroup" Value="ItemAttributes"/>
			<Argument Name="SearchIndex" Value="Books"/>
		</Arguments>
	<RequestProcessingTime>0.1920510000000000</RequestProcessingTime>
	</OperationRequest>

<Items>
	<Request>
		<IsValid>True</IsValid>
		<ItemSearchRequest>
			<Condition>New</Condition>
			<DeliveryMethod>Ship</DeliveryMethod>
			<Keywords>python</Keywords>
			<MerchantId>Amazon</MerchantId>
			<ResponseGroup>ItemAttributes</ResponseGroup>
			<ReviewSort>-SubmissionDate</ReviewSort>
			<SearchIndex>Books</SearchIndex>
		</ItemSearchRequest>
	</Request>
	<TotalResults>62</TotalResults>
	<TotalPages>7</TotalPages>
	<Item>
		<ASIN>4797353953</ASIN>
		<DetailPageURL>http://www.amazon.co.jp/%E3%81%BF%E3%82%93%E3%81%AA%E3%81%AEPython-%E6%94%B9%E8%A8%82%E7%89%88-%E6%9F%B4%E7%94%B0-%E6%B7%B3/dp/4797353953%3FSubscriptionId%3DAKIAJD5ODXRC4SZWODZA%26tag%3Dws%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D4797353953
		</DetailPageURL>
		<ItemLinks>
			<ItemLink>
				<Description>Add To Wishlist</Description>
			<URL>http://www.amazon.co.jp/gp/registry/wishlist/add-item.html%3Fasin.0%3D4797353953%26SubscriptionId%3DAKIAJD5ODXRC4SZWODZA%26tag%3Dws%26linkCode%3Dxm2%26camp%3D2025%26creative%3D5143%26creativeASIN%3D4797353953
			</URL>
			</ItemLink>
			<ItemLink>
				<Description>Tell A Friend</Description>
				<URL>http://www.amazon.co.jp/gp/pdp/taf/4797353953%3FSubscriptionId%3DAKIAJD5ODXRC4SZWODZA%26tag%3Dws%26linkCode%3Dxm2%26camp%3D2025%26creative%3D5143%26creativeASIN%3D4797353953
				</URL>
			</ItemLink>
			<ItemLink>
				<Description>All Customer Reviews</Description>
				<URL>http://www.amazon.co.jp/review/product/4797353953%3FSubscriptionId%3DAKIAJD5ODXRC4SZWODZA%26tag%3Dws%26linkCode%3Dxm2%26camp%3D2025%26creative%3D5143%26creativeASIN%3D4797353953
				</URL>
			</ItemLink>
			<ItemLink>
				<Description>All Offers</Description>
				<URL>http://www.amazon.co.jp/gp/offer-listing/4797353953%3FSubscriptionId%3DAKIAJD5ODXRC4SZWODZA%26tag%3Dws%26linkCode%3Dxm2%26camp%3D2025%26creative%3D5143%26creativeASIN%3D4797353953
				</URL>
			</ItemLink>
		</ItemLinks>
	
		<ItemAttributes>
		<Author>柴田 淳</Author>
		<Binding>単行本</Binding>
		<EAN>9784797353952</EAN>
		<Edition>改訂版</Edition>
		<ISBN>4797353953</ISBN>
		<Label>ソフトバンククリエイティブ</Label>
		<ListPrice>
			<Amount>2940</Amount>
			<CurrencyCode>JPY</CurrencyCode>
			<FormattedPrice>¥ 2,940</FormattedPrice>
		</ListPrice>
		<Manufacturer>ソフトバンククリエイティブ</Manufacturer>
		<NumberOfPages>484</NumberOfPages>
		<PackageDimensions>
		<Height Units="hundredths-inches">110</Height>
		<Length Units="hundredths-inches">827</Length>
		<Weight Units="hundredths-pounds">150</Weight>
		<Width Units="hundredths-inches">591</Width>
		</PackageDimensions>
		<ProductGroup>Book</ProductGroup>
		<ProductTypeName>ABIS_BOOK</ProductTypeName>
		<PublicationDate>2009-04-11</PublicationDate>
		<Publisher>ソフトバンククリエイティブ</Publisher>
		<Studio>ソフトバンククリエイティブ</Studio>
		<Title>みんなのPython 改訂版</Title>
		</ItemAttributes>
	</Item>
		:
		:
	
</Items>
</ItemSearchResponse>

詳しくはこちら

■ソース載っけます

  • main.swf(クライアントサイド)
    • main.pyへAMFで検索文字を渡す
    • main.pyからXMLを受け取る
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
	<mx:Script>
		<![CDATA[
			import mx.utils.ObjectUtil;
			import mx.messaging.AbstractConsumer;
			import mx.collections.ArrayCollection;
			import mx.collections.XMLListCollection;
			import mx.controls.Alert;
			import flash.net.*;

			[Bindable]
			public var xml_data:XML;
			[Bindable]
			public var xElementCollection:XMLListCollection;
			
			// 検索時のプルダウン用
			[Bindable]
			public var search_indexes:ArrayCollection = new ArrayCollection(
				[ {label:"全て", data:"All"},
				  {label:"和書", data:"Books"},
				  {label:"洋書", data:"ForeignBooks"},
				  {label:"DVD", data:"DVD"},
				  {label:"曲名", data:"MusicTracks"},
				  {label:"ソフトウェア", data:"Software"},
				  {label:"ゲーム", data:"VideoGames"}
				]);


                        // データ送信
			private function doRequest():void{
				// レスポンダーを作成
				var responder:Responder = new Responder(onSuccess, onFault);
				// コネクションの作成
				var connection:NetConnection = new NetConnection();

				// コネクト先のPythonのURL
				connection.connect("http://searchamazondatarabe.appspot.com/");
				connection.objectEncoding = ObjectEncoding.AMF3;

				// サーバサイドに渡す値
				var data:Object = new Object();
				data['search_word'] = nameText.text;
				data['search_index'] = myComboBox.selectedItem.data;

				// サーバサイド(main.py)のitemSearchメソッドを呼ぶ
				connection.call("Service.itemSearch", responder, data);
			}
			
			// データ取得成功
			private function onSuccess(e:*):void{
				// 受け取ったXMLをバインドする
				xml_data =  new XML(e.toString());
				xElementCollection = new XMLListCollection(xml_data.Items.Item.ItemAttributes);
			}

			// データ取得失敗
			private function onFault(e:*):void{
				Alert.show("通信失敗");
			}

			// 実ページ遷移用 add 2010/4/26
			private function onItemClicked(e:*):void{
				var i:int =  e.rowIndex;
				var urls:String = xml_data.Items.Item[i].DetailPageURL;
				var url:URLRequest = new URLRequest(urls);
				navigateToURL(url, "_blank");
			}
			
		]]>
	</mx:Script>
	<mx:TextInput x="10" y="43" id="nameText" enter="doRequest()" />
	<mx:Button label="検索" id="idTest" click="doRequest()" x="286" y="43"/>
	<mx:Label x="10" y="10" text="Amazonでデータを検索" color="#FFFFFF" fontSize="16"/>

	<mx:DataGrid right="10" left="10" top="84" bottom="30" itemClick="onItemClicked(event)"
				 id="myDataGrid" dataProvider="{xElementCollection}">
		<mx:columns >
			<mx:DataGridColumn headerText="タイトル" dataField="Title"/>
			<mx:DataGridColumn headerText="著者名" dataField="Author"/>
			<mx:DataGridColumn headerText="商品カテゴリー" dataField="ProductGroup"/>
			<mx:DataGridColumn headerText="Binding" dataField="Binding"/>
			<mx:DataGridColumn headerText="ページ数" dataField="NumberOfPages"/>
			<mx:DataGridColumn headerText="発行年月日" dataField="PublicationDate"/>
			<mx:DataGridColumn headerText="発売元" dataField="Publisher"/>
		</mx:columns>
	</mx:DataGrid>
	<mx:ComboBox x="178" y="43" width="100" dataProvider="{search_indexes}" id="myComboBox"/>

</mx:Application>

  • main.py(サーバサイド)
    1. main.swf(クライアントサイド)からのリクエストを受ける
    2. アクセスキー、シークレットアクセスキーをセット
    3. aws.pyへ検索ワードを渡す
    4. aws.pyからXMLを受け取る
    5. クライアントサイド(Flex)へXMLを渡す
# -*- coding: utf-8 -*-
import logging
import wsgiref.handlers
from pyamf.remoting.gateway.wsgi import WSGIGateway
from aws import AWS


def search(data):
	aws = AWS(select_access_key_id='自分のシークレットアクセスキー',
	          aws_access_key_id='自分のアクセスキー')
	result =  aws.doItemSearch(keyword=data['search_word'],
	          search_index=data['search_index'])

	return result

def main():

	services = {
		'Service.itemSearch': search,
	}

	gateway = WSGIGateway(services, logger=logging, debug=True)
	wsgiref.handlers.CGIHandler().run(gateway)	

if __name__ == '__main__':
	main()
  • aws.py(こいつがProduct Advertising APIとの連携をする)
    1. main.pyから受け取った値(アクセスキー、シークレットアクセスキー)をコンストラクタでセット
    2. doItemSearchがmain.pyから呼ばれ、検索ワード(keyword)を受け取る
    3. パラメータのURLエンコードや暗号化をしてREST URLを生成
    4. XMLを取得し、main.pyへ返す
# -*- coding: utf-8 -*-


from time import strftime, gmtime # GMTIME取得用
import urllib        # URLエンコード
import hmac, hashlib # HMAC-SHA256の算出用
import base64        # Base64エンコード用
import types		 # 型を調べるときに使用

AWS_DOMAIN = "ecs.amazonaws.jp"

class AWS:

	def __init__(self, **params):

		# パラメータセット
		self.__setParams(params)


	def __setParams(self, params):
		""" パラメータセット """

		self.setSelectAccessKey( params.get("select_access_key_id") )
		self.setAWSAccessKeyId( params.get("aws_access_key_id") )
		self.setResponseGroup( params.get("response_group") or "ItemAttributes")
		self.setTimeStamp( params.get("timestamp") or strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) )
		self.setOperation( params.get("operation") or "ItemSearch")
		self.setSearchIndex( params.get("search_index") or "Books" )
		self.setService( params.get("service") or "AWSECommerceService")
		self.setVersion( params.get("version") or "2009-01-06")
		self.setKeyWord( params.get("keyword") )


	def doItemSearch(self, **params):
		""" 検索 """

		# パラメータセット
		if params.get("keyword")      : self.setKeyWord( params.get("keyword") )
		if params.get("operation")    : self.setOperation( params.get("operation") )
		if params.get("search_index") : self.setSearchIndex( params.get("search_index") )
	    
		# パラメータをURLエンコード
		enc_params = self.__doParamEncode()
		# Signature作成
		signature = self.__createSignature(enc_params)
		# リクエストURLを生成
		request_url = 'http://' + AWS_DOMAIN + "/onca/xml?" + enc_params + "&Signature=" + signature
		# urlopen
		result = urllib.urlopen(request_url)

		return result.read()


	def __doParamEncode(self):
		""" URLのパラメータをURLエンコード """

		params = { 
				    "Service"        : self.getService(),
				    "AWSAccessKeyId" : self.getAWSAccessKeyId(),
				    "Operation"      : self.getOperation(),
				    "SearchIndex"    : self.getSearchIndex(),
				    "ResponseGroup"  : self.getResponseGroup(),
				    "Version"        : self.getVersion(),
				    "Timestamp"      : self.getTimeStamp(),
				    "Keywords"       : self.getKeyWord()
				  }

		# ソート
		sorted(params.iteritems())

		# URLエンコードした文字列を格納するタプル
		enc_param_list = []

		# パラメータをURLエンコード
		for key in sorted(params.keys()):
			if type(params[key]) == types.UnicodeType: 
				params[key] = params[key].encode("UTF-8")
			enc_param = "%s=%s" % (key, urllib.quote(params[key]) )
			enc_param_list.append( enc_param )


		return "&".join(enc_param_list)




	def __createSignature(self, enc_params):
		""" Signatureの作成 """

		# 署名用文字列の作成
		message = "\n".join(["GET", AWS_DOMAIN, "/onca/xml", enc_params])	
		# Secret Access KeyをHMAC-SHA256形式でハッシュ化
		hmac_digest = hmac.new(self.getSelectAccessKey(), message, hashlib.sha256).digest()
		# Base64エンコード
		base64_encoded = base64.b64encode(hmac_digest)
		# URLエンコード(2回目)
		signature = urllib.quote(base64_encoded)

		return signature		


	# KeyWord
	def setKeyWord(self, keyword):
		self.__keyword = keyword
		
	def getKeyWord(self):
		return self.__keyword

	# TimeStamp
	def setTimeStamp(self, timestamp):
		self.__timestamp = timestamp

	def getTimeStamp(self):
		return self.__timestamp

	# Service
	def setService(self, service):
		self.__service = service

	def getService(self):
		return self.__service

	# AWSAccessKeyId
	def setAWSAccessKeyId(self, aws_access_key_id):
		self.__aws_access_key_id = aws_access_key_id

	def getAWSAccessKeyId(self):
		return self.__aws_access_key_id

	# SelectAccessKey
	def setSelectAccessKey(self, select_access_key_id):
		self.__select_access_key_id = select_access_key_id

	def getSelectAccessKey(self):
		return self.__select_access_key_id

		
	# Operation
	def setOperation(self, operation):
		self.__operation = operation

	def getOperation(self):
		return self.__operation

	# SearchIndex
	def setSearchIndex(self, search_index):
		self.__search_index = search_index

	def getSearchIndex(self):
		return self.__search_index

	# ResponseGroup
	def setResponseGroup(self, response_group):
		self.__response_group = response_group

	def getResponseGroup(self):
		return self.__response_group

	# Version
	def setVersion(self, version):
		self.__version = version

	def getVersion(self):
		return self.__version
	

◎完成したものがこれ

GAEに載っけました。
http://searchamazondatarabe.appspot.com/main.swf

◎残タスク

  • XMLの返却値でAutherタグ(著者名)が複数ある場合に、まんまAutherタグが表示されちゃう

◎まだまだやりたいこと

  • デフォルトで10件しばりになってるんで、それを外す
  • 画像とかも表示したりしたい
  • 実ページへの遷移のクリックイベントの追加

◎作ってみて思ったこと

  • Product Advertising APIをちゃんとした使い方しよw
    • 今回は本当の用途と全然違う使い方をしてしまったw アフィリエイトでもやってみっか。
  • ってかPythonって楽しいね
    • なんかもっと綺麗な書き方(そもそも作りがおかしいとか)もあると思うけど、書いてて楽しかった

参考サイト

今日はこんなとこで!

※追記
・実ページへの遷移のクリックイベントの追加 add 2010/4/26

*1:AWS アクセスキー IDとは、AWSの開発者アカウントに関連付けられた識別子です。AWSで提供されているすべてのWebサービスにアクセスするときに、AWS アクセスキー IDが必要になります。

*2:クライアント(Flex)側でAPIに直接アクセスするって手もあったけどね。Python触りたかったしw

*3:AMFについてはこちらを参照

*4:詳細はこちら(認証(Timestamp及びSignature)@AjaxTower)もしくは、こっち(Product Advertising API)

*5:詳しくはこちら(Flex の Binding 具体例と内部事情の覗き見CommentsAdd Star@くって煮ブログ)を参照