モダンPerlの世界へようこそ

第31回encoding:いつまでもjperlから抜け出せない方に

いまさら使う人はいないと思っていますが

かつて、jperlと呼ばれるものがありました。これは当時まだシングルバイト文字にしか対応していなかったPerl本体にパッチをあてて日本語(など)の2バイト文字をより直感的に扱えるようにしたもので、いまとなっては史料的価値しかありませんが、1990年代にはそれなりに重宝されていましたから、筆者を含めて、お世話になったことのある方も少なからずいることでしょう。

jperlはその後、ライブラリレベルで日本語対応できるようにしたjcode.pl(1992年)や、その流れをくむJcode.pm(1999年)を経て、2000年にリリースされたPerl 5.6からは本家のほうでUnicode対応が始まったことで、その歴史的役割を終え、開発も事実上終了したのですが、困ったことに、それから10年がたったいまなお、jperlを求めたり、勧めたりする動きはやまないようです。

さまざまな制約をわかった上でどうしても、という方を留め立てするつもりはありませんが、jperlでできることのほとんどはもうPerlに標準装備されています。いまさらjperlを使うのは、茨の道を行くことにしかなりません。

今回からは、古いjperlユーザや、jperlという名前から連想されるなにかに惑わされてしまった方へのガイドを兼ねて、Perlで日本語を扱う際の基本を紹介していきます。今回は歴史的経緯を考慮してもっぱらMS-DOS/Windows環境での例をとりあげますが、ほかの環境でも大筋は同じですので、適宜読み替えていってください。

なにが問題だったのか

まずはPerlで日本語を扱う場合になにが問題となっていたのかを簡単におさらいしておきましょう。お手元に日本語Windows環境がある方は、プロンプトからこのようなワンライナーを実行してみてください。特殊な設定をしていなければAlt+半角/全角キーでコマンドプロンプトから日本語を入力できますが、面倒な方はブラウザでコピーしたものをコンソールにペーストしてもよいでしょう。

> perl -e "print qq/あ/"

コンソールには正しく「あ」という文字が表示されました。いまのPerlは英数字だけでなく、任意のバイナリを扱えますので、日本語の出力くらいはお手の物です。

では、このような例ならどうでしょうか。

> perl -e "print qq/ソ/"

今度は「Can't find string terminator "/" anywhere before EOF at -e line 1.」というエラーが出て処理が中断されてしまいました。

Windows環境で日本語を扱うプログラムを書いたことがある方ならご存じかと思いますが、これは俗に「5C問題」といわれるものです。一般的な日本語Windows環境では「ソ」という文字は16進数表記で「835C」という文字コードであらわされるのですが、後半の「5C」の部分が「\」と同じ文字コードになるため、標準的なPerlではあたかも「qq//」の2つめのスラッシュがエスケープされていたかのように扱われてしまったのでした。

もうひとつ例を見ておきましょう。今度も同じくワンライナーで試してみます。

> perl -e "my $str = qq/AB/; $str =~ tr/A/B/; print $str"
BB

一見うまく動作しているようですが、AとBの間に半角の「`」⁠バッククォート)を挟むと、なぜかそのバッククォートまで置換されてしまいます。

> perl -e "my $str = qq/A`B/; $str =~ tr/A/B/; print $str"
BaB

