Skip to content

Instantly share code, notes, and snippets.

@takehiko
Last active March 14, 2020 05:39
Show Gist options
  • Save takehiko/3af05b926673751959b3549ad5fbd890 to your computer and use it in GitHub Desktop.
Save takehiko/3af05b926673751959b3549ad5fbd890 to your computer and use it in GitHub Desktop.
Product search using PA-API v5
// paapi5search.js - Product search using PA-API v5
// by takehikom
// npm install paapi5-nodejs-sdk
// npm install optparse
// npm install bitly
// node search.js 'Harry Potter'
// node search.js 9784791628889
// node search.js -t 9784393376034 -j
// node search.js -t 9784623071326 -j
// node search.js -j -t 978-4-7741-5394-0
// node search.js -j -b -t 978-4-7741-5394-0
// 4.0から大きく変わった「PA-API v5.0(Amazon API)」の使い方! | HPcode
// https://haniwaman.com/pa-api-5/
// JavascriptでオプションのパースをするOptparse-js
// https://ota42y.com/blog/2014/08/26/optparse/
// ~/.amazonrcを読み出してaccessKey, secretKey, PartnerTagを獲得する
const find_keys = () => {
var hash = {};
const path = require('path');
const userHome = process.env['HOME'];
const rcFile = path.join(userHome, '.amazonrc');
try {
const fs = require('fs');
const text = fs.readFileSync(rcFile, 'utf8');
const lines = text.toString().split('\n');
for (var line0 of lines) {
const line = line0.replace(/ /g, '');
if (line.match(/^#/)) continue;
var m;
if ((m = line.match(/^secret_key_id=(.+)/)) != null) {
const v = m[1].replace(/^['"]|['"]$/g, '')
// console.log('secretKey = ' + v);
hash['secretKey'] = v
} else if ((m = line.match(/^key_id=(.+)/)) != null) {
const v = m[1].replace(/^['"]|['"]$/g, '')
// console.log('accessKey = ' + v);
hash['accessKey'] = v;
} else if ((m = line.match(/^associate=(.+)/)) != null) {
const v = m[1].replace(/^['"]|['"]$/g, '')
// console.log('PartnerTag = ' + v);
hash['PartnerTag'] = v
} else if ((m = line.match(/^bitly[^=]*=(.+)/)) != null) {
// https://bitly.comにサインアップまたはログインし,
// 右上のv,Profile Settings,Generic Access Token,
// パスワード認証でアクセストークンが生成されるので,
// ~/.amazonrcに「bitly = 'アクセストークン'」を書く
const v = m[1].replace(/^['"]|['"]$/g, '')
// console.log('Bitly Access Token = ' + v);
hash['Bitly'] = v
}
}
} catch (ex) {
return hash;
}
return hash;
};
const keys = find_keys();
// 読み出せなければ終了する
if (!keys['accessKey'] || !keys['secretKey'] || !keys['PartnerTag']) {
console.log('Please prepare ~/.amazonrc');
process.exit(0);
}
// Bitlyにアクセスして短縮URLを作成する
const shortenURL = async (uri) => {
const { BitlyClient } = require('bitly');
const bitly = new BitlyClient(keys['Bitly'], {});
let result;
try {
result = await bitly.shorten(uri);
return result.link;
} catch(e) {
return null;
}
}
// コマンドライン解析
const optSwitches = [
['-t', '--term TERM', 'Search term (product name or ISBN/ASIN)'],
['-m', '--max NUMBER', 'Max number of items'],
['-j', '--json', 'Print query result'],
['-S', '--no-summary', 'Suppress summary'],
['-b', '--bitly', 'URL shortening'],
['-d', '--debug', 'Debug mode']
];
const optparse = require('optparse');
var parameter = {};
var parser = new optparse.OptionParser(optSwitches);
parser.on('term', (opt, value) => { parameter['term'] = value; });
parser.on('max', (opt, value) => { parameter['max'] = value; });
parser.on('json', () => { parameter['json'] = true; });
parser.on('no-summary', () => { parameter['nosum'] = true; });
parser.on('bitly', () => { parameter['bitly'] = true; });
parser.on('debug', () => { parameter['debug'] = true; });
const parserResult = parser.parse(process.argv);
const debug = parameter['debug'];
if (debug) {
console.log('Parameter:');
console.log(parameter);
console.log('Non-parsed Parameter:');
console.log(parserResult);
// console.log('Original Parameter:');
// console.log(process.argv);
// process.exit(0);
}
// 引数がなければ使用法を出力して終了する
if (!parameter['term']) {
if (parserResult.length <= 2) {
console.log('Usage: search.js [...]');
console.log(optSwitches.map((a) => {
return ' ' + [a[0],
(a[1] + ' '.repeat(12)).slice(0, 12),
a[2]].join(' ');
}).join("\n"))
process.exit(0);
}
parameter['term'] = parserResult[2];
}
const PAAPIv1 = require('paapi5-nodejs-sdk');
var defaultClient = PAAPIv1.ApiClient.instance;
// Specify your credentials here. These are used to create and sign the request.
defaultClient.accessKey = keys['accessKey'];
defaultClient.secretKey = keys['secretKey'];
/**
* Specify Host and Region to which you want to send the request to.
* For more details refer:
* https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region
*/
defaultClient.host = 'webservices.amazon.co.jp';
defaultClient.region = 'us-west-2';
var api = new PAAPIv1.DefaultApi();
/**
* The following is a sample request for SearchItems operation.
* For more information on Product Advertising API 5.0 Operations,
* refer: https://webservices.amazon.com/paapi5/documentation/operations.html
*/
var siReq = new PAAPIv1.SearchItemsRequest();
/** Enter your partner tag (store/tracking id) and partner type */
siReq['PartnerTag'] = keys['PartnerTag'];
siReq['PartnerType'] = 'Associates';
// Specify search keywords
siReq['Keywords'] = parameter['term'];
// ISBNと推測される場合にはハイフンなどを取り除く
if (parameter['term'].match(/^[-_ \d]+[\dxX]/)) {
siReq['Keywords'] = parameter['term'].replace(/[-_ ]/g, '');
}
/**
* Specify the category in which search request is to be made.
* For more details, refer:
* https://webservices.amazon.com/paapi5/documentation/use-cases/organization-of-items-on-amazon/search-index.html
*/
siReq['SearchIndex'] = 'All';
// Specify the number of items to be returned in search result
siReq['ItemCount'] = parameter['max'] || 1;
/**
* Choose resources you want from SearchItemsResource enum
* For more details, refer: https://webservices.amazon.com/paapi5/documentation/search-items.html#resources-parameter
*/
siReq['Resources'] = [
'ItemInfo.ContentInfo',
'ItemInfo.ByLineInfo',
'ItemInfo.Classifications',
'ItemInfo.ExternalIds',
'ItemInfo.Title',
'Offers.Listings.Price'
];
// オブジェクトobjとプロパティ(任意個のドットを含む)propについて
// obj.propがあればその値を,なければundefinedを返す
const o = (obj, prop) => {
const props = prop.split('.');
var obj2 = obj;
for (var i = 0; i < props.length; i++) {
if (typeof (obj2) == 'object' && props[i] in obj2) {
obj2 = obj2[props[i]];
} else if (typeof (obj2) == 'object'
&& props[i].match(/^\d+$/)
&& props[Number(i)] in obj2) {
obj2 = obj2[props[Number(i)]];
} else {
return undefined;
}
}
return obj2;
};
// 呼び出し時のエラーの出力(キーワードに合致する商品がない場合など)
const printErrorByCalling = (error) => {
console.log('Error calling PA-API 5.0!');
const jerror = JSON.stringify(error, null, 1);
console.log('Printing Full Error Object:\n' + jerror);
console.log('Status Code: ' + error['status']);
if (error['response'] !== undefined
&& error['response']['text'] !== undefined) {
const jerror = JSON.stringify(error['response']['text'], null, 1);
console.log('Error Object: ' + jerror);
}
};
// SearchItemsResponse関連のエラーの出力
const printErrorWithSearchItemsResponse = (searchItemsResponseErrors) => {
console.log('Errors:');
const jerror = JSON.stringify(searchItemsResponseErrors, null, 1);
console.log('Complete Error Response: ' + jerror);
console.log('Printing 1st Error:');
var error_0 = searchItemsResponseErrors[0];
console.log('Error Code: ' + error_0['Code']);
console.log('Error Message: ' + error_0['Message']);
};
// 1件の商品情報の出力
const print_item_summary = async (item) => {
if (!item) return;
/*
item['ASIN']: ASIN(文字列)
item['ItemInfo']['ByLineInfo']['Contributors']: 著者など(配列)
item['ItemInfo']['ByLineInfo']['Manufacturer']['DisplayValue']: 出版社名(文字列)
item['ItemInfo']['Classifications']['Binding']['DisplayValue']: 形態(文字列)
item['ItemInfo']['ExternalIds']['EANs']['DisplayValues']: EAN(配列)
item['ItemInfo']['ExternalIds']['ISBNs']['DisplayValues']: 10桁ISBN(配列)
item['ItemInfo']['ContentInfo']['PublicationDate']['DisplayValue']: 出版年月日(文字列)
item['Offers']['Listings'][0]['Price']['DisplayAmount']: 価格(文字列)
*/
const asin = o(item, 'ASIN');
const isbn = o(item, 'ItemInfo.ExternalIds.EANs.DisplayValues.0');
const url1 = o(item, 'DetailPageURL');
const url2 = url1 ? url1.replace(/\?.*$/, '') : undefined;
const title = o(item, 'ItemInfo.Title.DisplayValue');
const price = o(item, 'Offers.Listings.0.Price.DisplayAmount');
const manu = o(item, 'ItemInfo.ByLineInfo.Manufacturer.DisplayValue');
const dateString = o(item, 'ItemInfo.ContentInfo.PublicationDate.DisplayValue');
const dateObj = dateString ? new Date(dateString) : undefined;
const year = dateObj ? dateObj.getFullYear() : undefined;
const contribs = o(item, 'ItemInfo.ByLineInfo.Contributors');
const author = Array.isArray(contribs) ?
contribs.map(x => {
var name = x['Name'];
const role = x['Role'];
if (x['Locale'] == 'ja_JP') name = name.replace(/ /g, '');
return name + (role !== undefined && role != '著' ? `(${x['Role']})` : '');
}).join(', ') : undefined;
const hn = isbn ? `[isbn:${isbn}]` : `[asin:${asin}]`;
const biblio = `${author}: ${title}, ${manu} (${year}). ${hn}`;
if (asin) console.log('<ASIN> ' + asin);
if (isbn) console.log('<ISBN> ' + isbn);
if (url1) console.log('<Detail Page URL> ' + url1);
if (url2) console.log('<Simple URL> ' + url2);
if (parameter['bitly'] && keys['Bitly'] && url2) {
const url3 = await shortenURL(url2);
console.log('<Shortened URL> ' + url3);
}
if (title) console.log('<Title> ' + title);
if (author) console.log('<Author> ' + author);
if (manu) console.log('<Manufacturer> ' + manu);
if (dateString) console.log('<Publication Date> ' + dateString);
if (price) console.log('<Buying Price> ' + price);
console.log(biblio);
};
// api.searchItemsのコールバック関数
const callback = function (error, data, response) {
if (error) {
printErrorByCalling(error);
return;
}
if (debug) console.log('API called successfully.');
var siResp = PAAPIv1.SearchItemsResponse.constructFromObject(data);
if (parameter['json']) {
const jresp = JSON.stringify(siResp, null, 1);
if (debug) console.log('Complete Response:');
console.log(jresp);
}
if (!parameter['nosum']
&& siResp['SearchResult'] !== undefined) {
// const count = o(siResp, 'SearchResult.TotalResultCount') || 0;
const items = siResp['SearchResult']['Items'];
const count = items.length;
if (debug) console.log('count = ' + count);
for (var i = 0; i < count; i++) {
if (debug) console.log(`Printing Information in SearchResult (${i + 1}):`);
var item = items[i];
print_item_summary(item);
if (i < count - 1) console.log('');
}
}
if (siResp['Errors'] !== undefined) {
printErrorWithSearchItemsResponse(siResp['Errors']);
}
};
try {
api.searchItems(siReq, callback);
} catch (ex) {
console.log('Exception: ' + ex);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment