ワードリストを作る


ここでは整形したテキストからワードリスト(index)を作成するプログラムを考えます。

語の境界
まず、文を語に分解する手法について見ていきます。英語の「語」は、スペースやカンマ、ピリオドなどの記号で区切られています。このような文字を「区切り文字」に指定し、split関数で配列に格納すると語の配列が得られます。
例:
@word = split (/ |\.|,|\"|\(|\)|\?|!/, 'This is a pen. (Is that a pen?)');
print "@word";
出力:
This is a pen   Is that a pen
--

上記の例から、最初のpenとIsの間が空いていることがわかります。これは、「. (」という区切り文字が連続したためで、「.」と「 」の間の空の部分も配列として納めているからです。したがって、これを取り除くことを考えます。そのためには配列@wordをソートし、空の部分をリストの最初に集めます。(実際にはnull文字という文字が1つ入っています)そして、前から配列を順に調べていき、空の要素を取り除きます。
例:
@word = split (/ |\.|,|\"|\(|\)|\?|!/, 'This is a pen. (Is that a pen?)');
@word = sort @word; #@wordをソート
for ($i=0; $i<=$#word; $i++) { #@wordの要素を順に確認
  if ($word[$i] eq '') { #もし$i番目の要素が空「''」(シングルクォート2個)なら
    splice (@word, $i, 1);  #その要素を削除
    $i--; #削除分カウンターを1減
  }
}
print "@word";
出力:
Is This a a is pen pen that
--

頻度計算
次に、出現数をカウントする方法を考えます。これには連想配列を利用すると比較的簡単にできます。出てきた語形をkeyにし、出現ごとにvalueの値をプラス1していけば、頻度をカウントできます。
@word = split (/ |\.|,|\"|\(|\)|\?|!/, 'This is a pen. (Is that a pen?)');
@word = sort @word;
for ($i=0; $i<=$#word; $i++) {
  if ($word[$i] eq '') {
    splice (@word, $i, 1);
    $i--;
  } else { #この後から連想配列への代入処理
    $assoc{$word[$i]}++; #%assocという連想配列のkey $word[$i]のvalueを1増
  }
}
while (($key, $value) = each %assoc) { #each関数でkeyを$keyへ、valueを$valueへ格納
  print "$key -> $\value\n";
}
出力:
This -> 1
that -> 1
a -> 2
pen -> 2
is -> 1
Is -> 1
--

小文字にそろえる
小文字(大文字)にそろえてリストを出力する場合、tr関数を使います。
以下の1文を$assoc{$word[$i]}++; の前に書けば、語はすべて小文字になります。
$word[$i] =~ tr/A-Z/a-z/;
次はその逆で、すべて大文字になります。
$word[$i] =~ tr/a-z/A-Z/;
tr関数は、正規表現の1文字ずつを対象に置き換えますので、Aはaに、Bはbに変換され、それ以外は変換されない(小文字や数字等)ので、大文字・小文字の処理には便利です。

ソート
ワードリスト作成のためには、後は連想配列のkeyとvalueを1つのデータに統合し、配列にそれを格納した後出力させます。今まで使ってきた文字列「This is a pen. (Is that a pen?)」を使った場合は以下のようになります。
--
@word = split (/ |\.|,|\"|\(|\)|\?|!/, 'This is a pen. (Is that a pen?)');
@word = sort @word;
for ($i=0; $i<=$#word; $i++) {
  if ($word[$i] eq '') {
    splice (@word, $i, 1);
    $i--;
  } else { #この後から連想配列への代入処理
    $word[$i] =~ tr/A-Z/a-z/; #大文字を小文字に変換
    $assoc{$word[$i]}++; #%assocという連想配列のkey $word[$i]のvalueを1増
  }
}
@word=(); #@wordを再利用するため1度空にする(初期化)
while (($key, $value) = each %assoc) { #each関数でkeyを$keyへ、valueを$valueへ格納
  push (@word, join(" -- ", $key,$value));
}
@word = sort @word;
foreach $yoso (@word) {
  print $yoso, "\n";
}
出力:
a -- 2
is -- 2
pen -- 2
that -- 1
this -- 1
--

リスト作成
それでは、ファイルから読み込んでワードリストを作成するプログラムを考えましょう。
まず、コンコーダンスの時と同様に、テキストを読み込みますが、その時に文を語に分解してしまい、一気に連想配列に読み込んでしまいます。$sepで区切り文字にしているのは、ここでは「空白文字(スペース、タグ、改行等)、,"()?!;:/[]」ですが、必要に応じて追加・削除して下さい。$dontは「.」を含む語が文末に来た場合の例外処理のためにあります。これも必要に応じて追加・削除・修正して下さい。
--
#!/usr/local/bin/perl

$directory="data"; 		#目的のファイルがあるフォルダの名称
$file="wowcat\.txt"; 		#ファイルの名称。「\.」としてありますが\はなくてもほぼOK
$separator="\\"; 		#パスをつなぐ記号で、機種固有値(UNIX... /  Mac... :)
$in=$directory.$separator.$file; #相対パスでファイルを指定
$out="wlist\.txt"; 		#出力ファイル

#区切り文字データ
$sep = "\\s|,|\\\"|\\(|\\)|\\?|!|;|:|\\\/|\\[|\\]";
#区切り文字例外マッチングパターン - 「.」の場合の例外処理
$dont="\[A-HJ-Z\]|M(r|s|rs)|Dr|Calif|V[Aa]|[MS][Tt]|Jan|Feb|Mar|Apr|Aug|Sep(t|)|Oct|Nov|Dec|Assoc|Co|Gov|Se(n|c)|Ont|i\\\.e|e\\\.g|v(s|)|Pa|Fla|Re(p|v)|Gen|Univ|Jr|[fF]t|[Ss]gt|[Pp]res|[Pp]rof|etc|al|[AaPp]\\\.[Mm]";

open (FILE, $in) || die "$in\n$!\n"; #ファイルオープン
while (<FILE>) { #ファイル内のデータ分繰り返し
	@word=(); #配列@wordを初期化
	$data = $_; #念のため$_を明示的に$dataに代入しています。
	$data =~ s/\.{2,}/ /g; #「...」などの表現をスペースにしています。

#以下のブロックは「.」が文末に来た場合の例外処理と文末の「.」の削除処理
	if ($data !~ /[^A-Za-z-'](($dont)\.)\s+$/){
		$data =~ s/\.\n/\n/;
	}

#ここは数字の処理 ... 22,569.233などを1語で出力
	while (1) {
		if ($data =~ /[^A-Za-z-'!\?]([\d\.,]+)[^A-Za-z-'!\?]/) { #数字列を検索したら
			push (@word, $1); #それだけ先に@wordにプッシュし、
			$data = $`." ".$'; #$dataを数字を抜いた文字列に書き換える
		} else {
			last;
		}
	}
	@array = split($sep, $data); #文字列を$sepで分割し、@arrayに格納
	foreach $yoso (@array) { #@arrayから1語ずつ@wordに格納
	  push(@word, $yoso); #これは数字の例外を処理するためだけのやや冗長な処理
	}

#以下は空白要素を削除しながら語数をカウントするブロック
	@word = sort @word;
	for ($i=0; $i<=$#word; $i++) {
	  if ($word[$i] eq '') {
	    splice (@word, $i, 1);
	    $i--;
	  } else { #この後から連想配列への代入処理
	    $word[$i] =~ tr/A-Z/a-z/; #大文字を小文字に変換
	    $assoc{$word[$i]}++; #%assocという連想配列のkey $word[$i]のvalueを1増
	  }
	}
}
close FILE; #ファイルのクローズ
@word=(); #@wordを再利用するため1度空にする(初期化)
#$totalで総語数もカウント
$total=0;
while (($key, $value) = each %assoc) { #each関数でkeyを$keyへ、valueを$valueへ格納
  push (@word, join("\t", $key,$value)); #key(TAB)valueという要素に変換して配列へ格納
  $total+=$value; #総語数計算
}
@word = sort @word; #alphabetical orderにするためのソート

open (FILE, "> $out") || die "Can't create $out\n$!\n"; #出力ファイルオープン
foreach $yoso (@word) {
  print $yoso, "\n"; #結果を出力
  print FILE $yoso, "\n"; #結果をファイルへ出力

}
print "\ntotal... $total\n"; #総合計表示
print FILE "\ntotal... $total\n"; #総合計をファイルへ出力
close FILE;

exit (0);
--

ここで出力されたリストはタブ区切りテキストですので、表計算ソフトで読み込んで操作できます。頻度順のリストもここから作ることができます。

サブルーチンでソート
perlで頻度順(数値順)のソートをするには、以下のサブルーチンが必要です。
sub numeric {$a<=>$b;}
<=>は数値用の比較演算子で、$aが大きければ1、$bが大きければ-1、同じなら0という値が返ります。sortから呼び出されるサブルーチンは、特殊な変数$aと$bに必ず変数を格納する仕組みになっていますので、上記のように1行で書けます。

例:
@num=(1,11,5,9,36,52,74,499,23,33,876,87,99,6,7,8);
@num=sort @num;
foreach $yoso (@num) {
  print $yoso, "\n";
}
print "--\n";
@num=sort numeric @num;
foreach $yoso (@num) {
  print $yoso, "\n";
}
sub numeric {$a<=>$b;}
出力:
1
11
23
33
36
499
5
52
6
7
74
8
87
876
9
99
--
1
5
6
7
8
9
11
23
33
36
52
74
87
99
499
876

--
ワードリスト作成における実際の処理では、頻度順でソートした後ASCII順でソートするのが普通なので、そのためのサブルーチンはもう少し複雑です。

数値で比較後、同じ数値だった場合にASCIIで比較するサブルーチン
これは文字列の最初に第1キーとしてソートしたい数値データがあることが必須です。(「23(tab)she」のようなデータ)
(文字列比較演算子cmpは数値の <=> と同様の働きをします)
($b <=> $a は降順を意味します。)
sub n_then_a {($b <=> $a) || ($a cmp $b);}

また、大文字・小文字が混じっていた場合、通常の表計算ソフトのソート出力のように文字の大小を無視して表示するサブルーチンも書けますが、さらに複雑です。

エクセル風文字列ソートサブルーチン
この例のように、$aと$bは別の変数に格納しないとダメです。そうでないと、tr関数が元データそのものを書き換えてしまいます。

sub my_alphabetical
{
	$aa=$a;
	$aa =~ tr/[A-Z]/[a-z]/;
	$bb=$b;
	$bb =~ tr/[A-Z]/[a-z]/;
	$cp=$aa cmp $bb;
	if ($cp == 0) {$cp=$a cmp $b;}
	return $cp;
}

数値と組み合わせるときは以下の文を$cp=$aa cmp $bb;の後を以下のように書き換えます。

$cp = $bb <=> $aa;
if ($co == 0) {
	$cp = $aa cmp $bb;
	if ($cp == 0) {$cp = $a cmp $b;}
}
return $cp;

--
以上のような処理には、perl独特の変数処理が影響します。以下の文を試してください。
$na = "12abc";
$an = "cde34";
print "$na + $an = ",$na+$an,"\n";
出力:
12abc + cde34 = 12

つまり、perlは数値演算の時、変数内に数字以外の文字列が出現後それ以降を「0」と見なしているわけです。CやC++とは格段に融通がきくと同時に相当曖昧です。

スクリプト全体...wordlist.pl