読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Word文書を解析して,英単語の出現回数を統計出力するバッチ (英文の用語索引を自動生成)

Word VBA 英語 WSH/JScript 正規表現 テキスト処理


文書の「単語索引」を,自動的に生成するプログラム。

  • Word文書の文章中に出現する,全ての英単語を抽出する。
  • 各英単語の出現回数をカウントし,ランキングを作成する。
  • また,各単語の出現するページ番号などを一覧表で出力する。

処理内容は,簡易なもの。

英単語の変化形などは,最低限のマージしか施していない。

(大文字・小文字は区別しないようにしてある。)


統計処理された情報を手っ取り早く得るためには役立つ。

WSH/JScriptで実装してあるので,実行も処理追加も簡単。


まず,実行結果のサンプルを掲載する。

次に,そのような処理を実行するためのバッチのソースコードを掲載する。


実行結果のサンプル

解析に使うサンプルページ:

Linuxの「grep」コマンドのマニュアル(英語)
http://linux.die.net/man/1/grep


このページの内容をWordファイルにコピペして保存。


コマンドプロンプトから,次のコマンドを実行。

cscript //nologo stat_tokens.js > out.txt

解析結果が,タブ区切りのCSVファイルとして保存される。


これをExcelに張り付けて,オートフィルタで並び替え。

1行目には適当に「単語」「出現回数」など書いておく。


出現頻度の降順に並び変えると,こんな感じ。

単語	出現回数	出現箇所個数	出現箇所
the	256	16	p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16
line	88	13	p1, p2, p3, p4, p5, p6, p7, p9, p11, p12, p13, p14, p15
and	82	16	p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16
match	75	13	p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13
file	69	9	p1, p2, p3, p4, p5, p6, p7, p13, p15
for	54	12	p1, p3, p4, p5, p8, p9, p11, p12, p13, p14, p15, p16
grep	52	13	p1, p2, p3, p4, p5, p6, p7, p8, p10, p11, p14, p15, p16
option	51	10	p1, p3, p4, p5, p6, p7, p11, p12, p13, p15
this	43	12	p1, p2, p3, p4, p5, p6, p7, p11, p12, p13, p14, p15
that	41	14	p1, p2, p3, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15
character	40	10	p2, p5, p6, p7, p8, p9, p10, p11, p14, p15
expression	39	8	p2, p7, p8, p9, p10, p11, p14, p16
default	37	11	p1, p2, p4, p6, p7, p8, p11, p12, p13, p14, p15
are	36	13	p1, p2, p3, p6, p7, p8, p9, p11, p12, p13, p14, p15, p16
regular	28	8	p2, p3, p7, p8, p9, p10, p11, p16
not	25	10	p3, p4, p6, p8, p9, p10, p11, p14, p15, p16
specified	25	7	p2, p3, p4, p11, p12, p13, p14
when	25	7	p3, p4, p11, p12, p13, p14, p15
output	24	7	p1, p3, p4, p5, p6, p7, p12
with	24	10	p2, p3, p4, p5, p7, p8, p11, p12, p14, p15
color	23	4	p3, p11, p12, p14
name	22	9	p1, p3, p4, p5, p6, p7, p9, p13, p15
matches	21	8	p2, p3, p5, p6, p7, p8, p9, p10
input	20	6	p1, p2, p3, p4, p6, p7
any	19	9	p2, p3, p4, p6, p8, p10, p11, p12, p13
pattern	19	5	p1, p2, p6, p7, p10
posix	19	6	p2, p3, p4, p11, p15, p16
text	19	7	p5, p6, p7, p11, p12, p13, p14
only	17	8	p2, p3, p4, p5, p7, p11, p13, p15
print	17	4	p1, p3, p4, p5
binary	16	3	p6, p7, p11
command	16	9	p1, p5, p6, p7, p11, p12, p13, p15, p16
locale	16	4	p8, p9, p11, p14
used	16	9	p2, p3, p5, p7, p11, p12, p13, p14, p15
context	15	5	p3, p5, p11, p12, p13
non	15	6	p2, p3, p4, p11, p12, p13
which	15	7	p2, p3, p6, p10, p11, p14, p15

...

回数が多い物は,英語の基礎単語に加えて,

この文書のメインテーマであるとみなせる。

タグクラウドみたいなものを生成するアルゴリズムだと思えばよい。


逆に,回数が少ない物に注目すれば,マイナーで難易度の高い英単語だけを重点的に暗記する助けになるかもしれない。



アルファベットの昇順に並び変えると,こんな感じ。

