Last active
March 14, 2020 05:39
-
-
Save takehiko/3af05b926673751959b3549ad5fbd890 to your computer and use it in GitHub Desktop.
Product search using PA-API v5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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