続・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;
	}
})();

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,""));
}

}
})();

Amazon Lowest Price CheckerのSleipnir版

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

 今さら説明するまでもないかもしれないけれど、アマゾンの商品ページを開いたときにカカクコムの最安価格と比較し、どちらが安いのかを表示してくれるスクリプト

 Firefoxもインストールしているものの、メインブラウザはSleipnirなのでそのままでは使えず、仕方がないので自分で何とかしてみた。

 とは言っても一から作り直したわけではなく、Sleipnir(というかTrident)では使えない機能を別の物に置き換えただけ。

 変更点は、大きく分けて以下の通り。

GM_xmlhttpRequestの追加
GM_xmlhttpRequest関数が存在しないため、ActiveXを利用して通信
class属性
<div class="hoge"> などとしたとき、getAttribute("class")では取得できないためgetAttribute("className")に変更
DOMParser
new DOMParser()が使えないために他のを探したが、探すのに時間がかかって面倒になり、そういうのを使うまでもないか?と思ったんで正規表現で代用


 一つ目。

 GM_xmlhttpRequestは、ここにあるLDR Full Feed for SeaHorseの中の物をほぼそのまま使用。

 ただ、何故か同期通信でやると価格が表示されないので、仕方なく非同期通信を使った。普通こういうのは同期通信でやるんだろうけどなあ。

 スクリプトの最後にalertを入れると同期通信でもちゃんと表示されるってことは、メインのスクリプトが終わった時点で全部が強制終了されてしまうからなんだろうか。

 JavaScriptにwait関数が無いのが辛い。


 二つ目。

 まあそのまま。

 class="hoge" としてるんだから "class" で取得できるのが普通だと思うんだけど、Tridentでは何故か出来ない。

 有名な話なのかもしれないけど、今回初めて知った。


 三つ目。

 今もう一度調べてみたら、parseFromStringだけならActiveXMSXML.DOMDocumentを利用して簡単に使えそう。

 その要素に対するgetElementsByTagNameなんかもそのまま使えるんだろうか。

 まあとりあえず今はこのままでいいや。


 細かいところは色々書き換えてあるけれど、Sleipnirで動かすために必須の変更点というのはこれぐらい。

 機能アップというか、型番取得の手法を一つ追加。

 とは言ってもASINでconecoから引っ張ってくるアレじゃないのであしからず。


 というわけで、以下のスクリプトを Sleipnir2\plugins\seahorse に "適当な名前.user.js" で保存すればOK。当然、要Seahorse

// ==UserScript==
// @name           Amazon Lowest Price Checker
// @namespace      http://gigi-net.net
// @include        http://www.amazon.co.jp/*
// @type           SleipnirScript
// ==/UserScript==
(function(){
var d = document;

if(V(d,'ID("buyboxTable")')==null) return;

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+'"');

	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,""));
}

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 if(window.XMLHttpRequest) {
		http = new XMLHttpRequest();
	} else return;
	http.open(opt.method.toUpperCase(), opt.url, false);
	for (var i in opt.headers) {
	    if (!opt.headers.hasOwnProperty(i)) continue;
		http.setRequestHeader(i, opt.headers[i]);
	}
	http.onreadystatechange = function() {
		if(http.readyState == 4){
			if(http.status == 200){//succeed
				opt.onload(http);
			} else {
				opt.onerror(http);
			}
			http = null;
		}
	};
	http.send(null);
}

//APIURL定義
var api_url ="http://api.kakaku.com/Ver1.1/ItemSearch.aspx";

//製造元リファレンス: EYE-FI-4GB-J 

//ロード中メッセージ表示
var title = V(d,'TAG("h1")');
var check_lowest = d.createElement("div");
check_lowest.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(check_lowest);

	//製品型番を取得
