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

第33回 enc2xs:標準の文字コード表にはない文字を変換する

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

Encodeを使っても文字化けするとき

Encodeは特定のエンコーディングにしたがって配列されたバイナリを「文字列」に置き換えるためのモジュールですが,かならずしもすべてのエンコーディングがあらゆるバイナリの組み合わせに対応しているわけではありません。

たとえば,「シフトJIS」環境における機種依存文字の例としてよく取り上げられる丸付き数字をEncodeのお作法通りにdecode,encodeする場合,「シフトJIS」だからと思って安易にshiftjis系列のエンコーディングでdecodeしてしまうと,丸付き数字のマッピングデータがないため「?@」のように文字化けを起こしてしまいます。

use strict;
use warnings;
use Encode;

my $binary = pack('C*', 0x87, 0x40); # ①;
my $string = decode(shiftjis => $binary);
print encode(shiftjis => $string);

この場合は,第31回の注にも書いたように,cp932と呼ばれるエンコーディングを使うか,

use strict;
use warnings;
use Encode;

my $binary = pack('C*', 0x87, 0x40); # ①;
my $string = decode(cp932 => $binary);
print encode(cp932 => $string);

あるいはCPANからEnocde::JIS2Kというモジュールをインストールして,このモジュールが提供しているshiftjisx0213(shiftjis2000,shiftjis2k)というエンコーディングを使う必要があるのでした※1)。

use strict;
use warnings;
use Encode;
use Encode::JIS2K;

my $binary = pack('C*', 0x87, 0x40); # ①;
my $string = decode(shiftjis2k => $binary);
print encode(shiftjis2k => $string);

もっとも,いつでもこのように最適なエンコーディングを利用できるとは限りません。Perlの内部表現にはマッピングされていない組み合わせもありますし,マッピングはあっても適切な文字があたっていないこともあります。最適なマッピングがあっても,利用する側に十分な予備知識がなければ選べないでしょうし,データ自体が複数のエンコーディングを利用しているため,そのままでは処理できない場合もありそうです。

※1

丸付き数字は2000年に制定されたJIS X 0213で正式にJIS規格入りしました。このように拡張エンコーディングを利用する場合はふつう明示的に拡張モジュールをuseする必要があります。

文字化けを許容したくない場合

まったく毛色の異なるエンコーディングであればencodeの結果を見れば一目瞭然とはいえ,shiftjisとcp932のように内容的にほとんど差がないエンコーディングの場合,めったなことでは文字化けにはなりません。そのようなものを毎回目視でチェックするのは無茶ですし,見落としのもとですから,文字化けの不安があるなら機械的に検出できるようにしておくべきでしょう。

Encodeは,デフォルトではエラーに対して非常に寛容な設定になっています。すでに「文字列」と認識されている内部表現をさらに「文字列」にしようとしたときは「Wide character in subroutine entry」というエラーになりますが,encode済みのオクテット列をさらにencodeする場合は(内部で強制的に文字列化が行われてしまうため)エラーにはなりませんし,間違ったエンコーディングを利用したからといってとがめられることもありません。

処理の自動化よりもデータの整合性のほうが大事な場合は,decodeやencodeにDIE_ON_ERR(または,それを引き継いだFB_CROAK)というフラグを渡しておくと,マッピングにない値を変換しようとしたときにエラーを吐いてくれます。このようなフラグはデフォルトではエクスポートされないので,利用する場合は明示的にエクスポートするか,完全修飾名で指定してください。

use strict;
use warnings;
use Encode;

my $binary = pack('C*', 0x87, 0x40); # ①;
my $string = decode(shiftjis => $binary, Encode::DIE_ON_ERR);
print encode(shiftjis => $string);

文字参照を利用する

大勢に影響を及ぼさなければ多少の文字化けは許容するけれど,最適なエンコーディングを用意するためにもどの文字が化けたのかは把握しておきたい,という場合は,チェックフラグを利用してマッピングがなかった文字のコードを表示させることもできます。

use strict;
use warnings;
use Encode;

my $binary = pack('C*', 0x87, 0x40); # ①;
my $string = decode(shiftjis => $binary, Encode::FB_PERLQQ); # \x87@
print encode(shiftjis => $string);

この例では,内部表現にマッピングできなかったオクテットのかわりに,Perlが理解できるような形で16進表記した文字列に置換されます(チェックフラグを使わない場合,マッピングのなかった文字はdecode失敗を意味する特殊な内部表現に置き換えられます)。

