はてなカウンターのリンク元情報をOpenOffice Calcに一括記録するWSHバッチ(ExcelとOOoを統一して扱えるライブラリの試作)


はてなカウンターから,1年分のリンク元情報を取得してCalcのシート上に記録するバッチ。 ====

@if(0)==(0) ECHO OFF

echo 処理開始時刻: %date% %time% > log.txt

rem WSHコードに,第一引数としてカレントフォルダを渡す
cscript.exe //nologo //E:JScript "%~f0" "%~dp0"
rem cscript.exe //nologo //E:JScript "%~f0" "%~dp0" > log.txt

rem 環境変数から「パス名」(フォルダ名)を取り出すには %~pX とする。
rem http://orangeclover.hatenablog.com/entry/20101004/1286120668


echo 終了しました。

echo 処理終了時刻: %date% %time% >> log.txt
@pause

GOTO :EOF
@end


/*

	はてなカウンターから,対象年内のリンク元URLを抽出してExcelに記録するバッチ
	
	・該当はてなIDにログイン済みであること
	・バッチ内の設定事項を調整すること

*/



// ---- 設定事項


// 自分のはてな情報
var hatena_id = "〜〜〜〜〜〜";
var counter_id = "1";

// 対象年
var target_year = "2014";

// はてなカウンターの基本的なURL。
// 表示情報のページングに関する情報は除外してある
var counter_url_base = "http://counter.hatena.ne.jp/" 
	+ hatena_id
	+ "/report?cid="
	+ counter_id
	+ "&date="
	+ target_year
	+ "-12-01&mode=summary&target=link&type=yearly&"
;
	// 実際にはこの後ろに page=4 などが付与される


// 情報を記録するExcelファイル名
var xls_filename = "link_report_"
	+ hatena_id
	+ "_"
	+ counter_id
	+ "_"
	+ target_year
	+ ".odt"
;

// このバッチでページングを行なう際の最高ページ,リミット
var max_page = 1000;

// リンク元からアクセスされた回数の下限として認める範囲
var min_cnt = 5;

// 1ページあたりに表示されるリンクの上限
var links_num_in_page = 50;

// ページングの開始ページ
var first_page_num = 1; // 通常は1からだが,実験用に変更することも可能



function log(s){ WScript.Echo(s); }





// ---- IEではてなカウンターから情報を抽出


// IEがビジー状態の間待ちます
function ie_wait_while_busy( ie, _url )
{   
	var timeout_ms      = 45 * 1000;
	var step_ms         = 100;
	var total_waited_ms = 0;
	
	while( ( ie.Busy ) || ( ie.readystate != 4 ) )
	{
		WScript.Sleep( step_ms );
		
		// タイムアウトか?
		total_waited_ms += step_ms;
		if( total_waited_ms >= timeout_ms )
		{
			log(
				"警告:タイムアウトのため,リロードします。("
				+ ie.LocationURL
					// http://blog.livedoor.jp/programlog/archives/298228.html
				+ ")"
			);
			
			// どこかに移動中なら,そこへの移動を再試行
			if( _url )
			{
				log( _url + "への遷移を再試行");
				ie_goto_url( ie, _url );
			}
			else
			{
				log( "リロード中");
				
				// 移動先が明示されていなければリロード
				ie.document.location.reload( true );
				ie_wait_while_busy( ie );
			}
			
			break;
		}
	}

	WScript.Sleep( 1000 )
}
	// http://d.hatena.ne.jp/language_and_engineering/20100310/p1
	// http://d.hatena.ne.jp/language_and_engineering/20100403/p1


// ページを移動
function ie_goto_url( ie, url ){
	ie.Navigate( url );
	ie_wait_while_busy( ie, url );
}





// ---- MS ExcelとOOo Calcを共通して取り扱うためのライブラリ ----


// 要件:
// Excelで組んだロジックをOOcで使いまわし,その逆も可としたい。

// 設計方針:
// Decorator・factoryパターンあたりを参考に,
// オフィス製品の差異を内部にコンポジットで隠蔽して切り替え,外側のAPIは統一する。


