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

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

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

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

まったく毛色の異なるエンコーディングであれば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;

マッピングを追加する

では、テストを通すためにマッピングを追加しましょう。マッピングデータはucmファイルの「CHARMAP」「END CHARMAP」に囲まれた部分に登録することになっています。重複するマッピングがある場合は並び順にも意味はありますが、新しいマッピングを追加するときは並び順を気にせず、最後に追加しておけば大丈夫です。

enc2xsのPODにも説明があるように、マッピングデータはUnicodeの文字ID、decodeする前/encodeしたあとの生のバイト列、フォールバックフラグ、⁠あれば)コメントの順に書きます。

Unicodeの文字IDはUnicodeサイトのチャートを調べればわかりますが、これはあくまでもほかのエンコーディングとの互換性を維持するために利用する便宜的なIDなので、ほかのエンコーディングではどのようなバイナリ表現になるかを調べて、そこから逆算したり、先ほど紹介したようにencode時のチェックフラグを利用することでも簡単にIDを取得できます(nullというエンコーディングを利用するとかならずチェックコードを通るようになることも覚えておくと便利です⁠⁠。

> perl -MEncode -MTerm::Encoding -e "print encode(null => decode(Term::Encoding::get_encoding, q/①/), Encode::FB_PERLQQ)"

バイナリ表現は、高機能なテキストエディタであればおそらくステータスバーなどに文字コードを表示できるようになっているでしょうが、上の例と同じようにチェックフラグを利用して調べることもできます(ここではバイナリへの自動ダウングレードが起こっても問題ないことがわかっているので横着をしていますが、厳密に書くならもちろんdecodeの結果はencodeでくくるべきです⁠⁠。

> perl -MEncode -e "print decode(null => q/①/, Encode::FB_PERLQQ)"

フォールバックフラグは、マルチバイト文字と内部表現が完全に一対一対応する場合は0を指定します(新しいマッピングを追加するときはたいていそうなるはずです⁠⁠。

同じようにほかの丸数字についてもIDとバイナリ表記を調べていくと、このようなマッピングをshiftjis_with_circled_numbers.ucmの最後に追加して、perl Makefile.PLからやり直せば、先ほどのテストが通るようになることがわかります。

<U2460> \x87\x40 |0 # CIRCLED DIGIT ONE
<U2461> \x87\x41 |0 # CIRCLED DIGIT TWO
<U2462> \x87\x42 |0 # CIRCLED DIGIT THREE
<U2463> \x87\x43 |0 # CIRCLED DIGIT FOUR
<U2464> \x87\x44 |0 # CIRCLED DIGIT FIVE
<U2465> \x87\x45 |0 # CIRCLED DIGIT SIX
<U2466> \x87\x46 |0 # CIRCLED DIGIT SEVEN
<U2467> \x87\x47 |0 # CIRCLED DIGIT EIGHT
<U2468> \x87\x48 |0 # CIRCLED DIGIT NINE
<U2469> \x87\x49 |0 # CIRCLED NUMBER TEN
<U246A> \x87\x4A |0 # CIRCLED NUMBER ELEVEN
<U246B> \x87\x4B |0 # CIRCLED NUMBER TWELVE
<U246C> \x87\x4C |0 # CIRCLED NUMBER THIRTEEN
<U246D> \x87\x4D |0 # CIRCLED NUMBER FOURTEEN
<U246E> \x87\x4E |0 # CIRCLED NUMBER FIFTEEN
<U246F> \x87\x4F |0 # CIRCLED NUMBER SIXTEEN
<U2470> \x87\x50 |0 # CIRCLED NUMBER SEVENTEEN
<U2471> \x87\x51 |0 # CIRCLED NUMBER EIGHTEEN
<U2472> \x87\x52 |0 # CIRCLED NUMBER NINETEEN
<U2473> \x87\x53 |0 # CIRCLED NUMBER TWENTY

encode専用のマッピングを追加する

Encodeはさまざまな環境で使われますから、マッピングはなるべくラウンドトリップ可能な状態にしておいた方がよいのですが、場合によってはあえてラウンドトリップできないようにしてしまうこともできます。

たとえば、機種依存文字である丸付き数字はポリシー的に使いたくない、という場合。もちろんいったんdecodeして内部表現にしたあと、正規表現を使って「(1)」のような形に置換し、それをさらにencodeしてもよいのですが、わざわざ正規表現を使わなくても、encodeのときに別の表記にマッピングしたほうが効率的です。

このように、encodeのときだけ別の表記にしたい場合は、フォールバックフラグを1にしたマッピングを追加します。先ほどと同じように「(1)」のバイナリ表現を調べると、このようなマッピングを追加することで、正規表現を使うことなく丸付き数字をすべてかっこ付き数字に置き換えることができるようになります。

<U2460> \x28\x31\x29 |1 # CIRCLED DIGIT ONE
<U2461> \x28\x32\x29 |1 # CIRCLED DIGIT TWO
<U2462> \x28\x33\x29 |1 # CIRCLED DIGIT THREE
<U2463> \x28\x34\x29 |1 # CIRCLED DIGIT FOUR
<U2464> \x28\x35\x29 |1 # CIRCLED DIGIT FIVE
<U2465> \x28\x36\x29 |1 # CIRCLED DIGIT SIX
<U2466> \x28\x37\x29 |1 # CIRCLED DIGIT SEVEN
<U2467> \x28\x38\x29 |1 # CIRCLED DIGIT EIGHT
<U2468> \x28\x39\x29 |1 # CIRCLED DIGIT NINE
<U2469> \x28\x31\x30\x29 |1 # CIRCLED NUMBER TEN
<U246A> \x28\x31\x31\x29 |1 # CIRCLED NUMBER ELEVEN
<U246B> \x28\x31\x32\x29 |1 # CIRCLED NUMBER TWELVE
<U246C> \x28\x31\x33\x29 |1 # CIRCLED NUMBER THIRTEEN
<U246D> \x28\x31\x34\x29 |1 # CIRCLED NUMBER FOURTEEN
<U246E> \x28\x31\x35\x29 |1 # CIRCLED NUMBER FIFTEEN
<U246F> \x28\x31\x36\x29 |1 # CIRCLED NUMBER SIXTEEN
<U2470> \x28\x31\x37\x29 |1 # CIRCLED NUMBER SEVENTEEN
<U2471> \x28\x31\x38\x29 |1 # CIRCLED NUMBER EIGHTEEN
<U2472> \x28\x31\x39\x29 |1 # CIRCLED NUMBER NINETEEN
<U2473> \x28\x32\x30\x29 |1 # CIRCLED NUMBER TWENTY

decode専用のマッピングを追加する

逆に、decode専用のマッピングを追加することもできます。たとえば全角の「1」を半角化したければ、次のようなマッピングを追加すると、decode時に半角の1にマッピングできます。

<U0031> \x82\x50 |3 # DIGIT ONE

同じ方法でほかの全角英数字を半角化したり、半角カナを全角化することもできますし、たとえばモバイルサイト用に複数キャリア共通のテンプレートを使っている場合など、ほかのキャリアでは表示できないのだけれど泣き別れて半角文字に解釈されては困るような絵文字をとりあえず内部表現にマッピングしておきたい場合も、decode専用のマッピングを追加することになります。

さらに複雑なマッピングをしたい場合

マッピングテーブルを使ったエンコーディングは書くのも楽ですし、処理も高速ですが、場合によってはそれだけでは済まないこともあります。

たとえば、今度はかっこ付き数字から丸付き数字へ変換したいとしましょう。直感的には先ほど追加した丸付き数字からかっこ付き数字へのマッピングのフォールバックフラグを3に変えれば済むように思えますが、少なくともこの原稿を執筆している現在、それでは期待通りの動作はしません。

このような場合は、マッピングテーブルを作成したときに用意されるデフォルトのコードを使うのではなく、Encode::Encodingを使って自前のdecode/encode関数を用意してやる必要があります。

やり方はいろいろありますが、いずれにしてもそのままではenc2xsが用意したエンコーディング名やdecode/encode関数が使われてしまいますので、ここではまずucmファイルのcode_set_nameを「_shiftjis_with_circled_numbers」という内部向けの名前に変更します。また、効果がわかりづらくなるので、先ほど追加した丸付き数字からかっこ付き数字へのマッピングも消しておきましょう。

続いて、enc2xsが用意してくれたShiftjisWithCircledNumbers.pmにこのようなコードを追加します。packageの宣言をコメントで改行しているのは、CPANにアップロードしたときに不要なパッケージ名を見せないようにするため。完璧を期すならPerlIOに対応するためのコードやMIMEタイプ等々のコードが必要ですが、ここでは省略しました。

package #
    Encode::ShiftjisWithCircledNumbers::Main;

use strict;
use warnings;
use Encode;
use base 'Encode::Encoding';
__PACKAGE__->Define('shiftjis_with_circled_numbers');

sub decode ($$;$) {
    my ($self, $octets, $check) = @_;
    my $string = Encode::decode(_shiftjis_with_circled_numbers => $octets);
    $string =~ s/\(([1-9]|1[0-9]|20)\)/chr(ord("\x{2460}") + $1 - 1)/ge;
    $_[1] = $string if $check;
    $string;
}

sub encode ($$;$) {
    my ($self, $string, $check) = @_;
    my $octets = Encode::encode(_shiftjis_with_circled_numbers => $string);
    $_[1] = $octets if $check;
    $octets;
}

実装としては内部表現にdecodeしたあと追加で置換しているだけですが、このようにお決まりの処理をdecode内部に入れてしまうだけでもモジュールの外のコードはすっきりします。より複雑な置換を行う場合は_shiftjis_with_circled_numbersでdecodeするときにチェックフラグをつける必要などがあるかもしれませんが、その辺はPure Perlで書かれているのですから、よいように手を入れてください。

みんなの利益になるものはCPANに

ここまでの内容が理解できれば、Encode::JP::Mobileのように複数のキャリアに対応した複雑なエンコーディングであっても、マッピングを追いながら自分好みに改造できることでしょう。特定の絵文字だけは文字列で表現するとか、画像へのリンクを含んだHTMLタグで表現するとかいうこともエンコーディングひとつで対応できるのですから、文字単位の置換をするために毎回同じような正規表現を書いているなあと思ったときにはぜひエンコーディングを拡張することを検討してみてください。

ただし、なんでも自分好みに改造できるからといって、あきらかにほかのエンコーディングのバグとおぼしきものまで手元の環境で直してしまうのはやりすぎです。Encodeモジュールに標準添付されているエンコーディングであればCodeReposgithubにリポジトリがありますし、モバイル業界御用達のEncode::JP::Mobileは、CodeReposにリポジトリがあるだけでなく、CPANへのリリースさえも複数のCPAN Authorが共同管理しているCODEREPOS用のアカウントで行われていますので、問題を見つけた場合はきちんとコミュニティに還元していただければと思います。

また、CPANにはほかにもさまざまな拡張エンコーディングが存在しています。ちょっとした不満であれば、案外すでにCPANにあがっているエンコーディングを使うだけでも解決してしまうかもしれませんので、自前のエンコーディングをつくるときには念のためもっと便利に使えるエンコーディングがないか確認してみるとよいでしょう。

おすすめ記事

記事・ニュース一覧