続・Amazon Lowest Price Checker改造版 Sleipnir/Firefox

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

 またまたこのネタ。

 今回も変更なしでSleipnir(2.8.5+IE8)のSeaHorseFirefox(3.0.11)のGreasemonkeyのどちらでも動作する。

 ちなみに括弧内は動作を確認したバージョンだが、そんなに古いバージョンでなければ恐らく動作するはず。

 あと、SleipnirではShift_JISFirefoxではutf-8でソースを保存するのがいいらしい。


 それで何が変わったのかというと、商品のバリエーション(色とか容量とか)を切り替えたときにもそれに対応した値段を表示できるようになった。

 ただし、Webページ上のJavascriptとして動作させなければならないという制限から、JSONPに対応しているconeco.netのみの対応。

 もしかしたらuserscriptとして動かせるのかもしれないが、やり方が分からないのでそのようになっている。


 まあ機能的な変更点と言えばそれぐらい。ただ、内部的には色々変わっている。

 まずは前回気になっていたtry〜catch祭りをなんとかした。

 これはもう単純にVが例外をthrowせずに黙殺してnullを返すように変更しただけ。

 ついでにVに渡す似非プリプロセッサで使える文字列を、必要に迫られて増やした。

 あとは値段や型番、ASINコードを取得する要素を変更したのと、coneco.netから値段を引っ張ってくるのをJSONPに変更した。

 この変更により、Firefoxで動かしているときに二度目以降はJSONPのコールバック関数が呼び出されないという現象が起こった。

 調べてみると、FirefoxではSCRIPTタグのsrcを変更しても再設定されたアドレスを読みに行かない仕様になっているらしい。

 これは追加した要素を一旦削除することで解決。


 さて、今回の「バリエーションを選択すると値段も変わる」の実装が、タイミング的にけっこう微妙なことになっている。

 というのも、新しい型番や値段の読み込みが終わったかどうかの判定をローディングバー(「ロード中...」というアレ)があるかどうかで行っているのだが、これが表示されるのはバリエーションのボタンを押してすぐではなく、一瞬の間があってからである。

 従ってウエイトを入れてからローディングバーの判定処理に行っている。

 さらに、ローディングバーが消えてすぐ型番等を読みに行くと何故かまだ書き換えが終わっておらず、前のデータを読んでしまった。なのでここにもウエイトを入れた。

 これらのウエイト値はとりあえず500ミリ秒に設定してあるが、環境や回線状況によって変わってくるかもしれない。変更場所は後述。

 そして、こういった処理を入れるとどういうわけだか「いまどちらのバリエーションが選択されているのか」を示すオレンジの枠の書き換えが行われなくなってしまった。

 仕方がないので自前で書き換えてしまっているが、これで大丈夫なのかなあ。

 それよりもどうして書き換えられなくなってしまうんだろう。不思議だ。


 というわけで、このスクリプトを使う前に変更が必要な部分。

 前回と同様、coneco.netのAPIKEYが必要になる。

 このアドレスで取得できる。必要なのはメールアドレスのみ。

 ここで取得したAPIKEYを94行目に書き込む。もしAPIKEYが123456789ならば

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

 となる。

 もう一つ、これは必須ではないが、もしバリエーション選択時に値段が正常に切り替わらない場合、これらのウエイト値を変更することでうまくいくかもしれない。

 一つ目のウエイトは72行目で、ボタンを押してからローディングバーが出るまでの待ち時間。

		setTimeout("AC()",500);

 これの 500 の部分を変更する。500で0.5秒、1000で1秒となる。

 二つ目は80行目で、これはローディングバーが消えてから型番等を読みに行くまでの待ち時間。

				setTimeout("amazon(1)",500);

 これも上と同様、500の部分を変更する。


 そういえば、価格.comも8月からAPIKEYが必要になる。既に登録が出来るようになっているが、coneco.netに比べると必須項目が多いので、ちとめんどい。

 それに併せてJSONPにも対応してくれるのかと思ったが、そういうわけではないらしい。残念。


 それはさておき、coneco.netのAPIKEYを入れないとconeco.netの値段を引っ張ってこないようになっているので注意。

