Amazon Product Advertising APIでISBN検索してみた

初めてAmazonAPIを叩いてみた。

準備

AWSのアカウントが必要なので取得しておく。 http://aws.amazon.com/

アカウントを取ったらログインして、Security CredencialのページでAccess Key IDとSecret Access Keyを調べて、控えておく。

Amazonアソシエイトを併用してお金を稼ぎたい場合は追加で申請が必要だが、ただ検索する分には不要であるらしい。

プログラミング

既存の適当なライブラリを使おうと思ったが、何を使ったらいいのかわからなかったので勉強がてら自作してみることにした。Ruby 1.8.7を使って、ガイド片手にREST APIを叩いてみる。

require 'rubygems'

gem 'ruby-openid'
gem 'nokogiri'

require 'cgi'
require 'net/http'
require 'hmac/sha2'
require 'nokogiri'

AWS_API_HOST='webservices.amazon.co.jp'
AWS_API_PATH='/onca/xml'
ACCESS_KEY_ID='YOUR_ACCESS_KEY_ID'
SECRET_ACCESS_KEY='YOUR_SECRET_ACCESS_KEY'
AWS_API_VERSION='2009-11-01'

params = [
  ["Service","AWSECommerceService"],
  ["AWSAccessKeyId", ACCESS_KEY_ID],
  ["Operation", "ItemLookup"],
  ["ItemId","4798025623"],
  ["ResponseGroup","Small"],
  ["Version", AWS_API_VERSION],
  ["Timestamp", Time.now.gmtime.strftime('%Y-%m-%dT%H:%M:%SZ')]
].map { |k,v| "#{k}=#{CGI.escape(v)}" }.sort.join("&")

string_to_sign = %W(
GET
#{AWS_API_HOST}
#{AWS_API_PATH}
#{params}
).join("\n")

signature = CGI.escape([HMAC::SHA256.digest(SECRET_ACCESS_KEY, string_to_sign)].pack("m").chomp)

path = '%s?%s&Signature=%s' % [AWS_API_PATH, params, signature]

body = Nokogiri(Net::HTTP.get(AWS_API_HOST, path))

errors = body / "Error"
unless errors.empty?
  errors.map { |error|
    puts "%s: %s" % [(error % "Code").text, (error % "Message").text]
  }
else
  (body % "Item").tap { |item|
    puts (item / "Author").map(&:text).join(", ")
    puts (item % "Title").text
  }
end

上記したソースコードでは、大きく3つの処理を実施している。

  1. パラメータ作成
  2. 認証用のSignature作成
  3. API呼び出し
パラメータ作成
params = [
  ["Service","AWSECommerceService"],
  ["AWSAccessKeyId", ACCESS_KEY_ID],
  ["Operation", "ItemLookup"],
  ["ItemId","4798025623"],
  ["ResponseGroup","Small"],
  ["Version", AWS_API_VERSION],
  ["Timestamp", Time.now.gmtime.strftime('%Y-%m-%dT%H:%M:%SZ')]
].map { |k,v| "#{k}=#{CGI.escape(v)}" }.sort.join("&")

パラメータ名と値の組を定義している。

Service, AWSAccessKeyId, Version, Timestampはおまじないとして書くもの、それ以外はコマンドになるようだ。

ItemIdとして定義しているものが検索対象のISBN-10コードである。正しくはASINコードだが、Amazonは書籍の場合ASINとしてISBN-10をそのまま使用しているので、問題無く検索できる。

サンプルを見るとTimestampにはアクセス日時をGMTで渡しているので、それに倣った。*1

map以下でごちゃごちゃ何かしているが、パラメータをHTTPクエリ文字列にしているだけだ。

  1. パラメータをURLエンコードする
  2. ソートする
  3. '&'で連結する

なぜソートしているのか、というと、このクエリ文字列がSignature作成に使われるからである。クライアント側とサーバ側で同一の文字列を生成できないとSignatureの検証ができないから、パラメータを好き勝手な順番で並べられるのは困るのだ。

認証用のSignature作成
string_to_sign = %W(
GET
#{AWS_API_HOST}
#{AWS_API_PATH}
#{params}
).join("\n")

signature = CGI.escape([HMAC::SHA256.digest(SECRET_ACCESS_KEY, string_to_sign)].pack("m").chomp)

Amazon Product Advertising APIを利用するためには、認証が必要である。昨年突如としてこの仕様がアナウンスされ、API利用者の間で騒ぎになっていたのは記憶に新しい。

ここでは認証に使うSignatureを作成している。

作成したパラメータ文字列を利用して、Signatureの種となる文字列を作る。次のような内容になるはずだ。*2

GET
webservices.amazon.co.jp
/xml/orca
AWSAccessKeyId=YOUR_ACCESS_KEY_ID&ItemId=4798025623&...(略)

これとSecret Access Keyを組み合わせてSignatureを作る。一行にまとめてしまっているが、やってることは次の通り。

  1. HMAC-SHA256でダイジェストを作る(ここではruby-openidのライブラリに含まれているものを利用)
  2. ダイジェストをBASE64エンコードする
  3. エンコードした文字列末尾の改行コードを削る
  4. HTTPクエリ文字列として送るので、URLエンコードしておく