var kataban = "";
try{
	var dom_kataban = V(d,'ID("productDetailsDiv").childNodes[0]');
	for(var i=0,l=dom_kataban.childNodes.length;i<l;i++){
		if(dom_kataban.childNodes[i].innerHTML.indexOf("メーカー型番") != -1){
			kataban = dom_kataban.childNodes[i].innerHTML;
			break;
		}
	}
	if(kataban == "") 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){
				kataban = LI[i].innerHTML;
				break;
			}
		}
	
	}catch(e){}
}
if(kataban != ""){
	kataban = kataban.replace(/<.*>\s*(.+?)\s*$/,"$1");
}else{
	check_lowest.innerHTML = "<b>Error:</b>商品の型番が取得できませんでした。";
	title[0].parentNode.appendChild(check_lowest);
}
//価格comAPIを用いて型番から最安値を取得
xml_url = api_url +"?Keyword="+encodeURIComponent(kataban)+"&CategoryGroup=ALL&SortOrder=pricerank&HitNum=5";

//Amazon.comの価格を取得
var ap = 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"){
		ap = V(table[0],'TAG("b")[1].innerHTML');

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

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

//価格表示関数
function ShowPrice(x){
	var res = x.responseText;
	if(res.indexOf("<LowestPrice>") == -1){

		check_lowest.innerHTML="<b>Error:</b>価格.comで該当商品が見つかりませんでした。";
	}else{
		res.match(/<LowestPrice>(.+?)<\/LowestPrice>/);
		price = RegExp.$1;
		res.match(/<ItemPageUrl>(.+?)<\/ItemPageUrl>/);
		var pageurl = RegExp.$1;
		var sa = ap - price;
		check_lowest.innerHTML ="<b>最低価格:<span class='priceLarge'> &yen;  "+SetPrice(price)+"</span></b>";
		if(sa > 0 && ap != 0 && sa < 100000){
			check_lowest.innerHTML += " <font size=3>  Amazonより<span class='priceLarge'> &yen; "+SetPrice(sa)+"</span>安く買えます。</font>";
		}
		check_lowest.innerHTML +="<font size=3><a target='_blank' href="+pageurl+">価格.comを見る</a></font>";
		check_lowest.style.fontSize ="18px";
	}
	title[0].parentNode.appendChild(check_lowest);
}

//APIからXMLを読み込んで表示する。
if(kataban != ""){
	GM_xmlhttpRequest({
	  method:"GET", 
	  url:xml_url,
	  onload:ShowPrice,
	  onerror:function(){}
	});
}

//著作表示
var powered =d.createElement("div");
powered.innerHTML +=" <center>powered by <a href ='http://kakaku.com/'>価格.com</a></center>";
d.body.appendChild(powered);

})();

 ほんとはOperaにも…と思ったけど、他ドメインにアクセスする方法が分からなかったので断念。

かんなぎトレス疑惑?

 かんなぎ6話「ナギたんのドキドキクレイジー」に出てきたメイドの写真を見たとき、「ぉはぎ?」と思ってしまった。

 この作品に矢作紗友里は出演してないから別の人だろうけどね。



追記:

http://www.new-akiba.com/archives/2008/11/6_59.html

 やっぱ違ったみたい。

GuiTest 1.54


 Win32::GuiTestというモジュールがある。

 これのバージョンは現在1.54で、CPANを使ってインストールできる…はずが、テストの部分で止まってしまう。

 これは、テストが英語版のCALCを想定して作られているのが原因で、何カ所か書き換えることでテストを通すことが出来る。

 まあ force install Win32::GuiTest として強引にインストールしてしまってもいいのかもしれないが…。

 とりあえずここを開いて右上にある Win32-GuiTest-1.54.tar.gz からソースを取得し、適当な場所に展開してから t\02_calc.t を開き、下記の通り書き換える。

30行目

	MenuSelect('表示(&V)|関数電卓(&S)'); sleep 1;

49行目

	($hex) = FindWindowLike($calc, "16 進");

58行目

	($bin) = FindWindowLike($calc, "2 進");

65行目

	($oct) = FindWindowLike($calc, "8 進");

71行目

	($dec) = FindWindowLike($calc, "10 進");

94行目

	PushButton("10 進"); sleep 1;

98行目

	PushButton("16 進"); sleep 1;

