Amazon Lowest Price Checker改造版 Sleipnir/Firefox

http://d.hatena.ne.jp/gigi-net/20090421/1240324155

 例の物をconeco.netでのASIN検索にも対応させてみた。

 ついでにSleipnir/Firefox(3.0.10で確認)両対応。改造することなくどちらでも動作可能。

 ただしFirfoxで動かすときには一つだけ注意点があって、それはソースをUnicode(UTF-8で確認)で保存しなければならないということ。そうしないと全角文字が化けてしまって何が何だかわからなくなってしまう。

 Greasemonkeyの世界では常識なのかもしれないけど、今回テストするためにGreasemonkeyを導入して初めて知った。

 さて、今回追加したconeco.netでの検索を利用するためにはconeco.netのAPIキーを取得し、そのキーをソースに書き込まなければならない。

 キーを入れなかったらconeco.netの価格が取得できないだけで、価格.comの価格は引っ張ってきて表示することは出来るため、別に必須というわけではない。

 まあメールアドレスを入力し、送られてきたメールのアドレスを開くだけで取得できるし、価格.comでは対応できない(型番が取得できないという理由が多いが)場合もそこそこあるから取得した方がよい。

 書き換えるのは15行目で、たとえば取得したキーが123456789の場合は、

//ここに取得したAPIKEYを入れる
var APIKEY = "123456789";

 とする。ちなみにキーの取得はこちらから。


 ところで、coneco.netに商品自体は存在するのにASINコードが登録されていない物がある。

 そんなときのために、とりあえずASINで検索を行い、ダメだった場合は型番でも検索するようにしてある。

 このASINで検索が出来たかをチェックするループのタイムアウトを20秒に設定してあるが、もしこれで足りないようであれば220行目のcountの数値を増やせばよい。


 非同期通信を使うようにした関係で、冒頭部分のおまじないがごちゃごちゃと…。

 Sleipnirで非同期通信(あと今回使っているウエイト付き条件ループ)を使うためにはこのScriptControlを利用する方法とexecScriptを使う方法があるが、何故か後者の方はうまく動かなかった。小さなサンプルではいけたんだけどなあ。

 execScriptの方が短くなるんだけど、そもそもこんなことをしなければならないということ自体がなんだかなあという感じで。

 ただでさえ、あれやこれやとやってる内に妙に巨大化してしまっているというのに。

 ふとソースを眺めてみたら、try 〜 catch のバーゲンセールだなこりゃ…。

 出来るだけこれを使わないで済ませる方向で考えないといけないかも。

 あと、DOMParserを利用するようにした。まあせっかく簡単に使えるんだから使ってしまおうという単純な理由だけど。


 最後に一つ。

 商品はあるが価格が登録されていない物は999999999円として処理するようにしてある。実際にこの値段で売ってるわけではないので。

 それぐらいかな。

// ==UserScript==
// @name           Amazon Lowest Price Checker
// @namespace      http://d.hatena.ne.jp/siachan/
// @description    Compares the prices at Amazon, kakaku.com and Coneco.net.
// @include        http://www.amazon.co.jp/*
// @type           SleipnirScript
// ==/UserScript==

// @original       http://d.hatena.ne.jp/gigi-net/20090421/1240324155

