複数行のテンプレートマッチング繰り返しで,項目を一覧表形式で抽出するJSコード (改良版)

下記のコードを改良。

一般テキストからテンプレートマッチングで項目を一覧表形式で抽出するJSコード (パターンの繰り返しから正規表現で連続キャプチャ) - ソフトウェア勉強ログとサンプルコード
http://source-code-student.hatenablog.jp/entry/20150102/p2

改良点:

  • マッチ結果の配列が,二次元配列として入れ子になっている場合,一次元配列としてflattenするようにした。flattenの動作のために,reduceも追加した。
  • マッチ結果の配列要素がundefinedの場合などに,"" に置き換えて,出力されるテーブル上にundefinedなどの文字列が現れないように修正した。

 

こちらに動作デモがあります。

複数行データから項目抽出して一覧表出力するフォーム
http://sourcecode-student.info/demo/2015_01_multi_line_to_table/

<h2>テンプレートマッチングを繰り返すことによって項目を抽出して一覧表形式に出力するフォーム</h2>



解析対象のテキスト:<br>

<textarea id="ta_honbun" cols="80" rows="20"></textarea><br>
<br>


テンプレート:<br>

<textarea id="ta_template" cols="80" rows="20"
>
</textarea><br>
<br>


<input type="button" value="この本文とテンプレートで項目抽出を開始!" onclick="f()"><br>
<br>


動作ログ:<br>

<textarea id="ta_log" cols="80" rows="20"></textarea><br>
<br>


項目の抽出結果:<br>

<pre id="pre_result_table"
style="padding:40px;margin:10px;background-color:lightgray;height:300px;overflow-y:scroll;overflow-x:scroll;"
></pre>


仕様:<br>

<pre style="padding:10px;margin:10px;background-color:lightgray;">

要件:

・テキストとテンプレートを入力すると,HTMLのTABLE形式で出力される。

・解析対象のテキストに対して複数行のテンプレートを渡すと,
テンプレートとマッチングした結果の該当する中身だけを抽出してくれる。
具体的に言うと,正規表現のキャプチャカッコに相当する部分だけを抜き出す。

・解析対象のテキストに対して,テンプレートは何回転もする。
同じような情報の繰り返しから,項目を抜き出すような用途に役立つ。


テンプレートの書き方:

・テンプレート文字列は,行内で正規表現として記述する。
アスタリスクなどの記号はエスケープ漏れに注意。

・テンプレート内で,正規表現として()でくくった内容がキャプチャされる。

・テンプレート文字列は,行内で,行頭を表す^と,行末を表す$を省略して記述する。

・テンプレート文字列には,二行続けて同じ内容の行なきこと。
1行書いておけば,次の行にも流用してマッチしようとするので,ダブらせる必要が無い。

・テンプレートに (.*) を書けば,空行を許可するひとつながりの連続行を表す。
いっぽうテンプレートに (.+) を書けば,空行を許可しないひとつながりの連続行を表す。


コード規約:

・変数名の接頭辞について,hは本文,tはテンプレートのつもり。

</pre>


<style>

textarea{
	overflow-x : scroll;
}

table.capture_results td{
	padding : 3px;
	background-color : whitesmoke;
}

</style>


<script>


// メイン処理
function f(){
	
	clear_log();
	$("pre_result_table").innerHTML = "処理中です。";
	
	var arr_honbun = $("ta_honbun").value.replace(/\r\n/g, "\n").split("\n");
	var arr_tmpl = $("ta_template").value.replace(/\r\n/g, "\n").split("\n");
	
	var match_infos_all;
	
	// マッチングを実行(すごく重い処理)
	var promise = getMatchInfos( arr_honbun, arr_tmpl );
	promise.execAndPassResultTo(function( result_of_promise ){
		// 実行結果を取得して
		match_infos_all = result_of_promise;

		// マッチング情報を結果表示
		showMatchResultsAsTable( match_infos_all );
		
		log("全処理が完了しました。");
	});
}