// ==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

//	sleipnir.API.OutputClear();
(function() {
	if (!V(document, 'ID("buyboxTable")')) return;

	var rep = V(document, 'ID("jsSwatches").innerHTML');
	if (rep) {
		rep = rep.replace(/return\s+goTwisterVariations.onClickSwatch\((.+?)\)/g, 'delayAC($1);return false;');
		V(document, 'ID("jsSwatches").innerHTML', rep);
	}

	if (!V(document, 'ID("regCode")')) {
		var regCode = V.toString() + amazon.toString() + CONECO_Callback.toString() + SetPrice.toString() + delayAC.toString() + AC.toString();
		var div = document.createElement("div");
		div.id = "regCode";
		div.innerHTML = "&nbsp;<script defer='defer'>" + regCode + "</script>";
		document.body.appendChild(div);
	}
/*@cc_on @if(true)
	var scriptControl = new ActiveXObject("ScriptControl");
	if (scriptControl && !_window[ScriptName]) {
		_window[ScriptName] = true;
		scriptControl.Language = "JScript";
		scriptControl.AddObject("scriptControl", scriptControl);
		scriptControl.AddObject("_window",      _window);
		scriptControl.AddObject("_document",    _document);
///		scriptControl.AddObject("sleipnir",      sleipnir, true);
		scriptControl.AddCode(freeSC.toString());
		scriptControl.AddCode(V.toString());
		scriptControl.AddCode(SetPrice.toString());
		scriptControl.Eval("_window.attachEvent('onunload', freeSC)");
		scriptControl.AddCode(amazon.toString());
		try {
			scriptControl.Run("amazon", 0);
		}catch(e){
			sleipnir.Output.Print('Error(' + scriptControl.Error.Source + ') @ line ' + (scriptControl.Error.Line + 91));
			sleipnir.Echo(' ' + scriptControl.Error.Description);
			freeSC();
		}
	}
	function freeSC() {
		scriptControl = null;
		_window.detachEvent('onunload', freeSC);
	}
	@else@*/

	amazon(0);

	/*@end @*/

	function delayAC(key,val) {
		var VarCount = DetailPage.StateController.getState()['num_total_variations'];

		goTwisterVariations.onClickSwatch(key,val);

		for(var i = 0; i < VarCount; i++) {
			if(i == val) {
				V(document,'ID("' + key + '_' + i + '").sATTR("class","swatchSelect")');
			} else {
				V(document,'ID("' + key + '_' + i + '").sATTR("class","swatchAvailable")');
			}
		}
		setTimeout("AC()",500);
	}

	function AC() {
		var WAIT = 500;
		var count = 40;		//500ミリ秒*40で最大20秒待つ
		(function(){
			if(!V(document,'ID("more-buying-choice-content-div").ATTR("loadingbarset")')) {
				setTimeout("amazon(1)",500);
				return;
			}
			if(count <= 0) {
				amazon(1);	//あきらめる
				return;
			}
			count--;
			setTimeout(arguments.callee,WAIT);
		})();
	}

	function amazon(mode) {
		//ここに取得したAPIKEYを入れる
		var APIKEY = "";

		//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_json?apikey="+APIKEY+"&categoryId=0&sort=price&count=1&callback=CONECO_Callback&freeword=";

/*@cc_on @if(true)
		if (typeof _document != "undefined") {
			var d = _document;
			var w = _window;
		} else {
			var d = document;
			var w = window;
		}
		var XMLTEXT = ".text";
//		var isIE = true;

//		var pnir = sleipnir.API;

		@else@*/

		var d = document;
		if(typeof unsafeWindow != 'undefined') {
			var w = unsafeWindow;
		} else {
			var w = window;
		}
		var XMLTEXT = ".textContent";
//		var isIE = false;
		/*@end @*/


		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")');
		if(mode == 0) {
			var KAKAKU = V(d,'ID("KAKAKU")');
			if(!KAKAKU) {
				KAKAKU = d.createElement("div");
				KAKAKU.id = 'KAKAKU';
			}
			KAKAKU.innerHTML = "<img src='http://img.f.hatena.ne.jp/images/fotolife/g/gigi-net/20090421/20090421143419.gif?1240292104' style='vertical-align:middle'>価格.comから最低価格を読み込んでいます。";
			if(!V(d,'ID("KAKAKU")')) title[0].parentNode.appendChild(KAKAKU);
		}
		if (APIKEY != "") {
			var CONECO = V(d,'ID("CONECO")');
			if(!CONECO) {
				CONECO = d.createElement("div");
				CONECO.id = 'CONECO';
			}
			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から最低価格を読み込んでいます。";
			if(!V(d,'ID("CONECO")')) title[0].parentNode.appendChild(CONECO);
		}
		if(!V(d,'ID("CREDIT")')) {
			//クレジット表示
			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.id = 'CREDIT';
			CREDIT.innerHTML = html;
			title[0].parentNode.appendChild(CREDIT);
		}

		var ASIN = V(d, 'ID("ASIN").value');

		//Amazon.comの価格を取得
		var price = V(d, 'ID("priceBlockTwister").TAG("div")[0].TAG("table")[0].TAG("tbody")[0].TAG("tr")');
		if (!price) {
			price = V(d, 'ID("priceBlock").TAG("table")[0].TAG("tbody")[0].TAG("tr")');
		} else {
			ASIN = "";
		}
		if (price) {
			for (var i = 0; i < price.length; i++) {
				var table = V(price[i], 'TAG("td")');
				if (V(table[1], 'TAG("b")[0].ATTR("class")') == "priceLarge") {
					AmazonPrice = V(table[1], 'TAG("b")[0].innerHTML');
					break;
				} else
				if (V(table[1], 'TAG("span")[0].ATTR("class")') == "priceLarge") {
					AmazonPrice = V(table[1], 'TAG("span")[0].innerHTML');
					break;
				}
			}
		} else {
//			pnir.OutputAddString("Amazon価格取得できず");
			AmazonPrice = 0;
		}
		AmazonPrice = ConvertPrice("" + AmazonPrice);
		w.AmazonPrice = AmazonPrice;

		//製品型番を取得
		var ModelNumber = "";
		var DOM_ModelNumber = V(d, 'ID("productDetailsDiv").childNodes[0]');
		for (var i = 0, l = V(DOM_ModelNumber, 'childNodes.length'); i < l; i++) {
			if (V(DOM_ModelNumber, 'childNodes[' + i + '].innerHTML.indexOf("メーカー型番")') != - 1) {
				ModelNumber = DOM_ModelNumber.childNodes[i].innerHTML;
				break;
			}
		}
		ModelNumber = ModelNumber.substr(0, 50);	//snip
		if (ModelNumber == "" || ASIN == "") {
			try{
				var DOM_link = V(d, 'ID("productDetails")');
				for (var i = 0; i < 10; i++) {
					DOM_link = DOM_link.nextSibling;
					if (!DOM_link) throw new Error('dead link');
					if (DOM_link.nodeName == 'TABLE') break;
				}
				var DIV = V(DOM_link, 'TAG("tbody")[0].TAG("tr")[0].TAG("td")[0].TAG("div")');
				for (i = 0; i < 5; i++) {
					if(V(DIV[i],'ATTR("class")') == "content") break;
				}
				var LI = V(DIV[i], '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;
						if(ASIN) break;
					} else
					if(LI[i].innerHTML.indexOf("ASIN:") != - 1) {
						ASIN = LI[i].innerHTML;
						ASIN = ASIN.replace(/<.*>\s*(.+?)\s*$/, "$1");
						if(ModelNumber) break;
					}
				}
			}catch(e){
//				pnir.OutputAddString("ModelNumber見つからず:"+e.description);
			}
		}
		//coneco.netの価格を取得
		if (APIKEY != "") {
			if (ASIN) {
				JSON_Request("CONECO", ASIN);
			} else {
				CONECO.innerHTML = "<b>Error:</b>ASINコードが取得できませんでした。";
			}
		}

		if (ModelNumber != "") {
			ModelNumber = ModelNumber.replace(/<.*>\s*(.+?)\s*$/, "$1");
		} else {
			KAKAKU.innerHTML = "<b>Error:</b>商品の型番が取得できませんでした。";
			return;
		}
		//APIでXMLを読み込んで表示する。
		if(mode == 0) {
			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:型番で検索");
					JSON_Request("CONECO", ModelNumber);
					return;
				}
				if(CONECO.innerHTML.indexOf("最低価格:") != -1) return;
				if(count <= 0) return;
				count--;
				w.setTimeout(arguments.callee,WAIT);
			})();
		}
		function JSON_Request(site, product) {
//			if (site != "KAKAKU" && site != "CONECO") return;
			if (site != "CONECO") return;

			var JSON = V(d,'ID("' + site + '_JSON")');
			if(JSON) d.body.removeChild(JSON);

			JSON = d.createElement('script');
			JSON.charset = 'UTF-8';
			JSON.id = site + '_JSON';
			if (site == "CONECO") {
				JSON.src = CONECO_API + product;
			}
			d.body.appendChild(JSON);
		}
		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';
			}
			var LOWEST = 'TAG("LowestPrice")[0]';
			var msg = V(xml, NOTFOUND + XMLTEXT);
			if (msg == NOTFOUNDVAL) {
				html = "<b>Error:</b>" + SITENAME + "で該当商品が見つかりませんでした。";
			} else {
				getprice : 
				{
					var price = V(xml, LOWEST + XMLTEXT);
					if (price != null) {
						if (price == "") {
							price = 999999999;
						}
						var diff = AmazonPrice - price;
					} else {
						html = "<b>Error:</b>最低価格が取得できませんでした。";
						break getprice;
					}
					var pageurl = V(xml, URLSTR + XMLTEXT);
					if (pageurl == null) {
						//				pnir.OutputAddString("urlみつからず");
						var 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";');
				}
			}
			eval(site + '.innerHTML = html;');
		}
		//価格を数値に変換
		function ConvertPrice(price) {
			//	price = price.replace(/-.+$/,"");
			return parseInt(price.replace(/\D/g, ""));
		}
	}

	function CONECO_Callback(data) {
		var html;
		var count = V(data, 'Header.Page.Count');
		if (count == null || count == 0) {
			html = "<b>Error:</b>CONECOで該当商品が見つかりませんでした。";
		} else {
			getprice:
			{
				var price = V(data, 'Item[0].LowestPrice');

				if (price != null) {
					if (price == "") {
						price = 999999999;
					}
					var diff = AmazonPrice - price;
				} else {
					html = "<b>Error:</b>最低価格が取得できませんでした。";
					break getprice;
				}
				var pageurl = V(data, 'Item[0].Url');
				if (pageurl == null) {
//					pnir.OutputAddString("urlみつからず");
					var pageurl = "";
				}
				if (pageurl == "") {
					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 + ">CONECOを見る</a></font>";
				V(document, 'ID("CONECO").style.fontSize', '18px');
			}
		}
		V(document, 'ID("CONECO").innerHTML', html);
	}

	//getElement(s)系を短い文字列で取得・設定
	function V(pre, p /*,value*/) {
		var s = p;
		var cls = "class";
		if (/*@cc_on!@*/false) 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 + '"');
		s = s.replace(/\bATTR\s*\(/g, 'getAttribute(');
		s = s.replace(/\bsATTR\s*\(\s*(["'])class(Name)?\1\s*,\s*(["'])(.+)\3/g, 'setAttribute("' + cls + '","$4"');
		s = s.replace(/\bsATTR\s*\(/g, 'setAttribute(');

		if (arguments.length >= 3) {
			try{
				eval("pre." + s + "=arguments[2]");
			}catch(e){
				return false;
			}
			return true;
		} else {
			try{
				var v = eval("pre." + s);
			}catch(e){
				return null;
			}
			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;
	}
})();