perl応用編



複数ファイルを同時処理する
ここまではフォルダ内の1つのファイルを指定して処理してきましたが、実際にはたくさんのファイルを一括して処理することの方が多いでしょう。複数ファイルを一括処理する場合は、ディレクトリ関連の関数を使用します。ディレクトリを処理するには、まず、ファイルと同じようにハンドルを使い、ディレクトリを開きます。
opendir (ハンドル, ディレクトリ名)

このディレクトリの内容は、readdir関数が配列で返してくれます。
readdir ハンドル

処理終了後、ファイルと同様にディレクトリもcloseします。
close ハンドル
--
例:
$directory="data";
opendir (DIR, $directory);
@foo=readdir(DIR); #配列@fooにディレクトリの内容を格納
close (DIR);

foreach $t (@foo) {
  print $t,"\n";
}
--
これを先のテキスト整形、コンコーダンサー、リスト作成の各スクリプトに組み込めば、ディレクトリ内のファイルを一括処理できます。ただし、ディレクトリ内には見えないデータがあったり、open関数で開けないデータもあるので、注意が必要です。特に、見えないファイルは機種依存です。
この問題を避けるために、通常はforeach処理の後、「.txt」の拡張子のついたファイルだけを処理するための命令を書きます。
foreach $t (@foo) {
	if ($t !~ /\.txt$/) {next;} #拡張子が「.txt」でなければ次のループへ飛ばす
  .....
}
タグ付け
タグ付けは、基本的には「マッチ→置換」処理です。s関数をうまくつかえば、定型的なタグ付けの自動化が可能です。

テキスト外のタグ付け
これは、テキスト本体と、日付や著者などの情報を分けるものです。ここでは以下のようにタグ付けをする方法を考えます。
--
<data>
<head>
データ識別記号、日付、著者(名前、国籍、年齢、男女別等)、URL、著作権関連など
</head>
<text>
本文
</text>
</data>
--
データ全体は<data>〜</data>で囲みます。これは、1つのファイルに複数のデータを格納する際に便利です。<head>〜</head>内に本文に関する情報を書き込みます。<text>〜</text>内には本文を入れます。
この処理のためには、テキスト整形前のデータにすでに本文に関する情報が書き込んであり、かつ、プログラムがそれを判別できることが必要となります。
情報はとにかく書かないとだめですが、問題は何を基準に情報と本文を分けるか、ということになります。最善の方法は、日付など(書かれた日やデータ取得日等)を、例えば「2001/10/7」のように書いておくことです。こうすれば、このパターンを正規表現にして検出し(例:/200\d\/\d+\/\d+/)このデータまでをヘッダに、それ以降を本文に分けることができます。
このようにして出来上がったコーパスデータを処理する場合、ヘッダを読み飛ばすには<text>と</text>を目安にして処理するかどうか決定する方法が最も簡単です。

テキスト内のタグ付け
これはエラータグや統語タグ、意味素性タグなど、必要に応じて本文に書き加えるものです。このような処理の場合、別途辞書データが必要となることが多いです。また、結果も完全なものは望めません。
ただ、lemma化のためのデータが手元にある場合などは、本文中の各語ごとにそれと付けあわせし、合致しない場合はエラータグを付与することなどはできそうです。この場合、精度はデータに左右されます。
スペルミスなどをチェックするには、まず上記のスクリプトでワードリストを作成します。その後、そのリストとベースになるリストを比較します。最終的に生成させるのは、ベースリスト内の語に合致しなかった語のリストです。これらの語をスペルミスと判断し、順にテキスト内を検索してその語にタグをつける処理を行います。スペルミスの候補語リストで一度出力させれば、その時手作業で修正も可能です。
また、エラーはどのテキストにも現れるような種類のものがあります。そのようなパターンを記録しておけば、他のテキストにも応用がききます。現在そのようなデータを持っている場合は、それをリスト化しておけばスクリプトで検出ができるでしょう。
これらのやり方を用いて実用化されているものがスペルチェッカーや入力支援ソフトです。

インデックス付き大規模コーパス構築
ここまで作ってきたものは、比較的小規模なコーパスには有用ですが、1億語などという大規模コーパスを検索するには、非常に時間がかかるようになります。そのため、通常の大規模コーパスには索引がつけられています。つまり、「apple」という語は「A.txt」のnバイト目と「B.txt」のmバイト目に現れる、というような情報です。この情報があれば、プログラムは「A.txt」をオープンして直接nバイト目にアクセスに行けば、高速に検索ができるわけです。このようなファイルアクセスの方法をランダムアクセスと言います。perlにもランダムアクセスのための低レベル関数が用意されており、このようなコーパスの構築も可能です。(システムの動作と同期しているような関数を低レベルと呼んでいます。)
ただし、このコーパスは一度構築してしまうと、修正がききません。この形式のものを作るには、テキスト整形を完全に行い、コーパスデザインが確定後に行うことになります。

低レベルファイルI/O関数
read関数
構文:read(ファイルハンドル, スカラー変数, バイト数 [, オフセット])
働き:指定されたバイト数のデータをファイルハンドルから読み込んでスカラー変数に格納。バッファリングを行う。
読み込んだ内容は print スカラー変数; で表示できます。

sysread関数
構文:sysread(ファイルハンドル, スカラー変数, バイト数 [, オフセット])
働き:指定されたバイト数のデータをファイルハンドルから読み込んでスカラー変数に格納。標準のバッファリングを省略する。

syswrite関数
構文:syswrite(ファイルハンドル, スカラー変数, バイト数 [, オフセット])
働き:変数から指定されたバイト数のデータを指定されたファイルハンドルに書き込む。