// 配列同士を比較してマッチングを実行
function getMatchInfos( arr_honbun, arr_tmpl ){

	// いまテンプレートの何行目にいるか
	var tmpl_line_cnt = 0;
	
	// マッチ情報の全集合体
	var match_infos_all = [];
	
	// テンプレートを一回転させた分に対してのマッチ情報
	var match_infos_one_tmpl = [];
	
	// テンプレート中のマッチが発生した行の番号
	var prev_matched_tmpl_line_cnt = -1;
	
	// 本文の全行に対して
	var promise = arr_honbun.each_with_heavy_operation(function( h_line, h_line_cnt ){

		// この中の処理はすごく重い。1400行ぐらいでブラウザの応用が停止の警告が出る。
		// なので,ここから先は同期的に実行せず,うまく非同期化する。

		var t_line;
		var matching_success = false;
		var testing_t_line_cnt;
		var m;
		
		log(
			"本文の" 
				+ h_line_cnt
				+ "行目を検査:"
				+ h_line
		);
		
		// テンプレートはまず一歩先でマッチングしてみる
		if( tmpl_line_cnt + 1 < arr_tmpl.length ){
			testing_t_line_cnt = tmpl_line_cnt + 1;
		}else{
			// テンプレートの最終行まで来たら,最初の行に戻ってみる
			testing_t_line_cnt = 0;
		}
		t_line = arr_tmpl[ testing_t_line_cnt ];
			log("テンプレートは一歩先でマッチングにトライ:" + t_line)
			
		m = match_honbun_and_template( h_line, t_line );
		if( m ){
			log( "マッチ成功:" + m.join(",") );
			
			// テンプレートが一回りした場合
			if( testing_t_line_cnt == 0 ){
				// ここまでを記録してから精算する
				match_infos_all.push( match_infos_one_tmpl );
				match_infos_one_tmpl = [];
			}

			// 今回のマッチ情報を保管(カッコによるキャプチャ情報がある場合のみ)
			if( m.length > 1 ){
				log("キャプチャ情報を格納");
				m.shift();
				match_infos_one_tmpl = match_infos_one_tmpl.concat( m.flatten().blanksAsString() );
			}
			
			// テンプレートの利用行を更新する
			tmpl_line_cnt = testing_t_line_cnt;
			matching_success = true;
			prev_matched_tmpl_line_cnt = tmpl_line_cnt;
			
			log();
			return;
		}else{
			log("マッチせず");
		}
		
		// この時点でまだマッチングが成功していなければ
		if( ! matching_success ){
			t_line = arr_tmpl[ tmpl_line_cnt ];
				log("テンプレートは進まずにマッチングにトライ:" + t_line)
		
			m = match_honbun_and_template( h_line, t_line );
			if( m ){
				log( "マッチ成功:" + m.join(",") );
				
				// 今回のマッチ情報を保管(カッコによるキャプチャ情報がある場合のみ)
				if( m.length > 1 ){

					log("キャプチャ情報を格納");
					
					// SPEC: 前回と同じ行をテンプレートとして利用した場合は,キャプチャ結果をマージする。
					if( prev_matched_tmpl_line_cnt == tmpl_line_cnt ){
						m.shift();
						
						// 末尾の要素に
						match_infos_one_tmpl[ match_infos_one_tmpl.length - 1 ] 
							+= (
								"\n" 
								+ m.flatten().join("") 
							)// マージする
						;
					}else{
						// ふつうにキャプチャ結果を記録
						m.shift();
						match_infos_one_tmpl = match_infos_one_tmpl.concat( m.flatten().blanksAsString() );
					}
					
				}
				matching_success = true;
				
				prev_matched_tmpl_line_cnt = tmpl_line_cnt;
				
				log();
				return;
			}else{
				// マッチしなかったらテンプレートに誤りがある
				log("テンプレートが不正です。動作停止");
				throw("wrong template");
			}
		}
		
	}).afterThat(function(){ // 重い処理が終わったら・・・
	
		// さいご余った分を記録
		match_infos_all.push( match_infos_one_tmpl );
		
		log("マッチングによるキャプチャ部分の項目抽出が完了");
		
		return match_infos_all;
	});
	
	// 実行予定だけを返す
	return promise;

}


// 本文とテンプレートを1行だけマッチ
function match_honbun_and_template( line_honbun, line_template ){
	return line_honbun.match( new RegExp( "^" + line_template + "$", "" ) );
		// NOTE: マッチングオプションにgを含めないことがポイント。
		// gがあると部分文字列が返ってこない。
		// http://d.hatena.ne.jp/chalcedony_htn/20090315/1237121111
}


// -------- Promise動作


// 配列の各行に対してすごく重い処理をしたい
Array.prototype.each_with_heavy_operation = function( func ){
	// promiseを数珠つなぎにする
	var promises = this.map(function( item, index ){
		return new Promise( func, [ item, index ] );
	});
	for( var i = 1; i < this.length; i ++ ){
		promises[ i - 1 ].setNextStep( promises[ i ] );
	}
	
	// 末尾のpromise要素を返す
	return promises[ promises.length - 1 ];
};