API呼び出し
path = '%s?%s&Signature=%s' % [AWS_API_PATH, params, signature]

body = Nokogiri(Net::HTTP.get(AWS_API_HOST, path))

errors = body / "Error"
unless errors.empty?
  errors.map { |error|
    puts "%s: %s" % [(error % "Code").text, (error % "Message").text]
  }
else
  (body % "Item").tap { |item|
    puts (item / "Author").map(&:text).join(", ")
    puts (item % "Title").text
  }
end

普通にHTTPでGETする。このとき、クエリ文字列にSignatureを足すのを忘れずに。

レスポンスはXML文書として返ってくるので、Nokogiriでバラす。

ところで、ここでHTTPステータスコードを見ていないのには理由がある。REST API、ということなのでレスポンス中のHTTPステータスコードを見れば結果を判断できるかと思ったが、そうでもなかったのだ。正しく処理されれば200 OKが返るし、認証に失敗すると403 Forbiddenが返る。これらが期待通りの動作なので、それならば商品が見つからない場合は当然404 Not Found、そしてパラメータがおかしければ400 Bad Requestあたりを返してくると思ったのだが、どちらの場合も200 OKが返ってくる。*3

仕方がないので、返ってきたXML文書にErrorエレメントがあるかどうかで処理の成功と失敗を判断させている。ガイドでも紹介されている手法なので、たぶん問題ないだろう。*4

検索に成功した場合、結果から著者名とタイトルだけを引き抜いて表示させている。著者が複数いる場合はカンマ区切りにしている。

実行!

こうなる。

$ ruby apa.rb
Leonard Richardson, Sam Ruby
RESTful Webサービス

パラメータに誤りがある場合。今回はOperationを削ってみた。

$ ruby apa.rb
AWS.InvalidOperationParameter: The Operation parameter is invalid. Please modify the Operation parameter and retry. Valid values for the Operation parameter include TagLookup, ListLookup, CartGet, SellerListingLookup, CustomerContentLookup, ItemLookup, SimilarityLookup, SellerLookup, ItemSearch, VehiclePartLookup, BrowseNodeLookup, CartModify, ListSearch, CartClear, VehiclePartSearch, CustomerContentSearch, CartCreate, TransactionLookup, VehicleSearch, SellerListingSearch, CartAdd, Help.

存在しないISBNコードで検索した場合。

$ ruby apa.rb
AWS.InvalidParameterValue: 4873113538は、ItemIdの値として無効です。値を変更してから、再度リクエストを実行してください。

エラーもハンドリングできてますな!

ISBN-13による検索

書籍に関しては、ASINとISBN-10が同一の値になるので、ASIN検索の検索キーにISBN-10コードを使えば目的の書籍を検索できる。

しかし、ISBN-10は古い規格なのである。番号の枯渇が迫っていたので、2007年から桁数を増やしたISBN-13に変更となった。ところが、Amazonは変わらずISBN-10をASINとして使用している。ISBN-13で検索する方法は無いのだろうか?

ASIN検索のキーにISBN-10を渡す、という変則的なやり方ではなくて、ISBN検索というズバリな機能があるのだが、これはUS Onlyだそうなので、amazon.co.jpでは使えないだろう。

ところで、ISBN-10とISBN-13は相互に変換可能である。そこで、ISBN-13をISBN-10に変換して、これをキーとして検索すればいいのでは…と思ったのだが。

オンラインツールで、13桁のISBNを10桁のISBNに変換してくれるサービスを発見したのですが、ここで生成された10桁のISBNを利用して、アソシエイト・リンクを作成することはできますか?
いいえ、アソシエイト・リンクは、10桁のASINを使用した場合にのみ有効ですので、こういった方法で算出した10桁のISBNをリンクに使用することはできません。この場合、お手数ですが、リンク作成画面で該当する商品の商品名・著者名・アーティスト名やメーカー名でサーチをしていただき、該当するASINを選択していただきますようお願いいたします。

https://affiliate.amazon.co.jp/gp/associates/help/t4/a8?ie=UTF8&pf_rd_t=501&ref_=amb_link_19687306_4&pf_rd_m=AN1VRQENFRJN5&pf_rd_p=&pf_rd_s=center-1&pf_rd_r=&pf_rd_i=assoc_help_t4_a5

よろしくないっぽい?とはいえ、全文検索で引っかけるのはスマートじゃないし、関係のない書籍まで引っかかってきそうだ。現状他に方法が無いように思える。

*1:厳密にはUTC

*2:最初はヒアドキュメントで書いていたのだが、改行コードが\nになっていなくて躓いたので、改行を明記する形に書き換えた。

*3:リクエスト自体は正しく処理されたからOK、という意味だろうか?でもレスポンスボディ中にErrorと書いているしなぁ?

*4:エラーについてより詳細な情報を知りたい場合は、IsValidエレメントやエラーコード等を見ればよい。