長文テキストの「注釈」をPerlで処理するために辿った軌跡

あなたはPerlの基礎文法ぐらいなら使える編集者で、今それなりの長さを持ったテキストに、それなりの量の「注釈」を付けたいとする。

具体的には、よくわからない(と思われやすいと想定される)語句の末尾に、つど「*」というマークを付けて、それについて色々編集したいとする。

夏目漱石の「草枕」をサンプルに示すと、(via 青空文庫

 山路を登りながら、こう考えた。
 智に働けば角が立つ。情に棹させば流される。意地を通せば窮屈だ。とかくに人の世は住みにくい。
 住みにくさが高じると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟った時、詩が生れて、画が出来る。
 人の世を作ったものは神でもなければ鬼でもない。やはり向う三軒|両隣りにちらちらするただの人である。ただの人が作った人の世が住みにくいからとて、越す国はあるまい。あれば人でなしの国へ行くばかりだ。人でなしの国は人の世よりもなお住みにくかろう。

この文中のいくつかの語句に「*」を付けてみる。

 山路を登りながら、こう考えた。
 智に働けば角が立つ。情に棹*させば流される。意地を通せば窮屈*だ。とかくに人の世は住みにくい。
 住みにくさが高じると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟った時、詩*が生れて、画*が出来る。
 人の世を作ったものは神*でもなければ鬼*でもない。やはり向う三軒|両隣りにちらちらするただの人である。ただの人が作った人の世が住みにくいからとて、越す国*はあるまい。あれば人でなしの国*へ行くばかりだ。人でなしの国は人の世よりもなお住みにくかろう。

1行目を除く各行に、計8個の「*」を付けてみた。
まあ、この程度の長さなら目視でも対応できるだろうけど、これが延々と何十行、何百行も続いていくと(「草枕」は1,000行ぐらいだった)、その中に振られた注釈語句をあれこれ処理するというのは、なかなかヘヴィーである。

さしあたって、このような時に僕がやりたいことは、「で、結局どんな語句に*印を付けたんだっけ?」という疑問に答えることで、あるいは「*印を付けたのはいいけど、もしかして同じ語句に複数(重複して)付けてしまったのではないか。確認しなければ・・」みたいなことだったりする。

ということで、以下ではそのような状況で自分がトライしたことを徒然に記す。

目次

1. 砂場を作る
2. 英文でテスト
3. 日本語で再挑戦
4. <DATA>でソースコード内に素材テキストを置いた場合
5. 展望

1. 砂場を作る

とりあえずPerlで対象のテキストをいろいろいじるための場所づくりとして、以下のようなスクリプトを考えた。
この際、素材データ(今回で言うと「草枕」のテキストファイル)は、スクリプトと同ディレクトリに source.txt というファイル名で入れてあることにする。

#!/usr/bin/env perl
use strict;
use warnings;

open my $fh, '<', 'source.txt'
    or die "failed to open file: $!";
for (<$fh>) {
    if ($_ =~ /\*/) {
        print $_;
    }
}
close $fh;

結果。

 智に働けば角が立つ。情に棹*させば流される。意地を通せば窮屈*だ。とかくに人の世は住みにくい。
 住みにくさが高じると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟った時、詩*が生れて、画*が出来る。
 人の世を作ったものは神*でもなければ鬼*でもない。やはり向う三軒|両隣りにちらちらするただの人である。ただの人が作った人の世が住みにくいからとて、越す国*はあるまい。あれば人でなしの国*へ行くばかりだ。人でなしの国は人の世よりもなお住みにくかろう。

ええと、とりあえず「*」のない1行目以外は全部そのまま出てくる。これだとほとんど意味がないというか、何も処理していないに等しい。

というか、たしかに数百行もあればその中には注釈を付けていない行もあるだろうし、それらを弾けるだけでも何もしないよりはマシかもしれないが、やっぱりそれでは何もしないのとほとんど変わらず、目視でやることが膨大に残ってしまう。

つまり理想としては、上記の例で言う「棹」「窮屈」「詩」みたいな*印を付けた語句だけが、ダダダッと出てきてほしい。

ということで、今度はfor文の中の正規表現をこうしてみる。

for (<$fh>) {
    if ($_ =~ /(\w+)\*/g) {
        print $1."\n";
    }
}

\w+でキャプチャグループを作って、そこに今回注釈として設定した(*印を付けた)語句が入ってくれ〜・・という感じ。
しかしこれを実行すると、

何も出てこない。

まったくの白紙。というのはそれなりにこたえるものだが、しばらく考えて、もしかして日本語だからかな、と思いついたので、英文でテストしてみることに。

2. 英文でテスト

ここで「草枕」の英訳を使えると綺麗なのだけど、すぐに良いものが見つからなかったので、PerlのWikipediaから冒頭部分を抜き出して、適当に*印も付けて、同じ素材ファイルの中に以下を追記した。

Perl is a family of high-level, general-purpose, interpreted, dynamic programming languages. The languages in this family include Perl 5 and Perl 6.
Though Perl is not officially an acronym*, there are various backronyms in use, the most well-known being "Practical Extraction* and Reporting Language". Perl was originally developed by Larry Wall* in 1987 as a general-purpose Unix* scripting language to make report processing easier. Since then, it has undergone* many changes and revisions*. Perl 6, which began as a redesign of Perl 5 in 2000, eventually evolved into a separate language. Both languages continue to be developed independently by different development teams and liberally borrow* ideas from one another.

実行。

acronym

はい。全然期待する結果ではないが、さっきよりマシなのは1個だけ語句がマッチしていること。
推測するに、最初に正規表現にマッチした「acronym」だけが出ているのだろう。逆にというか、1行の中で複数マッチすることを狙ったg修飾子は全然効いてない。

これを見て作戦変更。とりあえず日本語のことは一旦忘れて、まずはこの英文で1行内の複数の注釈語句を拾うことを目指してみる。

で、いろいろ検索して、for文のところを以下のようにすると、

$fh = do {local $/; <$fh>};
while ($fh =~ /(\w+)\*/g ) {
    print $1."\n";
}

こんな結果になった。

acronym
Extraction
Wall
Unix
undergone
revisions
borrow

OK!

ちなみに、このwhile文の部分をこんな感じにしてもほぼ同様の結果になる。

my @array = $fh =~ /(\w+)\*/g;
print join "\n", @array;

この辺の処理については以下の記事が参考になりました。ありがとうございました。hogem.hatenablog.com
www.abe-tatsuya.com

それにしても、「Perl 正規表現 複数マッチ」などで検索してもなかなかこの方法に行き着かず、難儀した。

よく出てくるのは「m修飾子を使うと複数行を対象にマッチする」みたいな話で、それはそれで勉強になるのだけど、今回の知りたいこととはちょっと別だったので。

3. 日本語で再挑戦

さて上記を受けて、あらためて日本語の処理にチャレンジしてみる。
これまた色々検索して、以下のようにEncodeモジュールとdecode/encode関数を使えば、

#!/usr/bin/env perl
use strict;
use warnings;
use Encode qw/decode encode/;

open my $fh, '<', 'source.txt'
    or die "failed to open file: $!";
$fh = do {local $/; <$fh>};

$fh = decode('utf8', $fh);
my @array = $fh =~ /(\w+)\*/g;
$fh = join "\n", @array;
print $fh = encode('utf8', $fh);
close $fh;

こんな感じに出てくることがわかった。

情に棹
意地を通せば窮屈


人の世を作ったものは神
でもなければ鬼
越す国
あれば人でなしの国

本当にほしい感じの抽出のされ方ではないにせよ、かなりの前進と言える。
というかむしろ、「\w+」などというムチャクチャ雑なリクエストにもかかわらず、けっこう頑張ってくれたぐらいの印象。

パッと見、どうやら句読点を単語の一部にカウントしていなくて、それが区切りになっているのかなと思われる。
まあ、とりあえず今回はもうこれでいいです。

4. <DATA>でソースコード内に素材テキストを置いた場合

ところで、僕はよくDATAファイルハンドルというのを使うのですが(<DATA>というやつ)、これでやると若干挙動が変わるみたい。

具体的には、上記のようにopen関数を使った場合にはEncodeモジュールのdecode関数が必要になるけど、ソースコードと同ファイル内に素材テキストを入れる場合にはutf8プラグマというのをuseしておけばそれは不要になる。

ただし出力時にはやっぱりencode関数が必要になるっぽいので、Encodeモジュールじたいは必要なままみたい。

DATAファイルハンドルとutf8プラグマを使った場合は、こんな感じ。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode 'encode';

my $fh = do {local $/; <DATA>};
my @array = $fh =~ /(\w+)\*/g;
$fh = join "\n", @array;
print $fh = encode('utf8', $fh);

__DATA__
 山路を登りながら、こう考えた。
(以下略)

この際、encodeをせずに出力すると、

Wide character in print at (略)

とかけっこうえらい騒ぎで怒られる。
だから本当は、上記のprint文のところは英語版でやったように1行で

print join "\n", @array;

と済ませたいのだけど、encode関数を通すために2行に分けている。この辺はもっと普通にスマートな方法もあるのだろうけど・・

ちなみに、最初の方で以下とか

binmode STDOUT => ':utf8';

以下とか

binmode STDOUT => ':encoding(utf-8)';

以下を入れておけば、

use open OUT => qw/:utf8 :std/;

encode関数は不要になるみたい。(よってEncodeモジュールも不要になる)
のだけど、この辺はあんまり自分で理解できていなかったので上では触れなかった。
(冨田尚樹さんの『CPANモジュールガイド』の文字列まわりの内容を読んでいるところだが、芯から理解するにはもうちょっと手を動かす必要がありそう)

またこれらPerlエンコーディングに関する情報としては、@moznionさんが書かれた以下が非常に面白く&参考になりました。

もちろん、そこで触れられている@tokuhiromさんのこちらも。

ありがとうございました!

5. 展望

あまりこればかりやっていても本来の作業ができなくなるので、この続きをいつどの程度できるのかは自分でもわからないのだけど、とりあえず上記の出力が


窮屈




越す国
人でなしの国

みたいになるように*1(最後の2つは両方「国」でもいいが)勉強して手を入れたい。
今回もmecabとかText::Mecabとか少し使ってみたのだけど、それはそれで盛大に化けてタイムアップ、という感じになったのでまだまだ道は険しい。

あと、冒頭で「重複を弾きたい」みたいなことを書きましたが、それは以前に以下で書いた overlap.pl というのをいまだに頻用しています。note103.hateblo.jp
これもプラクティス全体としてそろそろヴァージョンアップしたい。

*1:しかしこうしてランダム抽出した単語だけ見ると、『草枕』いったい何の話なのかまったく想像がつかない。