「便利!簡単!すぐ出来る!」雑用に役立つJScriptを見直してみた。

巷ではnode.jsが流行っている、そんなご時世だから(?)JScriptを使ってみた。
いや、サーバーサイドJSとはジャンルが違うけど。
とにかくJavaScriptで簡単にバッチが書けるという噂の言語を書いてみたんだ。

で、JScriptって?

  • JScriptの特長
    • JavaScript1.5互換
    • Windows上で動き、ファイルアクセスも出来る
    • コンパイル無しで.jsファイルをダブルクリックですぐ実行
    • WindowsXPをインストールした段階で実行可能


JScriptは上記のような利点を持っていて
外部のネットワークと隔離されて自由にソフトは入れられない環境でも
誰もが少しは知ってるJavaScriptで書いてすぐ動かせるし、
ITドカタにはうってつけの言語なのだ!(発言の責任は持たない)


で、何をするのかというと日々の作業の中で自動化出来るものを
JScriptで書いてバッチ化することにより作業時間を短縮し、
仕事をしているふりをする時間を増やそうという作戦である。


つまり、以下のようなことに役立つのではないかと。

  • ダミーの連番のテストデータ作成(1回しか必要ないけど整合性が必要なものとか)
  • 大量の出力結果の確認(深いとこにあるフォルダを順に見ていくとか)
  • 仕事がはかどっているふりをするとき(あともう少しで定時だ!)

JScriptでファイル操作

とりあえず、どんなバッチつくるにもファイル操作は必須だと思うのでその方法を。
ファイル操作にはFileSystemObjectを使う。
具体的には以下のコードを書いてみた。テキストファイルの内容を読み取るだけ。

//ファイル操作のためのオブジェクトFileSystemObjectをnew ActiveXObject()で取得。
var fs = new ActiveXObject("Scripting.FileSystemObject");
//定数を自分で宣言・・・しなきゃならない。(普通に数値突っ込んでもいいけど分かりづらすぎるw)
var MODE = {
	//ファイルオープンモードの列挙
	READING: 1,
	WRITING: 2,
	APPENDING: 8
};
//ファイルオブジェクト取得
var file = fs.getFile("test.txt");
//JavaのFileInputStream的な感じ(バッファリングが既にされてるし、InOutの区別はないけど。)
var fis = file.OpenAsTextStream(MODE.READING);
/*
↓これでもファイルのストリームを取得できる
var fis = fs.openTextFile("test.txt", MODE.READING);
*/
var buf = "";
//ファイルの終わりまで1行づつ読み込む
while (!fis.atEndOfStream){
	buf += fis.readLine() + "\r\n";
}
fis.close();//ストリームは閉じる

//WScript.echoでメッセージ出力
WScript.echo(buf);

//WScript.quitで引数で指定した終了コードでプロセス終了
//別になくても一番下までいけば落ちるけど
WScript.quit(0);

"test.txt"の内容

本日は
晴天ナリ。

出来上がったコードを拡張子「.js」ファイルで保存してそれをダブルクリックするだけで動く!


実行結果。よし。


ファイル書き込み例。
ログファイル的なノリのゴミファイルを出力してみる。

var fs = new ActiveXObject("Scripting.FileSystemObject");
var MODE = {
	READING: 1,
	WRITING: 2,
	APPENDING: 8
};

function fillzero(num, length){
	var numStr = (""+num);
	var numStrLength = numStr.length;
	while(numStrLength++ < length){
		numStr = "0" + numStr;
	}
	return numStr;
}

var today = new Date();
var filename = today.getFullYear() + 
		fillzero(today.getMonth()+1, 2) + 
		fillzero(today.getDate(), 2) + ".txt";

//第3引数を追加してtrueを指定で開くファイルが存在しない場合勝手につくってくれる。
var fos = fs.openTextFile(filename, MODE.APPENDING, true);