// 動作の約束オブジェクト
var Promise = function( func, args, params ){
	log( "promise生成" );
	
	this._func = func;
	this._args = args;
	
	if( params ){
		if( params.get_param_from_prev_step ){
			log("このpromiseは実行時に直前のpromiseから値を受け取ります。");
			this.get_param_from_prev_step = true;
		}
	}
}
Promise.prototype = {
	_func : null,
	_args : null,
	
	_prev_step : null,
	_next_step : null,
	
	get_param_from_prev_step : false,
	
	// 次の動作を決めておく
	setNextStep : function( p ){
		this._next_step = p;
		p._prev_step = this;
		
		return this;
	},
	
	// 次の動作をクロージャで指定
	afterThat : function( func ){
		var p = new Promise( func, [] );
		this.setNextStep( p );
		
		return p;
	},
	
	// ここまでのプロミスチェーンを全部実行して,
	// 返り値を次の動作に渡す
	execAndPassResultTo : function( next_func ){
		var p = this;
		var p_arr = [];
		
		// 前の動作へたどってゆく
		while( p != null ){
			p_arr.push( p );
			p = p._prev_step;
		}
		
		// 時系列に並び替える
		p_arr = p_arr.reverse();
		
		// 最後に付与
		var p_last = new Promise( 
			next_func, 
			[], 
			{
				// 直前の返り値を実行引数とする
				get_param_from_prev_step : true 
			} 
		);
		p_arr[ p_arr.length - 1 ].setNextStep( p_last );
		
		// 先頭のプロミスを取得して実行
		p_arr[0].execPromise();
	},
	
	// このプロミスを実行
	execPromise : function( arg ){
		
		// 前回の結果を引き継ぐか?
		if( this.get_param_from_prev_step ){
			// 引数として,前回の返り値を優先する。
			this._args = [ arg ];
			log("前回の結果を引き継ぐために,引数を更新しました。: " + this._args );
			log("更新済みの引数の個数:" + this._args.length );
		}
	
		// 動作を実行
		log("promise実行します・・・");
		var result = this._func.apply( this, this._args );
			//log("promise実行しました。実行結果:" + result );
		
		var next_p = this._next_step;
		if( next_p ){
			// 非同期に次へ移る
			setTimeout( function(){
				next_p.execPromise( result )
			}, 0 );
		}
	}
};


// ------ etc


// マッチ情報をテーブル形式で一覧表示
function showMatchResultsAsTable( match_infos_all ){
	
	log("結果表示:" + match_infos_all.length + "行のテーブル");
	
	// 全行
	var trs = match_infos_all.map(function( match_infos_one_tmpl, index ){
		
		log(
			"結果の連結中:テーブルの" 
				+ index 
				+ "行目は"
				+ match_infos_one_tmpl.length 
				+ "項目"
		);
		
		// 全項目
		var tds = match_infos_one_tmpl.map(function(item, index){

			// FIXED: 2015/01/10
			if( ! item ){
				item = ""; // undefinedの場合もあるから
			}
			
			return (
				"<td><pre>"
					+ ( "" + item )
						.replace(/>/g, "&gt;") // タグはソースとして表示する
						.replace(/</g, "&lt;")
						.replace(/^\n+/g, "") // 最初と最後の空改行は削除する
						.replace(/\n+$/g, "")
					+ "</pre></td>"
			);
		}).join("");
			// Array#mapと#joinの組み合わせサイコー!
		
		return "<tr>" + tds + "</tr>";
		
	}).join("\n");
	
	var table_html = "<table border=1 class='capture_results'><tbody>" + trs + "</tbody></table>";
	
	// 表示
	$( "pre_result_table" ).innerHTML = table_html;
}


// 配列のイテレータ
Array.prototype.each = function( func ){
	for( var i = 0; i < this.length; i ++ ){
		func.call( this, this[i], i ); 
	}
	return this; // チェインを継続
};

// map
Array.prototype.map = function( func ){
	var _arr = [];
	this.each(function( item, ind ){
		_arr.push( func.call( _arr, item, ind ) );
	});
	return _arr;
};


// FIXED: flattenを追加。マッチ結果が二次元配列になりうるので。

// 多次元配列を1次元にならす関数。
// 内部でreduceを使用
Array.prototype.flatten = function(){
	return this.reduce(
		function( result, item ){
	    	return (
	    		//Array.isArray( item ) // WSHや古いIEでは動かない
	    		( item instanceof Array )
	    			// 対象要素が配列ならば,再帰する
	    			? result.concat( item.flatten() ) 

	    			// 対象要素が配列でなければ,要素として採用
	    			: result.concat( item )
	    	);
		},
		
		// 空配列からはじめる
		[]
	);
};
	// http://d.hatena.ne.jp/TipsMemo+computer-technology/20150110/p1


// FIXED: 無効な値は空文字列に変換
Array.prototype.blanksAsString = function(){
	return this.map(function(item){
		if( ! item ){
			item = "";
		}
		return item;
	})
};


function $( dom_id ){
	return document.getElementById( dom_id );
}


function log( s ){
	if( ! s ) s = "";
	$("ta_log").value = ("" + s) + "\n" + $("ta_log").value;
}

function clear_log(){
	$("ta_log").value = "";
}



</script>

reduceとflattenについて:

JavaScriptの配列便利メソッドArray#reduceを,WSH/JScriptで使えるようにする方法 (flattenメソッドつき) - プログラミングとIT技術をコツコツ勉強するブログ
http://d.hatena.ne.jp/TipsMemo+computer-technology/20150110/p1

今後の課題:

  • 一般公開可能な,わかりやすい利用サンプルデータが必要。
  • 「行ごとの繰り返しを許可するような,行単位の正規表現」が,いまいち書きづらい気がする。どんな既存手法があるか調査したい。