(function(){
if(document.getElementById("buyboxTable")==null) return;

//ここに取得したAPIKEYを入れる
var APIKEY = "";

/*@cc_on @if(true)

try{
	var scriptControl = sleipnir.CreateObject("ScriptControl");
}catch(e){return;}

if (scriptControl) {
	scriptControl.Language = "JScript";
	scriptControl.AddObject("scriptControl", scriptControl);
	scriptControl.AddObject("_window", _window);
	scriptControl.AddObject("_document", _document);
	scriptControl.AddObject("APIKEY", Object(APIKEY));
	scriptControl.AddObject("sleipnir", sleipnir, true);
	scriptControl.Eval("_window.attachEvent('onunload', function(){scriptControl = null; _window.detachEvent('onunload', arguments.callee);})");
	scriptControl.AddCode(amazon.toString());

	try{
		scriptControl.Run("amazon", _window);
	}catch(e){
		sleipnir.Output.Print('Error('+scriptControl.Error.Source+') @ line '+(scriptControl.Error.Line));
		sleipnir.Echo(' '+scriptControl.Error.Description);
	}
}
@else@*/
amazon();
/*@end @*/

function amazon() {
/*@cc_on @if(true)
var d = _document;
var xmltext = "text";
var isIE = true;
  @else@*/
var d = document;
var xmltext = "textContent";
var isIE = false;
/*@end @*/

//var pnir = sleipnir.API;
//pnir.OutputClear();

//APIURL定義 
var KAKAKU_API = "http://api.kakaku.com/Ver1.1/ItemSearch.aspx?CategoryGroup=ALL&SortOrder=pricerank&HitNum=5&Keyword=";
var CONECO_API = "http://api.coneco.net/cws/v1/SearchProducts?apikey="+APIKEY+"&categoryId=0&sort=price&count=1&freeword=";

if(typeof GM_xmlhttpRequest == 'undefined')
GM_xmlhttpRequest = function(opt) {
	var http = null;

	if(typeof ActiveXObject != 'undefined') {
		try{
			http = new ActiveXObject("Msxml2.XMLHTTP");
		} catch(e){
			try{
				http = new ActiveXObject("Microsoft.XMLHTTP");
			} catch(e){
				return;
			}
		}
	} else return;
	http.open(opt.method.toUpperCase(), opt.url, true);
	http.onreadystatechange = function() {
		if(http.readyState == 4){
			if(http.status == 200){
				opt.onload(http);
			}
			http = null;
		}
	};
	http.send(null);
}

if(typeof(DOMParser) == 'undefined' && typeof(ActiveXObject) != 'undefined') {
	DOMParser = function() {}
	DOMParser.prototype.parseFromString = function(str) {
		var xmldata = new ActiveXObject('MSXML.DomDocument');
		xmldata.async = false;
		xmldata.loadXML(str);
		return xmldata;
	}
}

//ロード中メッセージ表示
var title = V(d,'TAG("h1")');
var KAKAKU = d.createElement("div");
KAKAKU.innerHTML = "<img src='http://img.f.hatena.ne.jp/images/fotolife/g/gigi-net/20090421/20090421143419.gif?1240292104' style='vertical-align:middle'>価格.comから最低価格を読み込んでいます。";
title[0].parentNode.appendChild(KAKAKU);

if(APIKEY != "") {
	var CONECO = d.createElement("div");
	CONECO.innerHTML = "<img src='http://img.f.hatena.ne.jp/images/fotolife/g/gigi-net/20090421/20090421143419.gif?1240292104' style='vertical-align:middle'>coneco.netから最低価格を読み込んでいます。";
	title[0].parentNode.appendChild(CONECO);
}

//クレジット表示
var html =' <center><a href="http://apiblog.kakaku.com/">WEB Services by 価格.com</a>';
if(APIKEY != "") {
	html += '<br><a href="http://apidoc.coneco.net/">Powered by coneco.net Web Services</a>';
}
html += '</center>';
var credit = d.createElement("div");
credit.innerHTML = html;
title[0].parentNode.appendChild(credit);

//Amazon.comの価格を取得
var AmazonPrice = 0;
try{
	var table = V(d,'ID("buyboxPriceBlock").TAG("table")[0].TAG("tbody")[0].TAG("tr")[0].TAG("td")');
	if(V(table[0],'TAG("b")[1].ATTR("class")') == "price"){
		AmazonPrice = V(table[0],'TAG("b")[1].innerHTML');

	}else if(V(table[0],'TAG("span")[0].ATTR("class")') == "price"){
		AmazonPrice = V(table[0],'TAG("span")[0].innerHTML');
	}
}catch(e){
	try{
		var price = V(d,'ID("priceBlock").TAG("table")[0].TAG("tbody")[0].TAG("tr")');

		for(var i=0;i<price.length;i++){
			var table = V(price[i],'TAG("td")');
			if(table[0].innerHTML.indexOf("価格") == 0){
				try {
					if(V(table[1],'TAG("b")[0].ATTR("class")') == "priceLarge"){
						AmazonPrice = V(table[1],'TAG("b")[0].innerHTML');
					}
				} catch(e) {
					if(V(table[1],'TAG("span")[0].ATTR("class")') == "priceLarge"){
						AmazonPrice = V(table[1],'TAG("span")[0].innerHTML');
					}
				}
			}
		}
	}catch(e){
		AmazonPrice = 0;
	}
}
AmazonPrice = ConvertPrice(""+AmazonPrice);

//coneco.netの価格を取得
if(APIKEY != "") {
	try {
		var ASIN = V(d,'NAME("ASIN")[0].value');
	}catch(e){}
	if(ASIN) {
		GM_xmlhttpRequest({
		  method:"GET", 
		  url:CONECO_API+encodeURIComponent(ASIN),
		  onload:function(x) {
			ShowPrice(x,"CONECO");
		  }
		});
	} else {
		CONECO.innerHTML = "<b>Error:</b>ASINコードが取得できませんでした。";
	}
}

//製品型番を取得
var ModelNumber = "";
try{
	var DOM_ModelNumber = V(d,'ID("productDetailsDiv").childNodes[0]');
	for(var i=0,l=DOM_ModelNumber.childNodes.length;i<l;i++){
		if(DOM_ModelNumber.childNodes[i].innerHTML.indexOf("メーカー型番") != -1){
			ModelNumber = DOM_ModelNumber.childNodes[i].innerHTML;
			break;
		}
	}
	if(ModelNumber == "") throw "";
}catch(e){
	try {
		var DOM_link = V(d,'ID("productDetails")');

		for(var i=0;i<10;i++) {
			DOM_link = DOM_link.nextSibling;
			if(DOM_link == null) throw "";
			if(DOM_link.nodeName == 'TABLE') break;
		}
		var LI = V(DOM_link,'TAG("tbody")[0].TAG("tr")[0].TAG("td")[0].TAG("div")[0].TAG("ul")[0].TAG("li")');
		for(var i=0,l=LI.length;i<l;i++){
			if(LI[i].innerHTML.indexOf("製造元リファレンス") != -1 ||
			   LI[i].innerHTML.indexOf("メーカー型番") != -1){
				ModelNumber = LI[i].innerHTML;
				break;
			}
		}
	
	}catch(e){}
}
if(ModelNumber != ""){
	ModelNumber = ModelNumber.replace(/<.*>\s*(.+?)\s*$/,"$1");
} else {
	KAKAKU.innerHTML = "<b>Error:</b>商品の型番が取得できませんでした。";
	return;
}

//APIでXMLを読み込んで表示する。
if(ModelNumber != ""){
	GM_xmlhttpRequest({
	  method:"GET", 
	  url:KAKAKU_API+encodeURIComponent(ModelNumber),
	  onload:function(x){
			ShowPrice(x,"KAKAKU");
	  }
	});
	if(APIKEY != "") {
		var WAIT = 500;
		var count = 40;		//500ミリ秒*40で最大20秒待つ
		(function(){
			if(CONECO.innerHTML.indexOf("Error:") != -1) {
//				pnir.OutputAddString("CONECO:型番で検索");
				GM_xmlhttpRequest({
				  method:"GET", 
				  url:CONECO_API+encodeURIComponent(ModelNumber),
				  onload:function(x) {
					ShowPrice(x,"CONECO");
				  }
				});
				return;
			}
			if(CONECO.innerHTML.indexOf("最低価格:") != -1) return;
			if(count<=0) return;
			count--;
			if(isIE) {
				_window.setTimeout(arguments.callee,WAIT);
			} else {
				setTimeout(arguments.callee,WAIT);
			}
		})();
	}
}

function ShowPrice(http,site) {
	if(site != "KAKAKU" && site != "CONECO") return;

	var parser = new DOMParser();
	var xml = parser.parseFromString( http.responseText, "text/xml" );
	var html;

	if(site == "KAKAKU") {
		var notfound = 'TAG("Message")[0].';
		var notfoundval = 'ItemNotFound';
		var urlstr = 'TAG("ItemPageUrl")[0].';
		var sitename = '価格.com';
	} else {
		var notfound = 'TAG("Count")[0].';
		var notfoundval = 0;
		var urlstr = 'TAG("Url")[4].';
		var sitename = 'coneco.net';
	}
	try {
		var msg = V(xml,notfound+xmltext);
	}catch(e){}
	if(msg == notfoundval) {
		html = "<b>Error:</b>"+sitename+"で該当商品が見つかりませんでした。";
	} else {
		try {
			var price = V(xml,'TAG("LowestPrice")[0].'+xmltext);
			if(price == "") price = 999999999;
			var diff = AmazonPrice - price;
			try{
				var pageurl = V(xml,urlstr+xmltext);
			}catch(e){
				pageurl == "";
			}
			if(pageurl == "") {
				if(site == "KAKAKU") {
					pageurl = "http://kakaku.com/";
				} else {
					pageurl = "http://www.coneco.net/";
				}
			}
			html ="<b>最低価格:<span class='priceLarge'> &yen;  "+SetPrice(price)+"</span></b>";
			if(diff > 0 && AmazonPrice != 0 && diff < 100000){
				html += " <font size=3>  Amazonより<span class='priceLarge'> &yen; "+SetPrice(diff)+"</span>安く買えます。</font>";
			}
			html +="<font size=3><a target='_blank' href="+pageurl+">"+sitename+"を見る</a></font>";
			eval(site+'.style.fontSize ="18px";');
		}catch(e){
			html = "<b>Error:</b>最低価格が取得できませんでした。";
		}
	}
	eval(site+'.innerHTML = html;');
}

//getElement(s)系を短い文字列で取得・設定
function V(pre,p /*,value*/) {
	var s = p;
	var cls = "class";

	if(isIE) cls = "className";

	s = s.replace(/\bID\s*\(/g,"getElementById(");
	s = s.replace(/\bTAG\s*\(/g,"getElementsByTagName(");
	s = s.replace(/\bNAME\s*\(/g,"getElementsByName(");
	s = s.replace(/\bATTR\s*\(\s*(["'])class(Name)?\1/g,'getAttribute("'+cls+'"');

	if(arguments.length >= 3) {
		try {
			eval("pre."+s+"=arguments[2]");
		} catch(e) {
			throw e;
		}
	} else {
		try {
			var v = eval("pre."+s);
		} catch(e) {
			throw e;
		}
		return v;
	}
}

//価格を3ケタ区切りにする関数
function SetPrice(price){
	var num = new String(price).replace(/,/g, "");
	while(num != (num = num.replace(/^(-?\d+)(\d{3})/, "$1,$2")));
	return num;
}


//価格を数値に変換
function ConvertPrice(price){
	price = price.replace(/-.+$/,"");
	return parseInt(price.replace(/\D/g,""));
}

}
})();