// 表計算ソフトのラッパーオブジェクト
var IExcel = function(){
	// 初期化
	this.defineExcelType();
};
IExcel.prototype = {

	// 内部で使うオフィス製品のタイプ
	isMS : false,
	isKS : false,
	isOO : false,
	type_code : null,
	
	// どれを使うか調査して決める
	defineExcelType : function(){
		try{

			// MS製のオフィスがインストールされていれば最優先する
			this._excel = WScript.CreateObject("Excel.Application");
				// http://d.hatena.ne.jp/language_and_engineering/20140214/p1

			this.isMS = true;
			this.type_code = "ms";

		}catch(e){ // MSが無かったら

			try{

				// KingSoftがあれば,Excelと同一のAPIなのでこれを使う
				this._excel = WScript.CreateObject("ET.Application");
					// http://d.hatena.ne.jp/language_and_engineering/20121218/p1
				
				this.isKS = true;
				this.type_code = "ks";

			}catch(e2){ // Kingsoftも無かったら

				try{
					// 最後の手段として,OpenOffice.org Calcを使う
					var service_manager = WScript.CreateObject("com.sun.star.ServiceManager");
					this._ooo_desktop = service_manager.createInstance("com.sun.star.frame.Desktop")
						// http://d.hatena.ne.jp/language_and_engineering/20141227/OOoCalcByWSHJScript
					
					this.isOO = true;
					this.type_code = "oo";
					
				}catch(e3){
					WScript.Echo("オフィス製品を何か一つインストールしてください。");
				}

			}

		}
		
		this._books = [];
		
		// 初期化完了
		return;
	}
	,
	
	_excel : null,
	_ooo_desktop : null,
	
	
	// Visible
	setVisible : function( b ){
		if( this.isMS || this.isKS ){
			this._excel.Visible = b;
		}else{
			// TODO:
		}
	}
	,
	
	// 新規ブックを開く
	getNewBook : function(){
		var ibook;

		if( this.isMS || this.isKS ){
			this._excel.Workbooks.Add();
			var book = this._excel.Workbooks( this._excel.Workbooks.Count );
			
			// インタフェースに変換
			ibook = new IExcel.IBook( this, book );
			
			return ibook;
		}else{
			var doc = this._ooo_desktop.loadComponentFromURL(
				"private:factory/scalc", 
				"_blank", 
				0, 
				[]
			);
			
			// インタフェースに変換
			ibook = new IExcel.IBook( this, doc );
			
			return ibook;
		}
	}

};


// ブックを表すラッパオブジェクト

IExcel.IBook = function( parent, real_book ){
	this._parent = parent;
	this._book = real_book;
	this.type_code = parent.type_code;
	this._sheets = [];
};
IExcel.IBook.prototype = {
	_parent : null,
	_book : null,
	type_code : null,
	
	// ファイルパスを指定して保存
	saveAs : function( file_path ){
		if( this.type_code == "ms" || this.type_code == "ks" ){
			this._parent.DisplayAlerts = false;
			this._book.SaveAs( file_path );
		}else{
			var file_url = "file:///" 
				+ file_path.replace(/\\/g, "/")
			;
			this._book.storeAsURL( file_url, [] );
		}
	},
	
	// 番号でシートを取得(1始まり)
	getSheetByIndex : function( index ){
			log("現在のシートの個数:" + this.getSheetsCount() );
			log("index: " + index);
	
		var sheet, isheet;
		if( this.type_code == "ms" || this.type_code == "ks" ){
			sheet = this._book.Worksheets( index );
			isheet = new IExcel.ISheet( this._parent, sheet );
			return isheet;
		}else{
			sheet = this._book.Sheets.getByIndex( index - 1 ); // 0始まり
				// http://blog.livedoor.jp/addinbox/archives/51243622.html
				// http://itref.fc2web.com/openoffice/basic/calc.html

				//sheet = this._book.Sheets( index - 1 ); // 0始まり
				//REM: これだとシートがうまく取れなかった
				
				log( "取得したシートの名称は" + sheet.Name );
				
			isheet = new IExcel.ISheet( this._parent, sheet );
			return isheet;
		}
	}
	,
	
	// シートの個数
	getSheetsCount : function(){
		if( this.type_code == "ms" || this.type_code == "ks" ){
			return this._book.Sheets.Count;
		}else{
			return this._book.Sheets.getCount();
		}
	}
	
	// TODO:他のブック操作メソッド

};


// シートを表すラッパ

IExcel.ISheet = function( parent, real_sheet ){
	this._parent = parent;
	this._sheet = real_sheet;
	this.type_code = parent.type_code;
};
IExcel.ISheet.prototype = {
	_parent : null,
	_sheet : null,
	type_code : null,
	
	// セル参照(番号は一始まり)
	getCell : function( y, x ){
			log( y + "行" + x + "列目に書き込み"  );

		if( this.type_code == "ms" || this.type_code == "ks" ){
			var cell = this._sheet.Cells( y, x ); // 1始まり

			var icell = new IExcel.ICell( this._parent, cell );
			return icell;
		}else{
			var cell = this._sheet.getCellByPosition( x - 1, y - 1 ); // 0始まりでMSと逆

			var icell = new IExcel.ICell( this._parent, cell );
			return icell;
		}
	}
	
	// TODO:他のシート操作メソッド

};