このチェックフラグは,encodeの際に適用することもできます。Encode 2.12以降ではフラグのかわりにコードリファレンスを渡すこともできるようになっているので,ここではそちらの例を載せておきましょう。

use strict;
use warnings;
use Encode;

my $binary = pack('C*', 0x87, 0x40); # ①;
my $string = decode(cp932 => $binary); # ひとまず正しい内部表現に
print encode(shiftjis => $string, \&check);  # \x{2460}

sub check {
    my $binary = shift;
    sprintf "\\x{%04x}", $binary;
}

自分でエンコーディングを用意する

チェックフラグ/コードは,限定的な用途では役に立ちますが,先ほどの例からもわかる通り,マッピングのないマルチバイト文字を泣き別れないように別の内部表現にマッピングしなおすような用途では使えません(マッピングのないマルチバイト文字についてはオクテット単位で処理されるので泣き別れしてしまいます)。マルチバイト文字の扱いに不満がある場合は自分でエンコーディングを用意してしまうのが早道です。

自分でエンコーディングを用意する,というと何やら大変なことのように思われるかもしれませんが,ふつうは既存のエンコーディングに少し手を加えるだけですからそれほどむずかしいことはありません。ここでは試しに丸付き数字の処理を少し変えてみましょう。

新しいエンコーディングモジュールの作り方については,Encodeに付属のEncode::Encodingというモジュールとenc2xsというコマンドにそれぞれ簡単な解説があります。単純なマッピングテーブルを用意できる場合はenc2xsを,マッピングの前後に特殊な処理が必要だったり,マルチバイト文字列の前後に特殊な符号がつくなど,動的にしか判定できないものについてはEncode::Encodingを使う,というのが原則です。今回はマッピングテーブルを書き換えるだけで対応できそうですから,enc2xsを使うことにします。

enc2xsを使う場合は.ucmという拡張子を持つマッピングテーブルを先に用意しておく必要があります。このテーブルはEncode独自のものではなくICU (International Components for Unicode)プロジェクトでも使われている標準的なマッピングのサブセットにあたるもので,詳細を知りたい方はICUプロジェクトのサイトに解説がありますが,ふつうはベースとなるマッピングテーブルを適当に加工するだけなので細かなことは気にしなくても結構です。

適当なディレクトリを用意したら,まずはEncodeのディストリビューションに同梱されているshiftjis.ucmというファイルに適当な名前をつけて保存しましょう。ここではshiftjis.ucmの派生物であることがわかるように,shiftjis_with_circled_numbers.ucmという名前で保存してみます。

それが済んだら,いま保存したucmファイルを開いて,先頭にある「<code_set_name> "shiftjis"」という行を「<code_set_name> "shiftjis_with_circled_numbers"」と修正します(これがEncodeで利用するエンコーディング名になります)。

続いてこのようなコマンドを実行すると,Makefile.PLをはじめ,いくつかのファイルが生成されます。ShiftjisWithCircledNumbersは新しく用意するエンコーディングにつけるパッケージ名です。

> enc2xs -M ShiftjisWithCircledNumbers shiftjis_with_circled_numbers.ucm

ここまでできたら,あとは(もし入っていなければコンパイラをインストールしてから)いつも通りperl Makefile.PLと,make (test)を実行することで,先ほどcode_set_nameに指定した「shiftjis_with_circled_numbers」というエンコーディングを利用できるようになります。

ためしにこのようなテストスクリプトをtest.plという名前で保存してから,make testしてみてください(このような独自エンコーディングを利用する場合は,エンコーディングを提供しているモジュールもロードする必要があります)。ここではまだ丸数字のマッピングを追加していないので後半のテストは失敗しますが,通常のシフトJISにおさまる文字は問題なくdecode/encodeできるはずです。

use strict;
use warnings;
use Encode;
use Encode::ShiftjisWithCircledNumbers;
use Test::More;

{
    my $binary = pack('C*', 0x82, 0xA0); # あ;
    my $string = decode(shiftjis_with_circled_numbers => $binary);
    is $binary => encode(shiftjis_with_circled_numbers => $string);
}

{
    my $binary = pack('C*', 0x87, 0x40); # ①;
    my $string = decode(shiftjis_with_circled_numbers => $binary);
    is $binary => encode(shiftjis_with_circled_numbers => $string);
}

done_testing;

著者プロフィール

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

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

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

コメント

コメントの記入