102行目

	PushButton("8 進"); sleep 1;

106行目

	PushButton("2 進"); sleep 1;

 あとは README に書いてあるとおり、

perl makefile.pl
nmake
nmake test
nmake install

 をすればいい。

 30行目に sleep 1 が増えているが、これはPerl(というかGuiTest)が電卓に対して終了命令(ALT-F4)を出したものの、関数電卓に切り替え中でそれを華麗にスルーしたため終了せず、切り替えが完了した電卓と、新たに起動された電卓とで二つになってしまったため、ウエイトを入れてある。

 もしウエイトを入れなくても、残った電卓を手動で終了させればテストは続行されるからそれほど問題はない。

 というかそんな古いマシンでもなければウエイトが無くても大丈夫かも。


 そうそう、GuiTestのインストールにはCコンパイラが必要なため、入ってない場合はmakeの時点でエラーが出る。

 そう言うときはCコンパイラをインストールするか、素直に1.50で我慢する(ppdが1.50.5だから)しかない。

バッチでバックアップ

 13日02:15 コメントで指摘のあった部分を訂正&追記。
 16日21:20 ブロック外のため、FORにする必要の無かったファイルサイズ取得部分を修正。

はじめに

 FCE Ultra(以下FCEU)というファミコンエミュレータがある。

 これにはムービー記録の機能が付いていて、それで記録したファイルや、それを後で再生しながらAVI等に録画したファイルを相手に渡すことで、ゲームのプレイ動画を相手に見せることが出来る。

 また、記録中にスナップショット(どこでもセーブ)を取っておけば、失敗したときにそれをロードしてそこから何度でもやり直すことが出来る。もちろん記録ファイルには失敗した部分は残らない。

 こういう機能を使ってTAS Videoは作られている。もちろんこの機能だけではそれなりの動画しか作れないが…。

 まあ詳しいことはGamesTechなどを見てもらった方がいい。

本題

 ここでは、そのムービー記録を行っているときに作成されるファイルを、自動的にバックアップするバッチファイルを紹介する。

 ちなみに、拡張機能を使っているため、2000以降のOSでないと動かないと思う。

 まずはソース(バッチでもそう呼ぶのかな?)。

@echo off

set P=bku.log
set TMPF=temp.$$$

set D=%DATE:~-10%
set T=%TIME:~0,8%

set DT=%D:/=%%T::=%
set DT=%DT: =0%

echo 日時:%DT%>%P%
echo ============>>%P%
echo セーブデータ>>%P%
echo ============>>%P%

for %%f in (.\fcs\*.fc?) do (
 echo savefile: %%f>>%P%
 call :bku "%%f" fcs
)

echo ------------------------------->>%P%
echo.>>%P%
echo ==============>>%P%
echo ムービーデータ>>%P%
echo ==============>>%P%

for %%f in (.\movie\*.fcm) do (
 echo moviefile: %%f>>%P%
 call :bku "%%f" movie
)

goto end

:bku
setlocal ENABLEDELAYEDEXPANSION
set FILE=%1

echo %FILE%

if "%2"=="fcs" (
 set FN=!FILE:~7,-1!
) else (
 set FN=!FILE:~9,-1!
)

set FNAME=%FN:~0,-4%
set EXT=%FN:~-3%

set FSIZE1=%~z1

for %%f in (".\%2\bku\%FNAME%_??????????????.%EXT%") do (
 for %%S in ("%%f") do set FSIZE2=%%~zS
 if !FSIZE1! EQU !FSIZE2! (
  echo fc /b "%%f" %FILE%>>%P%
  fc /b "%%f" %FILE%>%TMPF%
  for %%S in (%TMPF%) do set FSIZE=%%~zS
  if !FSIZE! LSS 1000 (
   FOR /F "eol=0 skip=1 tokens=2" %%D IN (%TMPF%) DO IF "%%D"=="相違点は検出されませんでした" goto no_diff
  ) else (
   echo skipped[chk]: %%f>>%P%
  )
 ) else (
  echo skipped[fc]: %%f>>%P%
 )
)

