プログラムで Amazon の商品を検索したり、ASIN で商品情報を引っ張ってきたりしたくなったので、Amazon Product Advertising API を触ってみた。この API を呼び出すには、あらかじめ Amazon アソシエイトにログインして認証キーを取得しておく必要がある。
Amazon Product Advertising API は API 呼び出しの URL を作成するのがかなり面倒なので、
Product Advertising API Signed Requests Sample Code - C# REST/QUERY という古い C# のサンプルに含まれているヘルパーをちょっと修正して利用している。
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Security.Cryptography;
namespace AmazonProductAdvtApi
{
public class SignedRequestHelper
{
private string endPoint;
private string akid;
private string associateTag;
private byte[] secret;
private HMAC signer;
private const string REQUEST_URI = "/onca/xml";
private const string REQUEST_METHOD = "GET";
public SignedRequestHelper(string awsAccessKeyId, string awsSecretKey, string destination, string associateTag)
{
this.endPoint = destination.ToLower();
this.akid = awsAccessKeyId;
this.secret = Encoding.UTF8.GetBytes(awsSecretKey);
this.associateTag = associateTag;
this.signer = new HMACSHA256(this.secret);
}
public string Sign(IDictionary<string, string> request)
{
ParamComparer pc = new ParamComparer();
SortedDictionary<string, string> sortedMap = new SortedDictionary<string, string>(request, pc);
sortedMap["AWSAccessKeyId"] = this.akid;
sortedMap["Timestamp"] = this.GetTimestamp();
sortedMap["AssociateTag"] = this.associateTag;
string canonicalQS = this.ConstructCanonicalQueryString(sortedMap);
StringBuilder builder = new StringBuilder();
builder.Append(REQUEST_METHOD)
.Append("\n")
.Append(this.endPoint)
.Append("\n")
.Append(REQUEST_URI)
.Append("\n")
.Append(canonicalQS);
string stringToSign = builder.ToString();
byte[] toSign = Encoding.UTF8.GetBytes(stringToSign);
byte[] sigBytes = signer.ComputeHash(toSign);
string signature = Convert.ToBase64String(sigBytes);
StringBuilder qsBuilder = new StringBuilder();
qsBuilder.Append("http://")
.Append(this.endPoint)
.Append(REQUEST_URI)
.Append("?")
.Append(canonicalQS)
.Append("&Signature=")
.Append(this.PercentEncodeRfc3986(signature));
return qsBuilder.ToString();
}
public string Sign(string queryString)
{
IDictionary<string, string> request = this.CreateDictionary(queryString);
return this.Sign(request);
}
private string GetTimestamp()
{
DateTime currentTime = DateTime.UtcNow;
string timestamp = currentTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
return timestamp;
}
private string PercentEncodeRfc3986(string str)
{
str = HttpUtility.UrlEncode(str, System.Text.Encoding.UTF8);
str = str.Replace("'", "%27").Replace("(", "%28").Replace(")", "%29").Replace("*", "%2A").Replace("!", "%21").Replace("%7e", "~").Replace("+", "%20");
StringBuilder sbuilder = new StringBuilder(str);
for (int i = 0; i < sbuilder.Length; i++)
{
if (sbuilder[i] == '%')
{
if (Char.IsLetter(sbuilder[i + 1]) || Char.IsLetter(sbuilder[i + 2]))
{
sbuilder[i + 1] = Char.ToUpper(sbuilder[i + 1]);
sbuilder[i + 2] = Char.ToUpper(sbuilder[i + 2]);
}
}
}
return sbuilder.ToString();
}
private IDictionary<string, string> CreateDictionary(string queryString)
{
Dictionary<string, string> map = new Dictionary<string, string>();
string[] requestParams = queryString.Split('&');
for (int i = 0; i < requestParams.Length; i++)
{
if (requestParams[i].Length < 1)
{
continue;
}
char[] sep = { '=' };
string[] param = requestParams[i].Split(sep, 2);
for (int j = 0; j < param.Length; j++)
{
param[j] = HttpUtility.UrlDecode(param[j], System.Text.Encoding.UTF8);
}
switch (param.Length)
{
case 1:
{
if (requestParams[i].Length >= 1)
{
if (requestParams[i].ToCharArray()[0] == '=')
{
map[""] = param[0];
}
else
{
map[param[0]] = "";
}
}
break;
}
case 2:
{
if (!string.IsNullOrEmpty(param[0]))
{
map[param[0]] = param[1];
}
}
break;
}
}
return map;
}
private string ConstructCanonicalQueryString(SortedDictionary<string, string> sortedParamMap)
{
StringBuilder builder = new StringBuilder();
if (sortedParamMap.Count == 0)
{
builder.Append("");
return builder.ToString();
}
foreach (KeyValuePair<string, string> kvp in sortedParamMap)
{
builder.Append(this.PercentEncodeRfc3986(kvp.Key));
builder.Append("=");
builder.Append(this.PercentEncodeRfc3986(kvp.Value));
builder.Append("&");
}
string canonicalString = builder.ToString();
canonicalString = canonicalString.Substring(0, canonicalString.Length - 1);
return canonicalString;
}
}
class ParamComparer : IComparer<string>
{
public int Compare(string p1, string p2)
{
return string.CompareOrdinal(p1, p2);
}
}
}
この SignedRequestHelper
と、あらかじめ取得しておいた認証キーを使って、ASIN から商品の詳細を取得するサンプルと、キーワードで商品を検索するサンプルを書いて見た。
using AmazonProductAdvtApi;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace ApaSample
{
class Program
{
const string MY_AWS_ACCESS_KEY_ID = "<アクセスキー ID>";
const string MY_AWS_SECRET_KEY = "<シークレットキー>";
const string ASSOCIATE_TAG = "<YOUR_ASSOCIATE_TAG>";
const string DESTINATION = "ecs.amazonaws.jp";
const string NAMESPACE = "http://webservices.amazon.com/AWSECommerceService/2011-08-01";
static void Main()
{
var helper = new SignedRequestHelper(
ACCESS_KEY_ID,
SECRET_KEY,
DESTINATION,
ASSOCIATE_TAG);
Console.WriteLine("# ItemLookup");
ItemLookupSample(helper).GetAwaiter().GetResult();
Console.WriteLine("# ItemSearch");
ItemSearchSample(helper).GetAwaiter().GetResult();
Console.WriteLine("Enter で終了します。");
Console.ReadLine();
}
static async Task ItemLookupSample(SignedRequestHelper helper)
{
var request = new Dictionary<string, string>()
{
["Service"] = "AWSECommerceService",
["Operation"] = "ItemLookup",
["ItemId"] = "B00XKM6TGY",
["ResponseGroup"] = "Small",
};
var requestUrl = helper.Sign(request);
var result = await GetResponseAsync(requestUrl);
foreach (var x in result)
{
Console.WriteLine(x);
}
}
static async Task ItemSearchSample(SignedRequestHelper helper)
{
var request = new Dictionary<string, string>()
{
["Service"] = "AWSECommerceService",
["Operation"] = "ItemSearch",
["SearchIndex"] = "All",
["Keywords"] = "かぐや様は告らせたい",
["ResponseGroup"] = "Small",
};
var requestUrl = helper.Sign(request);
var result = await GetResponseAsync(requestUrl);
foreach (var x in result)
{
Console.WriteLine(x);
}
}
static async Task<IEnumerable<string>> GetResponseAsync(string url)
{
using (var client = new HttpClient())
{
var xml = await client.GetStringAsync(url);
var xdoc = XDocument.Parse(xml);
var result = from x in xdoc.Descendants()
where x.Name == XName.Get("Title", NAMESPACE)
where x.Parent.Name == XName.Get("ItemAttributes", NAMESPACE)
select x.Value;
return result;
}
}
}
}
実行結果は次の通り。ASIN のルックアップとキーワード検索、どちらも上手くいった。
リクエストのパラメーターを Dictionary<string, string>
に詰めるのはミスしやすそうなので、型を定義したいところ。
時間があれば、Amazon Product Advertising API のクライアントでも書いてみようかな。