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

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

この記事を読むのに必要な時間:およそ 7 分

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

かつて,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で行う場合はどうするかを見ていきましょう。

※1

2004年2月にPerl 5.5系列のメンテナンスバージョンが出たのにあわせて,同年4月1日にはエイプリルフールのネタとしてjperlのパッチも更新されましたが,これは例外と見てもよいでしょう。

著者プロフィール

石垣憲一(いしがきけんいち)

あるときは翻訳家。あるときはPerlプログラマ。先日『カクテルホントのうんちく話』(柴田書店)を上梓。最新刊は『ガリア戦記』(平凡社ライブラリー)。

URLhttp://d.hatena.ne.jp/charsbar/

コメント

  • Re:

    # use Encode;
    use strict;
    use 5.00503;
    use Sjis;

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

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

    保存するときのコードは Shiftt-JIS、デリミタは CR+LF です。
    この記事は本当に高度だと思います。

    Commented : #2  ina (2011/07/17, 13:24)

  • テスト.txtを書き込むプログラムについて

    Perlの日本語の文字化けについて理由などがはっきり説明されていて大変参考になりました。
    3ページ目のテスト.txtを書き込むプログラムについて質問があります。このサンプルプログラムを秀丸エディタで作って、test.plという名前で保存しました。保存するときのコードはShift-JIS、デリミタはCR+LF です。
    これをDOSウインドウからコマンド、perl test.pl で動作させてみたのですが 
    open OUT, encode(cp932 => "> $file") or die $!;
    の行でInvalid argument のエラーが発生します。
    どこかいけない所があるのでしょうか?
    ご教授くださったら、助かります。

    Commented : #1  和田 博雄 (2010/11/28, 17:34)

コメントの記入