echo copy %FILE% ".\%2\bku\%FNAME%_%DT%.%EXT%">>%P%
copy %FILE% ".\%2\bku\%FNAME%_%DT%.%EXT%"

goto bku_end

:no_diff
echo 既にバックアップ済み>>%P%

:bku_end
echo.>>%P%
endlocal
exit /b

:end
del %TMPF%
start fceu.exe

 これを適当なファイル名.bat(たとえばbku.batとか)でFCEUをインストールしたフォルダに保存すればいい。

 このまま実行しても特に問題はないが、好みに応じて二つほど設定できる項目がある。

 最初に出てくる

set P=bku.log
set TMPF=temp.$$$

 のP(rint)は、このようにファイル名にすればそのファイルにログが記録されるし、CON とすれば画面に出て、NUL とすれば闇に葬られる。

 そのすぐ下は

echo 日時:%DT%>%P%

 となっているから、ファイル名を指定した場合は毎回上書きされてしまう。追記したい場合は > を >> にすればよい。

 次の TMPF はテンポラリファイルの指定。上書きをするので、存在しない・消えても構わないファイル名にしよう。

動作

 現在のスナップショット、ムービーデータが既にバックアップされているかどうかをチェックしてからコピーしているから、重複してコピーされることはない。

 内容チェックは、全ファイルに対して行うとかなり時間が掛かってしまうため、必要のない比較は行わないようにしている。

 まず、ファイルサイズのチェック。サイズが違うのに内容が同じと言うことはあり得ない。これで一番時間の掛かる fc が飛ばせる。

 次に、fc が返してきた内容をリダイレクトしたファイルのサイズをチェック。1000バイト以下の時のみチェックする。内容が一致している場合は、恐らく200〜300バイト以内には収まっているはずだが、多少余裕を持って1000バイトということにした。ファイルパスの長さもあることだし。

 また、FOR を回すときにも、0から始まる行はスキップすることで無駄な IF が実行されるのを防いでいる。

ファイル .\MOVIE\BKU\A_20071011212801.fcm と .\MOVIE\A.FCM を比較しています
0000000C: AD 8E
0000000D: 77 76
00000010: 9B 15

 こういった、下の三行みたいなのを飛ばすというわけだ。不一致部分が多くなると、先頭に1が出てくることもあるが、1000バイト制限を掛けているので問題はない。

 一致しているときには

FC: 相違点は検出されませんでした

 と出るわけで、0で始まる行が一致する訳がない。

 こうやって内容チェックを行い、途中で一致するファイルが出てきたらバックアップ済み、出てこなければまだバックアップしてないということでそのファイルをコピーする。

 ちなみに、fc が返した ERRORLEVEL を利用せずに FOR を使ってチェックしている理由は、fc の返す値が2000とXPとで違うため。

 2000では、たとえ内容が違っていてもファイルサイズが同じなら0を返すのに対し、XPでは、内容が違えば1を返す。

 普通に考えればXPでの挙動が当然のはずなんだけど、なんで2000はこんな仕様になっているのやら…。

バッチファイルについて

 それにしても、久々にバッチファイルに触れてみてびっくりした。

 これまでもバッチファイルはたまに作っていたけど、MS-DOS時代の機能しか使っていなかった。

 知らない間にこんなに進化していたとは…。

 まず、環境変数の値の、途中だけを取り出したり簡単な置換が出来るようになっている。

 文字列の長さを知ることが出来ればもっと便利なんだけどな。

 そして、不完全ながら IF や FOR でブロック文が使えるようになっている。

 不完全というのは、ブロック内で goto を使うと、そこがブロックであったことを忘れてしまうから。

たとえば、

for %%i in (1 2 3) do (
 if "%%i"=="2" goto next
 echo %%i
:next
echo next
)

 こんなのを実行したとする。期待しているのは、

1
next
next
3
next

 なのに、実際には

