表計算シート上のURLリストに,自動でページタイトルを付与するWSHバッチ


ExcelOOo Calcの統合ライブラリ計画の続き。

いままで単一のjsファイル内でWSHを実行していたが,WSFで処理ごとにライブラリに分けた。

こうすれば,やりたいことがあるたびに部分的な更新で済む。

 

以下は,エクセルファイル上のURLリストにタイトルを付与するコード。

このバッチファイルに,エクセルをドロップする。

URLにタイトルを付与.bat

@echo off
cscript //nologo add_title.wsf "%*"
pause

同一フォルダ上に下記のファイルを置く。

 

add_title.wsf

<job>
	<script language="jscript" src="lib_excel.js" />
	<script language="jscript" src="common.js" />
	<script language="jscript" src="main.js" />
</job>


lib_excel.js

//
// MS ExcelとOOo CalcとKingsoft Spreadsheetsを
// 共通して取り扱うためのライブラリ
//
// ver0.1, 2015/1/5
//

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

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


/*

  クラス設計:

    IExcel --- IBook --- ISheet --- ICell
  
  名前空間はIExcelに集約

*/



// ---------- 表計算ソフトのラッパーオブジェクト ---------- 


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;
		}
	}
	,
	
	// 既存のブックをファイルパスで開く
	openBookByFilePath : function( file_path ){
		var ibook;
		
		if( this.isMS || this.isKS ){
			this._excel.Workbooks.Open( file_path );
			var book = this._excel.Workbooks( this._excel.Workbooks.Count );

			ibook = new IExcel.IBook( this, book );
			return ibook;
		}else{
			var doc = this._ooo_desktop.loadComponentFromURL(
				"file:///" + file_path.replace(/\\/g, "/"),
				"_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;
		}
	}
	,
	
	// 値を取得
	getValue : function(){
		if( this.type_code == "ms" || this.type_code == "ks" ){
			return this._cell.Value;
		}else{
			return this._cell.String;
		}
	}
	
	
	// TODO:他のセル操作メソッド
};


/*

サンプルコード


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

// 対象ブックを開く
var book = excel.openBookByFilePath( filepath );

// 最初のシート
var sheet = book.getSheetByIndex(1);

・・・

// ブックを保存
book.saveAs( filepath );

*/

common.js

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



// WSHの実行引数から有効なファイルを取得
function getFilePathFromArgument( wargs ){

	// 引数があるか
	if( wargs.length == 0 )
	{
		// http://d.hatena.ne.jp/language_and_engineering/20110921/p1
		
		log("同一フォルダ上のファイルをドロップしてください。");
		WScript.Quit();
	}

	// ファイルパスを構築
	var filepath = wargs.Unnamed(0);
	var fso = WScript.CreateObject("Scripting.FileSystemObject");

	// ファイルが存在するか
	if( ! fso.FileExists( filepath ) )
	{
		// http://wsh.style-mods.net/ref_filesystemobject/fileexists.htm

		log( filepath + " は無効なファイルパスです。");
		log("同一フォルダ上のファイルをドロップしてください。");
		WScript.Quit();
	}
	else
	{
		log( filepath + " は有効なファイルです。");
	}

	return filepath;
}


// IE


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


// IEがビジー状態の間待ちます
function ie_wait_while_busy( ie, _url )
{   
	var timeout_ms      = 30 * 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
				+ ")"
			);
			
			// どこかに移動中なら,そこへの移動を再試行
			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 )
}

main.js

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


// 引数取得
var filepath = getFilePathFromArgument( WScript.Arguments );


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

// ブックを開く
var book = excel.openBookByFilePath( filepath );

// シートを取得
var sheet_urls = book.getSheetByIndex(2);


// IEで全URLを調査する


// IE起動
var ie = WScript.CreateObject("InternetExplorer.Application")
ie.Visible = true;

ie.Silent = true; // BASIC認証を回避

ie_goto_url( ie, "http://www.google.co.jp/" );
log("ブラウザでのアクセスを開始します。");


// シート掲載順にアクセス
var continue_flag = true;
var y = 1;
while(continue_flag)
{
	var target_url = sheet_urls.getCell( y, 1 ).getValue();

	if( target_url )
	{

		// 開く
		log("[" + y + "] " + target_url + "を開きます");
		ie_goto_url( ie, target_url );
		
		// タイトルを抽出して記録
		var tit = ie.Document.title;
		log( "タイトルは " + tit );
		sheet_urls.getCell( y, 3 ).setValue( tit );
		
		y ++;
	}
	else
	{
		continue_flag = false;
	}
}
log("全URLのタイトル抽出が完了");


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



// ---- 終了


// ブックを保存
book.saveAs( filepath );

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

 

これでOK。

 

工夫した点:

  • たくさんのURLに自動的にアクセスするが,アクセス先がBASIC認証つきのページである場合,認証ダイアログが出るとIEの動作が停止してしまう。そこを自動で回避するように, ie.Silent = true; でサイレンとモードにした。


今後の課題:

  • ファイルを分けることで,個別の処理ごとのライブラリのバージョン管理はしやすくなった。また,コードを流用する際に,流用したい箇所をピンポイントに絞ることも可能になった。
  • しかし,個別のファイルごとに内容をコピペしてブログに貼り付けるのが面倒になったというデメリットもある。そのため,「フォルダ内の全BAT・WSF・JSをまとめて一つのブログ投稿内容にまとめるようなバッチ」があるといいなと思う。
  • エクセルのライブラリ化とともに,IE操作のライブラリ化も進めたい。
  • IEがページを開く際に,タイムアウトした場合は自動でリロードする。しかし,「IEではこのページは開けません」のようなエラー画面が出てしまうと,そこで全処理がとまる。なので,ネットワーク接続がいったん切れてしまった場合にも,再試行してエラーハンドリングできるようにIE処理を改良したい。
  • IEが開こうとするURLの中には,現在のネットワーク環境からはアクセスできないものもある。たとえば192.168〜のようなページ。こういう場合,何回かリロードしてもダメだったらあきらめて次に進むようにしたい。
  • URLごとに画面キャプチャを保存してシート上に貼り付けるようにできないか?証跡の自動保存。
  • IEがページを開いたあとで,アラートダイアログが出て固まってしまうケースがある。ロード時にページにalert関数を再注入して,アラートを無効化したい。(onloadでalertされたら困るけど。)