var buf = "";
//1行書きこむ(追記モード)
fos.writeLine(today.toString() + " 実行!");
fos.close();

WScript.quit(0);

実行するごとにファイルに追記していくプログラムになっている。



よく使いそうなファイル操作

var fs = new ActiveXObject("Scripting.FileSystemObject");
var fis = fs.openTextFile("test.txt", 1);
var fos = fs.openTextFile("test2.txt", 2, true);

//TextStream#readAll:開いたファイルの内容をすべて取得出来る。Javaにはない。ちょっといいかも。
text = fis.readAll();
//TextStream#writeLine:1行書き込み。ファイルを上書きまたは追記モードで開く必要あり。
fos.writeLine("こんにちわ World!!");
//FileSystemObject#copyFile:ファイルコピー。
fs.copyFile("src.txt", "dist.txt");
//FileSystemObject#fileExists:指定したファイルが存在すればtrue。
WScript.echo( fs.fileExists("test.txt")?"ある":"ねーよ" );

その他はMSDNを参照

FileSystemObject リファレンス
http://msdn.microsoft.com/ja-jp/library/cc409800.aspx

JScriptWindows Script Hostの機能を使用

何かを処理するためには、引数とか取得できたほうがなにかと便利。
引数は「WScript.arguments」に入っている。


argumentsの型はWshArgumentsというオブジェクトで、
これはコレクションというJScript独自の、集合を表すオブジェクトの仲間で、
複数の引数を入力した場合もそれぞれ取り出すことができる。
が、このコレクションを処理する場合、JScript独特の書き方になる部分が登場。
JScriptのEnumeratorクラスを使わないといけない場面が出てくる。

var args = WScript.arguments;//とりあえず、長いので変数に入れ替えるw

/*
JavaScriptの配列やオブジェクトではない
WScript.argumentsはfor inでは処理できない。
for(var arg in args){//動かない
	WScript.echo(args[i]);
}
*/

//Enumeratorクラスを使って処理する。引数にコレクションを渡してインスタンス化。
var iterator = new Enumerator(WScript.arguments);
//JavaのIteratorに似たことをする。
for(;!iterator.atEnd();iterator.moveNext()){
	item = iterator.item();
	WScript.echo(item);//itemには引数のStringが入っている。
}


//ちなみにlengthが取得できるので普通のfor文でも書ける。
var len = args.length;
for(var i = 0; i < len; i++){
	//コレクションの要素にインデックスでアクセスするには
	//args[i]ではなくargs(i)と書く。メソッドだから。
	WScript.echo(args(i));
}

WScript.quit(0);

Enumeratorを使うとなんか目がチカチカするコードが出来上がりそうな…
なのでlengthを取得して書いたほうがシンプル。あれ?Enumeratorいらない子

  • 追記:

FileSystemObjectのFilesとかFoldersのコレクションは
Enumerator使わないと反復処理出来なかった・・・。少しだけいる子だった…。


ファイルを引数にしたい場合、
エクスプローラ上で引数にしたいファイルを.jsファイルの上にドラッグアンドドロップすると
そのファイルの場所のフルパスを引数として起動する。
複数同時にドラッグアンドドロップした場合もコレクションを処理するからそれぞれ取得できるわけ。



ついでにシェルのコマンドを実行出来るWshShellオブジェクトも書いたので紹介してみる。
WshShell.runメソッドでコマンドプロンプトで使えるコマンドを実行することが可能!

var shell = new ActiveXObject("WScript.shell");
var WINDOW_STYLE = {
	HIDE: 0,
	SHOW: 1
	//この他にもたくさんあるので後述のリファレンス参照
};


//runメソッドには
//「コマンド, ウインドウスタイル, 実行したコマンドが終了するまで待つか」を指定。
shell.run("cmd /K java -version", WINDOW_STYLE.SHOW);//第3引数を省略の場合false
WScript.echo("コマンド実行後すぐにここに来る");