// セルのラッパ

IExcel.ICell = function( parent, real_cell ){
	this._parent = parent;
	this._cell = real_cell;
	this.type_code = parent.type_code;
};
IExcel.ICell.prototype = {
	_parent : null,
	_cell : null,
	type_code : null,
	
	// 値を書き込み
	setValue : function( v ){
		if( this.type_code == "ms" || this.type_code == "ks" ){
			this._cell.Value = v;
		}else{
			this._cell.String = v;
		}
	}
	
	// TODO:他のセル操作メソッド
};








// ------------------------ メイン処理

// ---- カレントフォルダにExcelを新規生成


var curr_dir = WScript.Arguments.Unnamed(0);
var file_path = curr_dir + xls_filename;
var fso = WScript.CreateObject("Scripting.FileSystemObject");

// ファイルが存在するか
if( fso.FileExists( file_path ) )
{
	log( "既にファイルが存在します。実行停止");
	WScript.Quit();
}
else
{
	log( "記録対象:" + file_path );
}



// Excel起動
var excel = new IExcel();
excel.setVisible( true );


// 新規ブック
var book = excel.getNewBook();


// 新規ブックを保存
book.saveAs( file_path );

log("とりあえずブックを保存しました");

// 先頭のシートを情報の記録場所とする
var sheet = book.getSheetByIndex(1);







// IE起動
var ie = WScript.CreateObject("InternetExplorer.Application")
ie.Visible = true;
ie_goto_url( ie, "http://www.google.co.jp/" );
log("ブラウザでのアクセスを開始します。");


// ページが存在する限り抽出を続行
var page_num = first_page_num;
var continue_flag = true;
while( continue_flag )
{
	var target_url = counter_url_base 
		+ "page="
		+ page_num
	;

	// IEで開く
	log("[" + page_num + " ページ目] " + target_url + " を開きます");
	ie_goto_url( ie, target_url );
	
	// tableを取得
	var table = ie.document
		.getElementById("hourlyreport")
		.getElementsByTagName("table")[0]
	;
	var trs = table.getElementsByTagName("tr");
	
	// trが51行あるので情報抽出。先頭のタイトル行はスキップ
	for( var i = 1; i < links_num_in_page + 1; i ++ )
	{
		// 行があるか?
		var tr = trs[ i ];
		if( tr )
		{
			var y = ( page_num - 1 ) * links_num_in_page + i;
			log( y + "番目の情報を抽出");
		
			var tds = tr.getElementsByTagName("td");
			
			// URLを認識
			var elem_as = tds[0].getElementsByTagName("a");
			if( elem_as.length > 0 )
			{
				var elem_a = elem_as[0];
				
				//var link_url = elem_a.getAttribute("href"); // エラーになる場合がある
				//var link_url = elem_a.href; // エラーになる場合がある
				
				var link_url = elem_a.getAttribute("href", 2);
					// http://might1976.doorblog.jp/archives/51159843.html
			}
			else
			{
				// 「不明」などの文言の場合もある
				var link_url = tds[0].innerText;
			}

			// アクセス回数
			var cnt = parseInt( tds[1].innerText.replace( /,/g, "" ), 10);
			if( cnt < min_cnt )
			{
				continue_flag = false;
				log( "アクセス回数が下限に達したので抽出を終了" );
			}
			else
			{
				log( i + " 行目からリンクを抽出:「" + link_url + "」, " + cnt );
				
				// 書き込み
				sheet.getCell( y, 1 ).setValue( link_url );
				sheet.getCell( y, 2 ).setValue( cnt );
			}
		}
		else
		{
			// 行が途切れたらそこで終わり
			continue_flag = false;
			
			// ページが終わる場合もtable自体と先頭行は表示され,
			// 下部に「アクセスが記録されておりませんでした。」と出る。
		}
	}
	
	
	// 次のページへ
	page_num ++;
	if( page_num > max_page )
	{
		continue_flag = false;
	}
}

log("全ページのリンク抽出が完了");


// IEの制御を破棄
ie.Quit();
ie = null;



// ---- 終了


// ブックを保存
excel.DisplayAlerts = false;
book.saveAs( file_path );
log( "ブックを保存しました。" );


// Excelを閉じて終了
//excel.Quit();
//excel = null;

log("全処理が終了");

今後の課題:

  • Excelの統一APIライブラリに,シートの作成などの関数を追加してゆく。
  • カウンターから抜き出した情報に対して,URLのフィルタをかけて,意味のあるURLだけを残し,それらにタイトルを付すような処理を追加したい。
  • ライブラリの種別ごとにjsファイルに整理して,wsfで実行するようにしたい。