単語	出現回数	出現箇所個数	出現箇所
abbccdd	1	1	p8
abcd	2	1	p8
about	1	1	p4
access	1	1	p16
action	9	1	p6
active	2	1	p13
actual	2	1	p5
actually	1	1	p4
addition	3	3	p1, p9, p16
additional	1	1	p8
address	1	1	p1
adjacent	1	1	p13
advisable	1	1	p15
affect	2	2	p11, p14
after	7	2	p3, p5
afterwards	1	1	p8
against	1	1	p15
alignment	1	1	p5
all	6	5	p1, p5, p6, p7, p8
allow	2	2	p1, p11
also	9	4	p3, p4, p5, p15
alternate	1	1	p10
alternation	2	1	p10
always	1	1	p3
american	1	1	p14
analogously	1	1	p7
anchoring	1	1	p9
and	82	16	p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16
any	19	9	p2, p3, p4, p6, p8, p10, p11, p12, p13
anything	1	1	p4
anywhere	1	1	p9
appears	1	1	p15
applications	1	1	p1
applies	3	2	p8, p12
apply	1	1	p14
arbitrary	2	2	p5, p7
are	36	13	p1, p2, p3, p6, p7, p8, p9, p11, p12, p13, p14, p15, p16
arithmetic	1	1	p7
ascii	3	3	p5, p7, p9
assembling	1	1	p14
assume	2	1	p6
assuming	1	1	p11
attempts	1	1	p11
attributes	2	2	p12, p14
auto	1	1	p3
available	4	3	p1, p8, p15
avoid	3	3	p4, p10, p11
awk	1	1	p16
back	3	2	p10, p16
background	13	4	p11, p12, p13, p14
backslash	6	5	p6, p8, p9, p10, p11

...

これで,どの言葉が何ページ目に出てくるのか,一目瞭然である。

※applyとappliesみたいな変化形が統一されてないが,そういう事情は周りをサッと見ればわかるし,もし必要ならプログラムに修正を加えればよい。

ソースコード

上記のような処理を可能にするバッチのソースコード:


stat_tokens.js

/*

  Word文書を解析して,英単語の出現回数を統計出力するバッチ

*/

var cwd = WScript.CreateObject("WScript.Shell").CurrentDirectory;
var doc_file_path = cwd + "\\text.docx"; // ファイル名
var wdActiveEndPageNumber = 3;

function log(s){ WScript.Echo( s ); }
Array.prototype.includes = function( key ){
	for( var i = 0; i < this.length; i ++ )
	{
		if( this[ i ] == key ) return true;
	}
	return false;
};



// ------------- 記録オブジェクト ------------- 



var TokenRecords = function(){
	this._token_list = [];
	this._dic        = {};
};
TokenRecords.prototype = {

	// 全トークンを単語として一次元で保持
	_token_list : null,

	// 全トークンの詳細情報をハッシュで保持
	_dic : null,
	
	add : function( token, page_num )
	{
		// 出現個所情報
		var str_spot = "p" + page_num;
		
		// 登録
		if( ! this.hasToken( token ) )
		{
			// このトークンを初回登録
			this._dic[ token ] = ({
				spots : [ str_spot ],
				cnt   : 1
			});
			
			this._token_list.push( token );
			//log("[DEBUG]token = " + token + "を新規登録。");
		}
		else
		{
			// トークンの存在箇所情報を更新
			if( ! this.tokenAlreadyInSameSpot( token, str_spot ) )
			{
				//log( "[DEBUG]token = " + token + ", str_spot = " + str_spot );
				this._dic[ token ][ "spots" ].push( str_spot );
			}
			
			// トークンの出現回数情報を更新
			this._dic[ token ][ "cnt" ] ++;
				//log("[DEBUG]token = " + token + "を追加登録。");
		}
		
		//log("[DEBUG]this._token_list.length = " + this._token_list.length);
	}
	,
	
	// 特定の箇所にトークンが既に存在するか
	tokenAlreadyInSameSpot : function( token, str_spot )
	{
		var arr = this._dic[ token ][ "spots" ];
		if( ! arr )
		{
			return false;
		}
		
		if( arr.includes( str_spot ) )
		{
			return true;
		}
		
		return false;
	}
	,
	
	// トークンの存在判定
	hasToken : function( token )
	{
		return !! ( this._dic[ token ] );
	}
	,
	
	// 保持する全トークンを調整
	adjust : function()
	{
		var token = "";
		var spots = "";
		var cnt = 0;
	
		for( var i = this._token_list.length - 1; i > -1; i -- )
		{
			// 元
			token = this._token_list[ i ];
			
			// 変化形をマージ
			if( 
				(
					token.match( /^(.+)ed$/ )
					||
					token.match( /^(.+)ing$/ )
					||
					token.match( /^(.+)s$/ )
				)
				&& 
				this.hasToken( RegExp.$1 )
			)
			{
				// 登録済みの原形の方を優先
				var dest_token = RegExp.$1;
				this.merge_tokens( token, dest_token, i );
			}
/*
NOTE: この処理順だとApplesとappleが分離してしまうので,
とりあえず最初から全部小文字で登録

			else 
			// 大文字・小文字をマージ
			if( 
				( token != token.toLowerCase() )
				&&
				this.hasToken( token.toLowerCase() )
			)
			{
				// オール小文字のトークンを優先して登録する
				var dest_token = token.toLowerCase();
				this.merge_tokens( token, dest_token, i );
			}
*/
			
		}
		
	}
	,
	
	// 2つのトークン保持情報をマージする
	merge_tokens : function( token, dest_token, i )
	{
		var spots = this._dic[ token ][ "spots" ];
		var cnt   = this._dic[ token ][ "cnt" ]
		
		// 新情報を書き換え
		for( var j = 0; j < spots.length; j ++ )
		{
			if( ! this._dic[ dest_token ][ "spots" ].includes( spots[ j ] ) )
			{
				this._dic[ dest_token ][ "spots" ].push( spots[ j ] );
			}
		}
		this._dic[ dest_token ][ "spots" ].sort(function(sp1, sp2){
			sp1.match(/^p([0-9]+)$/);
			var page1 = parseInt( RegExp.$1, 10 );

			sp2.match(/^p([0-9]+)$/);
			var page2 = parseInt( RegExp.$1, 10 );

			if( page1 < page2 )
			{
				return -1;
			}
			else
			{
				return 1;
			}
				// http://crocro.com/write/manga_javascript/wiki.cgi?p=%C7%DB%CE%F3%A4%CE%A5%BD%A1%BC%A5%C8%A4%C8%CC%B5%CC%BE%B4%D8%BF%F4
		});
		this._dic[ dest_token ][ "cnt" ] += cnt;
		
		// 元情報を削除
		this._dic[ token ] = null;
		this._token_list.splice(i, 1);
			// http://javascript-memo.seesaa.net/article/24832361.html
	
	}
	,
	
	// 出力
	dump : function()
	{
		//log("[DEBUG]ダンプします。");
		//log("[DEBUG]this._token_list.length = " + this._token_list.length);
	
		var token = "";
		var str_spot = "";
		var cnt = 0;
		
		// 全トークン
		for( var i = 0; i < this._token_list.length; i ++ )
		{
			token    = this._token_list[ i ];
			str_spot = this._dic[ token ][ "spots" ].join(", ");
			cnt      = this._dic[ token ][ "cnt" ]
			
			//log( "・" + token + " (" + cnt + ") " + str_spot );
			
			// タブ区切りのCSVとして
			log( "" + token + "\t" + cnt + "\t" + this._dic[ token ][ "spots" ].length + "\t" + str_spot );
				// NOTE:出現箇所の表示上個数も出力する。
				// この情報はマージ済みの個数なので実際の出現回数ではないが,Excel上でソートするために必要。
		}
	}
};
var recorder = new TokenRecords();



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