こちらはtrが正しく日本語のコンテキストを理解しているわけではなく、実際にはこのようなワンライナーと等価のものになっているために起こる問題でした(この例の場合、実際に置換されているのは\x60と\x61の部分だけになります⁠⁠。

> perl -e "my $str = qq/\x82\x60\x60\x82\x61/; $str =~ tr/\x82\x60/\x82\x61/; print $str"

同じような問題は、末尾の文字を削除するchop()や、文字列の長さを返すlength()、部分文字列を返すsubstr()などでも起こりえます。たとえば、次の例はコンソールでは一見正しく動作しているように見えますが、⁠B」の1バイト目が残っているので、続けてなにかを出力しようとすると、その出力の最初の1バイト目が残っていた「B」の1バイト目と結びついて文字化けを起こします。

> perl -e "my $str = qq/AB/; chop $str; print $str"

jperlがもたらしたもの

Perl 4.0のリリース(1991年)にあわせて公開されたjperlは、Perl本体の文字列処理に2バイト文字かどうかを判定するコードを追加することで部分的にこの問題を解決するものでした。CPANには当時すでにjgawk(Japanized Gnu Awk)などの作者として知られていたserowこと田中良知氏によるPerl 4.019へのパッチと4.036へのパッチが残っていますが、そのパッチに同梱されているreadme.sjによると、jperlでは以下のような漢字対応が行われていたことがわかります。

  • 文字列中のShift-JISの漢字の2バイト目はメタキャラクタと衝突しない
  • 正規表現やchop()では2バイト文字も1文字として扱われる
  • formatされた文字が漢字境界で切れてしまう場合は半角スペースを追加して揃える
  • ファイルテスト演算子のうち、-Tと-Bでは2バイト文字を正しく判定できる

jperlを使えば、先ほどあげた例はすべて期待通りの動作をするようになります。2バイト文字の2バイト目にたまたまエスケープ記号と同じコードが使われていてもエスケープ扱いされることはありませんし、2バイト文字が泣き別れることもありません。

jperlの制約

ただし、田中氏のパッチではすべてが2バイト文字対応されていたわけではありません。readme.sjには以下のような制約が残っていることも明記されています。

  • tokenに漢字を使うことはできない
  • 文字列の大小関係はCのライブラリ関数の実装に依存
  • substrやreverseは漢字を意識しない
  • index、rindexの返す値は(文字位置ではなく)バイト位置

substrやindexについては対応できなかったというより、すべてを漢字対応にしてしまうと(たまたま漢字と同じバイト列が含まれているような)バイナリデータを扱うときや、全角半角入り混じった文字列の長さを考慮した処理をしたいときに困るので、あえてオリジナルの挙動を残しておいたと見るべきでしょうが、⁠jperlを使えばどんな書き方をしても正しく日本語が処理される」というのは幻想にすぎません。jperlを使おうと、こう書いてしまえば文字化けは起きます(上のchopの例と同じく、一見正しく動作しているように見えますが、次の出力があったときに文字化けが起こります⁠⁠。

> jperl -e "my $str = qq/AB/; print substr($str, 0, 1)"

このように書けば期待通り?の結果は得られますが、手間ですし、効率もよくありません。

> jperl -e "my $str = qq/AB/; print ((split //, $str)[0..0])"

また、jperlが対応しているのはあくまでも日本語Shift-JISと日本語EUCだけです。オプションで中国語(台湾)TCAや韓国語EUC(KS C5601-1987)にも対応できるようになってはいますが、UTF-8をはじめとするUnicodeにはいっさい対応していません。

jperlはPerl 5.5時代に更新終了となったため[1]⁠、最近のいわゆるモダンなモジュールはまず動かない、という問題もあります。そもそも野良で配布されているWindows向けバイナリディストリビューションにはCPAN.pmをはじめとする標準モジュールが含まれていないのですが、連載第1回でも紹介したように、ExtUtils::MakeMakerをはじめとする基本的なツールがどんどんPerl 5.5系列のサポートを打ち切るようになってきました。jperlではもうLWP系のモジュールを使ったネットアクセスも、DBIを使ったデータベースアクセスも、簡単にはできません。巷ではモジュールの供給先として古いActivePerlを同時にインストールすればよい、という話もされてきましたが、ActiveState社もモダンPerlの流れに乗って古いバージョンの公開ポリシーを変え、数百ドルもする商用ライセンスを購入しないと古いソース、バイナリにはアクセスできないようになりました。新しくjperlをインストールするのはますます難しくなってきています。

ほかにも、たとえばウェブアプリケーションの場合は外部のサーバ環境にインストールしたときでも同じように動作してくれないと困るので、日本語を特別扱いしてくれるjperlを使うより、Unix系の環境にはたいてい標準でインストールされている素のPerlを使うつもりでコードを書いたほうが移植性が高くなるとか、容量の限られたフロッピーディスクにすべてを押し込む必要があった昔と違って、いまはUSBメモリにせよなんにせよ、容量的にははるかに余裕があるので、バイナリだけ持ち歩くより、連載第18回で紹介したPerl on a Stickのようにライブラリまで含めて持ち歩いたり、すべての環境にライブラリごとインストールしてしまうほうが簡単になったとか、いまさらjperlを使うべきでない理由はいくつも思いつきますが、いくら問題があろうと、素のPerlで同じことができないのであればjperlを手放す理由にはなりません。今度は、同じことを素のPerlで行う場合はどうするかを見ていきましょう。

encodingプラグマを使う

ごく簡単なjperlスクリプトであれば、Perl 5.8で導入されたencodingプラグマを使ってください。先ほどあげたワンライナーの例はすべてこれだけで解決します[2]⁠。

> perl -Mencoding=sjis -e "print qq/ソ/"
> perl -Mencoding=sjis -e "my $str = qq/A`B/; $str =~ tr/A/B/; print $str"
> perl -Mencoding=sjis -e "my $str = qq/AB/; chop $str; print $str"

それどころか、encodingプラグマは、jperlでは対応できなかったsubstrの問題も解決してくれます。

> perl -Mencoding=sjis -e "my $str = qq/AB/; print substr($str, 0, 1)"

いちいちencodingプラグマを設定するのは面倒だ、という方は、パスの通ったところにこのようなjperl.batを用意しておけばよいでしょう(Windowsの場合⁠⁠。

@perl -Mencoding=sjis %*

これで「jperl -e "print qq/ソ/"」のように書けます。簡単ですね。

encodingプラグマの仕組み

jperlは文字列の処理をする際に個々の文字が2バイト文字に含まれるべき文字かどうかを判別することで日本語対応していましたが、encodingプラグマの仕組みは異なります。

最初にtr///の例がうまくいかない理由を紹介したときに、素のPerlは、たとえば「A`B」という文字列を「\x82\x60\x60\x82\x61」のように解釈する、という話をしました。要するに、Perlにとって「A`B」という「文字列」は、実際には私たちが考えるような文字列ではなく、5つの「オクテット」⁠いわゆるバイト)が連続したバイナリにしか見えていなかったのですが、encodingプラグマを使ってコンテキストを指定してやると、Perlはスクリプトを解釈するときに「\x82\x60\x60\x82\x61」というバイナリを「A」⁠`」⁠B」という3つの「文字」からなる文字列に変換してしまいます。この「文字」は、内部的には複数のオクテットを含んでいますが、文字列処理の際にはあくまでもひとつのマルチバイト文字として扱われるため、途中で泣き別れることはありませんし、内部に含まれるオクテットが変わってしまうこともありません[3]⁠。

encodingプラグマの制約

このように、encodingプラグマは暗黙のうちにオクテット列をPerlの内部表現に変換してしまうため、思いがけない副作用を引き起こすことがあります。

たとえばこのようなjperlスクリプトがあったとしましょう(use strict;をコメントアウトしているのはjperlのバイナリしか入手できなかった方でも実行に困らないようにするためで、他意はありません⁠⁠。

#!/usr/local/bin/jperl

#use strict;

my $file = 'テスト.txt';
open OUT, "> $file" or die $!;
print OUT $file;
close OUT;

open IN, "< $file" or die $!;
print <IN>;
close IN;

ここでは「テスト.txt」というファイルに「テスト.txt」というテキストを出力して、それを再度読み込み、画面に出力したいだけなのですが、これを先ほどと同じようにencodingプラグマを使って実行すると、⁠テスト.txt」というファイルは作られず、得られる出力もこのように不本意なものになってしまいます。

> perl -Mencoding=cp932 test.pl
Wide character in print at test.pl line 7.
"\x{0083}" does not map to cp932.
"\x{0086}" does not map to cp932.
"\x{0082}" does not map to cp932.
"\x{0083}" does not map to cp932.
"\x{0088}" does not map to cp932.
a\x{0083}\x{0086}a\x{0082}1a\x{0083}\x{0088}.txt

これは、encodingプラグマの影響で「テスト.txt」というcp932のオクテット列がPerlの内部表現に変換され、ファイル名や、出力する内容にはその内部表現(をオクテット列に直したもの)が使われてしまったため。

encodingプラグマは、Perlのもっとも基本的な用例のひとつであるフィルタ型のスクリプト(⁠⁠perl script.pl < infile > outfile⁠)を書くときに楽ができるように、標準入出力を通すときにはPerlの内部表現とオクテット列の変換をしてくれるようになっているのですが(要するに、スクリプトの文字コードを含めて、システム標準の文字コードを使っていることが保証できそうな場所についてはencodingプラグマの方で面倒を見てくれるわけです⁠⁠、一般のファイル入出力の場合はかならずしもシステム標準の文字コードが適しているという保証ができないため、その変換を行いません。

そのため、ファイルに出力する場所(7行目)ではPerlの内部表現をcp932に戻す処理が行われずにおかしなオクテット列が書き込まれ、ファイルから読み出す場所ではそのおかしなオクテット列をそのまま読み込み、標準出力に書き出す場所(11行目)ではそのおかしなオクテット列をcp932に変換しようとして、またおかしな結果になってしまった(そして、大量の警告が出力された)わけです。

この問題にはいくつかの解決方法があります。

encodingプラグマを省略してしまう

今回のスクリプトでは、日本語を含む正規表現のようにオクテット列のままだと問題を起こす要素はひとつも使われていません。このような場合は日本語を扱うときのお作法なんて無視して、直感的に理解しやすいオクテット列のまま押し通してしまうのもひとつの手です。Perlはバージョン3.0以降任意のバイナリをそのまま扱えるようになっているので、マルチバイト文字列なんてただのバイナリの塊に過ぎないと割り切ってしまえば、Perlの内部表現に悩む必要はなくなります。今回のスクリプトであればjperlであろうと素のperlであろうとそのまま実行できてしまうのですから、余計なことをする必要はなかったのです。

> perl test.pl
テスト.txt

実際に日本語を含む文字列に対して正規表現などを適用する場合でも、そう。本当に厳密な処理が必要な場合や、tr///のように1文字単位の比較をするような場合は別ですが、ログから日本語を含む行を抜き出すような処理であれば、バイナリのまま比較したところでそれほど問題になることはありません。

Windowsユーザーでこの手法を追求したい方は、筆者のラテン語仲間でもある貞廣知行氏のサイトに興味深い考察がまとめられているので、参考にしてみるとよいでしょう。

openプラグマを使う

外部ファイルの入出力問題については、openプラグマを使って対処することもできます。このプラグマを有効にすると、標準入出力以外の、openやreadpipeを使って読み書きするデータについてもオクテット列とPerlの内部表現との変換を行ってくれるようになります。

> perl -Mopen=":encoding(cp932)" -Mencoding=cp932 test.pl
テスト.txt

こうすると、出力されるファイル名はあいかわらず化けていますが、ファイルの内容と画面に出力される内容については正しくcp932の「テスト.txt」に変わります[4]⁠。

Encodeモジュールを使って個別に対処していく

encodingプラグマやopenプラグマはいかんせん影響範囲が広すぎて、コードが大きくなってくると使いづらいという側面があります。そのため、簡単なjperl用のスクリプトを変換する場合はともかく、多くのモジュールを使う大規模なアプリケーションでこれらのプラグマを使うことはまずありません。

そのかわりに使うのが、Perlの国際化対応の肝であるEncodeモジュールです。

その詳細については次回に譲りますが、ここでは特定の文字コードで書かれたものを(文字コードとは無縁の)Perlの内部表現に変換するときには「⁠⁠文字)コードを取り除く」という意味のdecodeを、Perlの内部表現を特定の文字コードに変換するときはencodeを使う、という基本のルールだけ覚えておいてください[5]⁠。

先ほどの例では、openに渡すファイル名が内部表現のままになっているという問題が残っていました。先ほどと同じようにencodingプラグマとopenプラグマを有効にした状態で実行することを前提とするなら、ファイルを開くときに、このようにファイル名をcp932にencodeしてやれば問題を解決できます。

#!/usr/local/bin/jperl

#use strict;
use Encode;

my $file = 'テスト.txt';
open OUT, encode(cp932 => "> $file") or die $!;
print OUT $file;
close OUT;

open IN, encode(cp932 => "< $file") or die $!;
print <IN>;
close IN;

できれば古いjperlでも動作するようにしておきたい、という方は、Encodeを直接呼び出すのはやめて、このようにラッパ関数のなかから呼び出すようにしてやればよいでしょう。

#!/usr/local/bin/jperl

#use strict;

my $file = 'テスト.txt';
open OUT, encode(cp932 => "> $file") or die $!;
print OUT $file;
close OUT;

open IN, encode(cp932 => "< $file") or die $!;
print <IN>;
close IN;

sub encode {
    if ($] < 5.006) { # jperl
        return $_[1];
    }
    require Encode;
    Encode::encode($_[0], $_[1]);
}

ここではあえて文字コードを自由に設定できるようにしてありますが、cp932でしか使わないことがわかっているのであれば、その設定もencode関数のなかに埋め込んでやればもっと短く書けます。関数名も、encodeではなく、たとえばsjisのような名前にしておけば、より明確になりますし、この関数をモジュールにして、jperl.batから自動的に読み込むようにしてやれば、さらにコード量を少なくできるはずです。

取り残されたformat()の問題

ここまでの内容を理解しておけば、ほとんどのjperlスクリプトはモダンなPerlに移行できますが、ひとつ、いまのところどうしてもモダンなPerlでは即座に対応できないものがあります。その昔、プリンタや画面など、整形したテキストを再利用する必要のない場所に出力するときによく使われていたformat()がそれです。

もちろんモダンなPerlでもformat()がまったく使えなくなったわけではありません。実際、泣き別れなどを気にせず、単なるバイナリ列をフォーマットできればよい、という話であれば、いまでもそれなりには使えるのですが、Perl 5.6以降、openするファイルハンドルにはmyによるレキシカル変数を渡せるはずだからと思って、このようなコードを書くとシンタックスエラーになってしまうくらい、format()はモダンなPerlから取り残されています。

my ($id, $name) = ('ISHIGAKI', 'Ishigaki Kenichi');

open my $OUT, "> format.txt" or die $!;

format $OUT =
ID: @<<<<<<<<<  名前: @>>>>>>>>>>>>>>>>>
$id, $name
.

write($OUT);

その背景には、実際のオクテット数と表示すべき文字幅がかならずしも一致しない文字や、右から左へ文字を並べる言語の問題をきれいに解決する方法がないこと、あるいは、昔と違ってこのような複雑な表示をしたい場合、いちいちPerlで頑張らなくても、ワープロソフトを使うなり、HTML+CSSを使うなりすれば当座の問題は解決できるので直す意欲がわきにくい、といった理由もあるのかもしれませんが、いずれにしても当面はこの問題が解決に向かう見込みはありません。これまで紹介してきた多くの制約を承知のうえでどうしてもformat()を使う必要があるなら、jperlもひとつの選択肢として残しておくしかないでしょう。

内部表現を意識するのは面倒かもしれませんが

Encodeモジュールを使った日本語処理は、jperlの日本語処理に比べて複雑でわかりづらい、という声をよく聞きます。オクテット列(バイナリ)とPerlの内部表現(文字列)の違いを意識しないといけないなんて悪夢だ、というのも、一面では真実でしょう。

でも、いま私たちが扱うテキストは、20年前に比べてはるかに多様化して複雑になっているのもまた事実です。昔はシステム標準の文字コードさえ扱えれば事足りましたが、いまはネットワークを経由してほかのOS、ほかの言語のファイルを取り扱うことも日常茶飯のことになりました。それをひとつのツールで片付けようと思ったら、ツールが複雑化するのは避けられません。

ただ、一般的なWindowsユーザがWindows内部の文字コードを意識することはないように、Perlの場合も、一般的なユーザが内部コードを意識する必要はないようになっています。内部のことを知りすぎた人がトリッキーなことをして自分の足を撃ち抜くことはままありますが、せいぜい自作のモジュールをひとつふたつ使って簡単な日本語処理を行えればよい、というレベルなら、Perlが管理していない(OSなどが管理している)情報にアクセスするときだけはPerlの内部表現を変換する必要がある、ということさえ気をつけておけば、あとはいっさい文字コードを意識しないでスクリプトを書き、実行できます。⁠簡単なことは簡単に、ただし複雑なこともできるように」というのがPerlのモットーなのですから、簡単なことに頭を悩ませているときは前提となる条件を疑ってみたほうがよいかもしれません。

おすすめ記事

記事・ニュース一覧