var ret = shell.run("calc", WINDOW_STYLE.SHOW, true);//終了まで待つ場合は終了コードが得られる
WScript.echo("コマンド実行完了後にここに来る\r\n終了コード:" + ret);

WScript.quit(0);


応用すると任意のフォルダをエクスプローラで開くことが出来る。

var fileProtocolHandler = "rundll32.exe url.dll,FileProtocolHandler ";
var path = "C:\\";
shell.run(fileProtocolHandler + path);//Cドライブを開いた状態のエクスプローラが立ち上がる
var file = "test.txt";
shell.run(fileProtocolHandler + file);//ファイルを関連付けられたプログラムで起動


runメソッドで物足りない場合は、
WshShell.execメソッドを使うと実行中のアプリケーションへのストリームを取得できる。

//WshScriptExecが取得出来るのでそこから実行ステータスやストリームを取得する。
var exeObj = shell.exec("cmd /C for %a in ('バッチ','ファイル','見辛いな') do echo %a");
//status :実行ステータス
//stdIn  :標準入力
//stdOut :標準出力
//stdErr :標準エラー


//コマンド実行終了まで待つ。
while(exeObj.status == 0){//statusが実行中の場合0
     WScript.sleep(100);
     //実行中はstdInも有効。
}

WScript.echo(exeObj.stdOut.readAll());//ストリームで使えるメソッドが使える。
WScript.echo(exeObj.stdErr.readAll());

標準出力を表示した結果…

echo offしてねぇ〜。シングルクォート付いてるし。
だらしない実行結果…。


Windows Script Hostのリファレンス
http://msdn.microsoft.com/ja-jp/library/cc364453.aspx

任意のフォルダをエクスプローラで開く方法に関しては以下のサイトを参考にさせて頂きました。
他にも実用的なコードが載っています。
http://www.happy2-island.com/vbs/

JScriptXMLファイルを処理

みんな大好きDOMを使ったXMLファイルの調理をJScriptで行おうと思います。

とりあえず、そこら辺に転がってたxmlを読ませることにします。
build.xml

<?xml version="1.0" encoding="UTF-8"?>
<project name="JScript" default="default" basedir=".">
    <description>Builds, tests, and runs the project JScript.</description>
    <import file="nbproject/build-impl.xml"/>
        <target name="run" depends="JScript-impl.jar">
            <exec dir="bin" executable="launcher.exe">
                <arg file="${dist.jar}"/>
            </exec>
        </target>
</project>
//XMLDOMパーサを取得
var dom = new ActiveXObject("MSXML2.DOMDocument");

//loadメソッドでxmlドキュメントを読み込み
dom.load("build.xml");

//あとは標準的なDOM APIを使ったプレイが楽しめる
var nodeName = dom.documentElement.firstChild.nodeName;
WScript.echo(nodeName);//description

//IXMLDOMNode#selectNodesメソッドでXPathを使用したノードリストの取得もできる。
var nodeList = dom.documentElement.selectNodes("//exec/@*");

//IXMLDOMNodeListはコレクションなので
//for inを使った処理はできない。
var iterator = new Enumerator(nodeList);
for(;!iterator.atEnd();iterator.moveNext()){
	item = iterator.item();
	WScript.echo(item.nodeName);
}//[dir],[executable]の2つのノードが取得できる。


//これでも取得可能
var len = nodeList.length;
for(var i = 0; i < len; i++){
	WScript.echo(nodeList[i].nodeName);
}

このような感じでXMLの処理を行える。

MSXMLリファレンス(英語)
http://msdn.microsoft.com/ja-jp/library/ms761386.aspx


ところでコレクションをfor inで処理できないのはやっぱ不満な感じがする。
下みたいなやつを書いてみた。

//新しくイテレータを作る方針でやってみた
var iterator = function(collection){
	return {
		forEach : function(callback){
			var len = collection.length;
			for(var i = 0; i < len; i++){
				callback(collection(i));
			}
		}
	};
};