1
next
next

 となってしまう。

 まあバッチファイルにあまり凄いことを求めすぎてもいけないのかな。

 その IF や FOR 自体もいろいろ機能アップしていて、IF では、比較演算子というものが導入されている。このバッチファイルでも使っているが、EQU や LSS 等がそれに当たる。

 FOR の方はかなり色々と追加されていて…HELP FOR などで見た方がいいかもしれない。

 本来、一つのファイルのサイズを求めるためだけなら FOR を使わずに %~z1 なんかを使った方がいいのかもしれないが、うまくいかなかったから仕方がない。

 %1と%2は既に使っていたから実際には%3で %~z3 となるのだけど、サイズを取得したいテンポラリファイルを削除した状態で始めると空欄になってしまったし、存在する状態だと同じサイズばかりを報告してきた。fc を実行した直後に取得しているのに。

 ループ内でやっていたため、おそらく後述する即時変数展開が原因だろう。

 まあそんなわけで、%~z1 を使わずに FOR を利用して取得するようにしたのだ。


 さて、その即時変数展開の話。

 ブロック内に書かれた変数は、そのブロックが読み取られた時点で展開されてしまう。

 HELP SET に書いてあるサンプルを少しだけいじったものだが、これを見ればわかりやすいだろう。

set VAR=before
if "%VAR%" == "before" (
 set VAR=after
 echo %VAR%
)

 これ、どう見たって画面には after と表示されるとしか思えないだろう。なんせ VAR に after をセットした直後にその内容を echo なんだから。

 でも、実際に出力されるのは before である。何故?

 IF文の行まで来たとき、( があるからブロックであることが分かる。そして ) まで読み込み、そこで変数の内容が展開される。そうすると、上の内容が

if "before" == "before" (
 set VAR=after
 echo before
)

 に置き換えられてから実行される。

 つまり、いくら VAR に値をセットしても、次の行は echo before となってしまっているため、before が出力されるのは当然ということになる。

 これを回避するには、cmd /V:ON としてコマンドプロンプトを起動するかバッチファイル内に

setlocal ENABLEDELAYEDEXPANSION

 と書くかして遅延環境変数の展開を有効にした上で、環境変数を % ではなく ! で括ればいい。

 要するに

setlocal ENABLEDELAYEDEXPANSION
set VAR=before
if "%VAR%" == "before" (
 set VAR=after
 echo !VAR!
)
endlocal

 とすれば、after と表示されるようになる。

参照:http://f32.aaa.livedoor.jp/~kobun/index.php?CMD.EXE%A4%CE%C3%D9%B1%E4%B4%C4%B6%AD%CA%D1%BF%F4%A4%CE%C5%B8%B3%AB

 というわけで先に書いた「ファイルサイズが正常に取得されない」というのは、ループ内で fc→%~z1 とやったために、ループに入る前、つまり fc がテンポラリファイルを作成する前にファイルサイズを取得しに行ったため、ファイルが無い状態で実行した時は空欄、存在した状態で実行した時は前に実行したファイルのサイズを返してきたのだろう。

 出来るかと思って、一応 !~z1 なんてことをしてみたのだけど、そのまま !~z1 と表示されてしまった。うーん。


 あともう一つ。

 サブルーチンのような物が使えるようになっている。

 実際には、バッチファイルの中から別のバッチファイルを呼び出す call 命令を拡張したような物みたいだけど。

 call の引数として、ファイル名ではなくラベルを指定すればいい。

 return の代わりに goto :eof もしくは exit /b と書けば戻ってくる。サブルーチン側で単に exit としてしまうと、大元のバッチファイル自体が終了してしまうので注意。


 というわけで何が何だか分からなくなってしまったが、まあ「バッチファイルでも色々出来るんだね!」という感じでよろしく。

なかのひと


 いつも見てるサイトに載ってたから、あまりよく意味も分からないままに設置してみた。

 アクセス解析の一種のようだけど、一般的なプロバイダからのアクセスは記録せず、企業とか機関とか…そういうところからのアクセスだけを拾うのかな?

 だとしたら、ただでさえあまりアクセスのないのに、設置しても意味がないかも?

 まあとりあえずしばらく様子を見てみて、どうするか考えてみようか…。