seek関数
構文:seek (ファイルハンドル, オフセット, 位置)
働き:ファイルの先頭からのバイト数でファイル中の位置を指定する。位置は「0」「1」「2」のいずれか。
0 - ファイル先頭
1 - 現在位置
2 - ファイル終端
位置からオフセット(バイト数)分現在位置が移動するわけです。オフセットがマイナスの場合は先頭へ向かって移動します。

tell関数
構文:tell ファイルハンドル
働き:ファイルハンドルの現在位置を先頭からのバイト数として返す。通常ここで得られた値をseekの引数として渡す。

文字をバイナリ形式で検索
pack関数は、文字列や配列をバイナリ形式に変換します。また、unpack関数はバイナリデータをASCII形式に変換します。
以下はその例です。フォルダ「data」内のテキストファイル中に英数字以外の文字コードがある場合、それをレポートします。ない場合は何もせずに終了します。
--
#!/usr/local/bin/perl

#checkcode.pl

$read_dir="\\data\\";

opendir (DIR, $read_dir);
@foo=readdir(DIR);
close (DIR);

foreach $t (@foo) {
	if ($t !~ /\.txt$/) {next;}
	$t=$read_dir.$t;
	open (FILE, $t) || die "Can't open $t\n$!\n";

	while (<FILE>) {
		$caution=0;
		chomp;
 	$ed=length;
		for ($i=0; $i<$ed; $i++) {
			$cha=substr($_, $i, 1);
			$che=unpack("C", $cha);
			if (($che<32 || $che>126) && $che !=9) {
				$_ =~ s/([^\*]|^)$cha/$1\*$cha/;
				$i++;
				$ed++;
				$caution=1;
			}
		}
		if ($caution == 1) {
			print $t,"\n\t\t",$_,"\n";
		}
	}
close FILE;
}

exit (0);



リファレンス
perlの扱う変数は、スカラー変数、配列、連想配列の他に、それらがメモリ上に格納されているアドレス(Cで言うところのポインタ)を変数として扱えます。perlではこれをリファレンスと呼びます。また、そのアドレスの値に格納されているデータを取り出すことをデリファレンスと呼びます。

スカラー変数のリファレンスとデリファレンス
\スカラー変数  $リファレンス

例:
$msg="Hello";
$p=\$msg; #$msgに\をつけてリファレンスの値を$pに格納
print $p,"\n",$$p,"\n";
#$pにもう一つ$をつけて元の文字列に復元

出力例:
SCALAR(0x20aa5dc)
Hello

配列のリファレンスとデリファレンス
\配列  @リファレンス

例1:
@array=("You", "are", "my", "sunshine");
$p=\@array; #@arrayに\をつけてリファレンスの値を$pに格納
print $p,"\n", "@$p\n";
#$@に@をつけて元の配列の内容に復元
出力例:
ARRAY(0x20aa594)
You are my sunshine

例2:配列の個々の要素へのアクセス($$p[$i]または$p->[$i];->は矢印演算子)
@array=("You", "are", "my", "sunshine");
$p=\@array;			#@arrayに\をつけてリファレンスの値を$pに格納
for($i=0; $i<=$#$p; $i++) {	#「$#$p」の$#は配列の最後の要素の番号を取得するためのもの
	print $$p[$i],"\n";		#内容表示1
	print $p->[$i],"\n";		#内容表示2(1と同じ)
}
例3:無名配列への代入(@配列を用いず、直接リファレンスに代入して配列を作成)
$p=["You", "are", "my", "sunshine"];
print "$p\n@$p\n";

リファレンスに対して、要素を書き換えたり、pushやsortなどの関数を使うことも可能です。

連想配列のリファレンスとデリファレンス
\連想配列  %リファレンス

例1:
%var=('scalar'=>'$', 'array'=>'@', 'hash'=>'%');
$p=\%var;
foreach $key (sort keys %$p) { #keys関数でkeyの値の配列を取得
	print "$key: $p->{$key}\n";
}
例2:無名ハッシュへの代入(例1と等価)
$p={'scalar'=>'$', 'array'=>'@', 'hash'=>'%'};
foreach $key (sort keys %$p) {
	print "$key: $p->{$key}\n";
}
矢印演算子の省略
perlでは、括弧の間の矢印演算子は省略できます。

例1:矢印演算を省略していない場合
$array->[0]->[0]='omlet';
$array->[0]->[1]='egg';
$array->[1]->[0]='noodle';
$array->[1]->[1]='wheat';

print map{join("\t",@$_)."\n"} @$array;

例2:矢印演算を省略した場合(例1と等価)
$array[0][0]='omlet';
$array[0][1]='egg';
$array[1][0]='noodle';
$array[1][1]='wheat';

print map{join("\t",@$_)."\n"} @$array;

上記例2からわかるように、perlでもn次元の配列をリファレンスを用いて実現させることができます。

注)
map関数
構文:map BLOCK LIST  map EXPR,LIST
働き:LISTの各要素に対して、最初の形式の場合はBLOCKを、後の形式の場合はEXPRを評価し、その結果を返す。各要素は$_に格納されて評価される。
map EXPR,LISTの例:数値に対応する文字を返す
@nums=(80,90,100);
@chars = map(chr, @nums);
print "@chars\n";

シンボリックリファレンス
これは変数名やサブルーチン名を、変数を用いて動的に決定できる仕組みです。
例:
$a="word";
$my_word="Hello";

print ${"my_".$a},"\n";