//iterator(コレクション).forEach(コールバック);で書ける
iterator(nodeList).forEach(function(node){
	WScript.echo(node.nodeName);
});

とりあえずは、考えることが減ったので
新しくWindowがインストールされたPCが支給されたときはメモ帳を立ち上げ、
上記のコードを真っ先に書くことにするw

さて、なんか作る

なんかつくろうとしたけど、何も思い浮かばないので
ニコニコ動画のコメントデータXMLを処理しなきゃいけないことにしてみた。

処理概要:

コメントのユーザ毎にテキストファイルを作り、その中にその人のコメを書きだす。


・・・特に、意味は無い。(全くもって誰得)

ちなみに、ニコニコ動画のコメントXMLはこんな感じ。

以下、ソース

var workPath = "R:/output/";
var xmlPath = "sm5777427.xml";

var startTime = new Date();//開始時間

//標準でforEachがあればいいのに…
var iterator = function(collection){
	return {
		forEach : function(callback){
			var len = collection.length;
			for(var i = 0; i < len; i++){
				callback(collection(i));
			}
		}
	};
};

//コメントのdate値(投稿日時)を読める文字列に変換
var getDateString = (function(){
	var week = ["(日)\t","(月)\t","(火)\t","(水)\t","(木)\t","(金)\t","(土)\t"];
	return function(sec){
		var date = new Date(sec * 1000);
		return date.toLocaleDateString() +
				week[date.getDay()] +
				date.toLocaleTimeString();
	}
})();

//コメントのvpos値(動画のどこでコメしたか)を読める文字列に変換
var getVposString = function(vpos){
	var time = (vpos / 100) >> 0;
	var min = (time / 60) >> 0;
	var sec = ("0" + (time % 60));
	return min + ":" + sec.substr(sec.length - 2);
};

//使い捨てのスクリプトなのでグローバル変数を大量生産している
var fs = new ActiveXObject("Scripting.FileSystemObject");
var dom = new ActiveXObject("MSXML2.DOMDocument");

//フォルダがないなら作る
if(!fs.folderExists(workPath))fs.createFolder(workPath);

dom.load(xmlPath);

var doc = dom.documentElement;

//XPathを使ってuser_id属性ノードを根こそぎ処理する
var nodeList = doc.selectNodes("//@user_id");

//HashSet的なことがしたい…
var userSet = [];
iterator(nodeList).forEach(function(item){
	userSet[item.nodeValue] = null;
});

//ユーザーごとに処理を行う
for(var userId in userSet){

	var buf = "";
	//指定したユーザー名の属性値を持つchat(コメント)ノードのリストを得る!
	var chatList = doc.selectNodes("//chat[@user_id = '" + userId + "']");
	
	//コメントごとの処理
	iterator(chatList).forEach(function(node){
	
		var dateStr;
		var vposStr;
		//dateとvpos属性の値を処理
		iterator(node.attributes).forEach(function(attr){
			switch(attr.nodeName){
				case "date":
					dateStr = getDateString(attr.nodeValue);
					break;
				case "vpos":
					vposStr = getVposString(attr.nodeValue);
					break;
				default:
			} 
		});
		
		buf += dateStr + "\t" +
			   vposStr + "\t" + 
			   node.firstChild.nodeValue + "\r\n";
		
	});
	
	//「ユーザーID.log」で保存
	fos = fs.createTextFile(workPath + userId +".log", true);
	fos.write(buf);
	fos.close();
	
}
WScript.echo("出力完了。\r\n" + (new Date() - startTime) + "msec");

WScript.quit(0);

ユーザごとに書き込み日時、書き込み時間、コメントが出力されるはず。
実行したところ。

で、実行した結果がこれだよ!

無駄なファイルがいっぱい出来ちゃった。。。


これは酷い