// Wordを起動する
var word = WScript.CreateObject("Word.Application");
word.Visible = true;
	// NOTE: visibleにしておかないと,途中でエラー発生時に
	// ファイルが見えないまま開きっぱなしになり,閉じることができず後片付けが大変

// 指定したWordファイルを開く
var doc = word.Documents.Open( doc_file_path );


// 全ての段落についてループ
var num_paras = doc.Paragraphs.Count;
for( var i = 1; i <= num_paras; i ++ )
{
	// この段落を取得
	var para = doc.Paragraphs( i );

	// この段落内の文字列を取得
	var txt_in_para = para.Range.Text;
	
	// この段落に関する情報を表示
	//log( "[para " + i + "] '" + txt_in_para + "'");
	
	// この段落の終了するページ番号を取得
	var page_num = para.Range.Information(wdActiveEndPageNumber);
		// http://www.vbalab.net/vbaqa/c-board.cgi?cmd=ntr;tree=475;id=word
	
	// この段落を解析,記録
	analyzeParaText( txt_in_para, page_num );
	
}
	// http://language-and-engineering.hatenablog.jp/entry/20101105/p1


// 全記録内容の調整
recorder.adjust();

// 全記録を出力
recorder.dump();


// ファイルを閉じる
doc.Close();

// ワードを終了する
word.Quit();



// 1パラを解析する
function analyzeParaText( txt, page_num )
{
	// 事前に加工
	var txt_arr = txt

		// 余計な文字を置換
		.replace( /[\.,!\?\(\)\[\]‘''“”’\-]/g, " " )
			// NOTE: シンタックスハイライトが乱れないように'を2回入れている

		// 半角スペースで分解
		.split(" ")
	;
	
	
	// 全トークンを処理
	var token = "";
	for( var i = 0; i < txt_arr.length; i ++ )
	{
		token = txt_arr[ i ];
		
		// 有効なトークンか
		if( 
			// アルファベットだけ
			( token.match( /^[A-Za-z]+$/ ) )
			&&
			// 2文字以下は無視
			( token.length > 2 )
		)
		{
			// 全部小文字にして登録
			recorder.add( token.toLowerCase(), page_num );
		}
	}
}

突貫工事で作ったため,少々粗いソースだがご勘弁を。


発展

こういうツールがあれば,下記みたいなまとめ情報を比較的容易に作成できる。

原文をスラスラ読みたい!「MSDNライブラリによく出る英単語 100選」
http://codezine.jp/article/detail/4951

  • 単語を原形・単数形に統一 
  • 頻度順にソート 
  • 中学校で習う簡単な単語を除外 
  • 開発者であれば当然知っているであろう単語を除外