今日のCPANモジュール(跡地)

宣伝と注意書き

このサイトが元になったCPANモジュールガイドという本を書きました。

本書でも取り上げています。

このページにあることは間違いではないのですが、「UTF8フラグ」「Unicode」「文字列」「バイト列」このあたりについて、より正確で簡潔な解説を心がけました。また、全編を通して日本語を扱う方法もふれています。

2008-06-11

use Encode;

日本語/フラグ周りでつまづくのはありがちなので落ち込む必要はないです。この連載も「そろそろ日本語の話題か・・」と思ったのですが、まとめるのがめんどうでテラ放置してました。

すでにたくさんの資料が出回っていることを踏まえ違う切り口でまとめてみます(少々乱暴な部分もあるので、後で ADVANCED も見て下さい)。

まず、以下はそれぞれだいたい同じものと考えていいです。

混同しやすいのは、上記 A 郡と「UTF-8」が別物という点です。UTF-8 とは Shift_JISEUC-JP と同じ、バイトの並び順の仕様(エンコーディング)で、Unicode を表す方法の一つです。

Perl は Unicode を保持する際内部で付けているフラグを(UTF-8 を使っているため)「utf8」フラグと呼んでおり、一般向けの説明でもその表現なので少々ややこしいです。

文字処理の基本

Encodedecode()/encode() を使います。

use Encode;

# 入力はバイト列。以下は例で、euc-jp エンコーディングでの「ああ」 
my $input = "xa4xa2xa4xa2";

warn length $text; # => 4 (バイト長。2バイト×2文字で4バイト)

# (1) まず該当エンコーディングで decode() する
my $text = decode('euc-jp', $input);

# (2) Unicode になったものを文字として好きに処理する
warn length $text; # => 2 (文字長。2文字)

# (3) 外部へは好きなエンコーディングで encode() して出す
print encode('utf-8', $text); # => xe3x81x82xe3x81x82

上記例の $input$text は意味的には同じ「ああ」ですが、前者はバイト列・後者は文字列です。最後 print() で出す時にまたバイト列にしています。

たとえ入力が UTF-8 な文字だったとしても、そのままでは「UTF-8 で並んだバイト列」にすぎませんので、decode('utf-8', $input); して Unicode にする必要があります。

よくある入出力

具体的な入出力の場面でどう decode()/encode() するのか見てみます。

ファイル読み書き

use Encode;
open(my $fh, '<', 'text.txt') or die $!;
while (<$fh>) {
    my $line = decode('euc-jp', $_); # text.txt は euc-jp の場合
    $line =~ s/^s*//; # とかなんとか
    print encode('utf-8', $line); # 例えば utf-8 で出力
}

CGI.pm

#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use CGI::Carp qw(fatalsToBrowser);
use Encode;
use utf8;

my $cgi = CGI->new;

# charset=Shift_JIS な HTML からのフォーム送信を想定
my $name = decode('cp932', $cgi->param('name')); # 入力を decode()

if ($name eq 'とみた') { # この cgi スクリプト自体は utf-8 で保存すること
    die "「とみた」は私です";
}

my $message = "$nameさん こんちは";

print $cgi->header('-charset' => 'Shift_JIS');
print encode('cp932', $message); # ブラウザに返すものは encode()

Catalyst

C::P::Unicode::Encoding を使うと、入力($c->req->params)の decode()、最終出力($c->res->body)の encode() を config で指定したエンコーディングで自動にやってくれます。

use Catalyst qw(
    ConfigLoader
    ...
    Unicode::Encoding
);

__PACKAGE__->config(encoding => 'euc-jp');

decode()/encode() 専業ですので、必要に応じて Content-Type ヘッダを指定する必要もあります。特に RenderView を使ってる場合は指定しないとデフォルトの 'text/html; charset=utf-8' になるので必須です。

$c->res->content_type('text/html; charset=EUC-JP');

DBIx::Class

DBIx::Class::UTF8Columns を使って ResultSource のクラスで

__PACKAGE__->utf8_columns(qw( name kana ));

とか指定すると、そのフィールドへの出し入れの際に自動で decode()/encode() してくれます。使われるエンコーディング(つまり DB の文字コード)は UTF-8 前提です。それ以外の場合は自分で inflate/deflate を書くです。

Template-Toolkit

POD に明記されていないので要確認なのですが、UNICODEENCODING を指定することで、TT にテンプレートファイルを decode() して読むよう指示できます。

use Template;

my $tt = Template->new({
    UNICODE  => 1,       # テンプレートファイルを Unicode として扱う宣言
    ENCODING => 'utf-8', # テンプレートの文字コード。これで decode() される
});

もしくは Template::Provider::Encoding を使い、以下のようにもできます。この場合 Template::Provider::Encoding の他の機能も使うことができます。

use Template;
use Template::Provider::Encoding;

my $tt = Template->new({
    LOAD_TEMPLATES   => [ Template::Provider::Encoding->new ],
    DEFAULT_ENCODING => 'utf-8', 
});

$tt->process() の結果は Unicode のままですので、出力する時には以下のように encode() します。(process()binmode オプション)や $Template::BINMODE を使うことも可能)

# 上の続き

use utf8;
my $vars = { name => "とみた" }; # 変数は Unicode

$tt->process('template.tt', $vars, my $text) # $text に入れて
    or die $tt->error;

print encode('euc-jp', $text); # 出す時に encode()

その他のモジュールと Unicode

最近は Unicode を渡し Unicode を受け取るモジュールがほとんどであり、そうでないものも移行しつつあります。ですので、今でも文字をむりやり全部バイトのまま扱うことも可能ですが、Unicode で扱ったほうが何かと楽になってきました。

各モジュールの Unicode 受け入れ体制についてはそれぞれの POD や Changes をチェックして欲しいのですが、少なくとも今後この連載で取り上げるモジュールについては日本語まわりの話題に触れるようかと。

Unicode を表記する

上の例ですでに使っていますが、use utf8; するとそのスコープ(レキシカル)において、UTF-8 で書いたリテラルがそのまま Unicode として扱われます。これ以外にも Unicode を表記する方法がいくつかあります。以下の例はどれもだいたい同義で最終的に $text は Unicode です。

# A: utf8 プラグマのスコープ内に utf-8 で書く
use utf8;
my $text = "笑"; # この時点でもう Unicode(ソースは UTF-8 で保存する)

# B: utf-8 で書いて decode()
use Encode;
my $text = "笑"; # この時点ではバイト(ソースは UTF-8 で保存した場合)
   $text = decode('utf-8', $text); # ここで Unicode

# C: xHHxHH 表記で書いて decode()
use Encode;
my $text = decode('euc-jp', "xbexd0"); # バイト部分は笑の euc-jp 表現

# D: B の UTF-8 版. perl にとってはほぼ B と同義
use Encode;
my $text = decode('utf-8', "xe7xacx91"); # バイト部分は笑の utf-8 表現

# E: x{UUUU} 表記. UUUU は 16進表記の Unicode コードポイント
my $text = "x{7B11}";

# F: chr() に Unicode コードポイントの10進を渡す
my $text = chr 31505;  # または、
my $text = chr 0x7B11; # コードポイントは16進表記が多いので 0x 表記が楽かも

特定の文字の Unicode コードポイントや、いろんなエンコーディングでどう表現されるかは Unicode Character Search が超便利です。(例: 「笑」のページ や More.. からたどる 各種エンコーディングでの表記)。最近落ちてることが多くて悲しいです。

Unicode を Data::Dumper した場合、E の "x{UUUU}" 表現になりバイト列と区別が付きます。(ADVANCED も見てください)

正規表現

Perl は Unicode サポートを実直に実装しており、正規表現もサポートしています。具体的には、Unicode に対しては . が「1文字」に対応します。これは良いですが、sdw なども意味的なマッチになるので要注意です。

use utf8;
if ("あ12  " =~ /^(.)(d+)(s+)$/) {
    warn $1; # あ
    warn $2; # 12   // 全角半角問わず数字にマッチ
    warn $3; # "  " // 全角半角問わず空白にマッチ
}

半角スペースや数字のみにマッチさせたい場合、やり方はいろいろありますが

  1. 事前に Unicode::NormalizeEncode::JP::H2Ztr/// などで半角にしておく。
  2. 正規表現の部分だけ(use utf8; の逆である)use bytes; を使う
  3. s の代わりに [ ]d の代わりに [0-9] を使う

3番でいいと思います。

文字化け・エラー

「化けました」(ãããã みたいなアクセント付きアルファベット多数散見)

フラグ付きのものとそうでないものをくっつけるとこう化けます。よく TT を、上で示した方法を取らずに使い、テンプレートファイルと挿入する変数のどちらかがバイト列のまま(= decode() 忘れ)だったりとかで起こります。

「Wide character in print ...」

print "x{7B11}";

フラグ付きのまま print() すると出ます(= encode() 忘れ)。この際、文字そのものは UTF-8 で出てくるので読めちゃうかもしれませんが、print() 前に encode() すべきです。ちなみに warn() だとこの warning は出ないですので warning を当てにしている場合は注意です。

「機種依存文字が出ません」/「波ダッシュが出ません」

'shift_jis' ではなく 'cp932'decode()/encode() するようにすればたいていうまくやってくれます。

「?」になる

ある Unicode を encode() した時、指定したエンコーディングでは対応する文字がないよ、という場合、デフォルトでは ? になります。この動作を、encode() の第三引数で変更してやることができます。(詳細

print encode('shift_jis', "x{3231}"); # => ? (株) が Shift_JIS にないので
print encode('shift_jis', "x{3231}", Encode::FB_HTMLCREF); # => &#12849;
print encode('shift_jis', "x{3231}", Encode::FB_XMLCREF);  # => &#x3231;
print encode('shift_jis', "x{3231}", Encode::FB_PERLQQ);   # => x{3231}

なおブラウザは10進の文字参照(Encode::FB_HTMLCREF)もしくは16進の文字参照(Encode::FB_XMLCREF)でそのまま見えたりしますので使えます(10進の方がより安全と記憶)。

Encode のその他の機能

エンコーディング名の表記ゆれ

Encode はたくさんのエンコーディングを知っていますEncode::JP あたりに日本語で使いそうなエンコーディングが書いてあります。表にあるようにエンコーディング名は表記ゆれをある程度吸収してくれます。(euc-jp の場合 EUC-JP でも ujis でも OK)

Encode::Encoding オブジェクト

Encode::find_encoding() で、あるエンコーディング名がサポートされているかのチェック、そしてサポートされている場合は Encode::Encoding オブジェクトを取得できます。

my $encoding = Encode::find_encoding('cp932')
    or die "non supported";

my $text = $encoding->encode($input); # encode('cp932', $input); と同じ
my $out  = $encoding->decode($text);  # decode('cp932', $text); と同じ

機能というわけじゃないですが

Encode 以外で decode()/encode() というサブルーチンを持つモジュールがけっこうあるので、Encode::decode()/Encode::encode() と書く習慣を付けてると問題が防げるかもしれません。


ADVANCED

だいたい同じと乱暴にまとめた latin-1 の話

latin-1 とは俗称で、正式には ISO-8859-1 とか言う。おおざっぱに言うと ascii 127 文字の続きに1バイトに収まる範囲でアクセント記号付きアルファベットとかを付け加えたもの()。付け加えるもののバリエーションで latin-2(ISO-8859-2)とかあったりする。

Perl は utf8 フラグのなかった時代のコードもそのまま動くよう、latin-1 の範囲(1バイト = 10進で255以下、16進で xFF 以下)を優遇しその範囲は Encode::decode() なしにフラグが立たないようにしている。(例えば "x{UUUU}"chr 0xUUUU 表記)

なので Data::Dumper で "x{UUUU}" 表記になるか見るだけで utf8 フラグが立っているかどうかを判断するのは実は正確ではない。日本語(非1バイト)を扱っている場合はだいたいそれでわかりますが。

この辺は主に入出力を担当するモジュールを書く人が気をつけるべき話題で、モジュールを作る人はできるだけ利用者が Unicode だけを使えばよいようにしてあげよう。ということで本編では流した。モジュールでの Unicode 利用の歴史: 1, 2, 3

自身でフラグを気にしなければいけない場合は Data::Visitor::Encode が便利。

まったく登場しない utf8:: の話

utf8::decode()Encode::decode() と違い latin-1 の範囲はフラグを立てないのでその差に注意しなければならない。あと日本人はどうせ utf8::* だけでは済まないことが多い。なので utf8:: ではなく Encode::* 推奨とした。

同様の理由で Catalyst でエンコーディングが UTF-8 だけの場合は使えるのだけど C::P::Unicode は割愛した。

まったく登場しない encoding プラグマの話

非推奨であるのとソースは UTF-8 を使えるケースが多いと思うのでスルー。

まったく登場しない PerlIO の話

Perl には PerlIO というファイルハンドル動作のラッパー機能があり、これを使ってファイル open や print の時 自動で decode()/encode() させることができる。

PerlIO は便利だけどこれ専用の機能ではないし Encode::decode()/ Encode::encode() で済むといえば済むので、よくわからないうちは無理に使わなくても良いと思いスキップ。あくまでも便利機能という位置づけとした。

ここで簡単に触れておきます。PerlIO の使い方は3パターン。それぞれ例を示します。

と、decode()/encode() を書かずに透過的に decode()/encode() できます。PerlIO レイヤは :utf8:encoding() タグ以外にもいろいろあったりします。入出力のラッパーだと認識した上で使うと便利です。

cp932 の話

Encode の Shift_JIS エンコーディングは純粋な Shift_JIS。Microsoft が Shift_JIS を拡張して入れた ?? みたいな文字を含む Shift_JIScp932 というエンコーディングで別に入っている。機種依存文字も最近は Mac でも見れたり入力できたりするので、cp932 をつかっておけばよい、とした。

この話題についてはLegacy Encoding Project に詳しい。shift_jis に対する cp932 と同じようなポジションで Encode::EUCJPMS や Encode::ISO2022JPMS(開発中?)がある。

波ダッシュ/全角チルダ問題

charset=Shift_JIS な HTML からキーボードから普通に入力した「?」は、shift_jisdecode() すると U+301C に、cp932 では U+FF5E にマッピングされる。さらに、charset=UTF=8 な HTML からは Windows と Mac で違うのが来て、utf-8decode() すると Windows からのは U+FF5E に、Mac からのは U+301C にマッピングされる。

問題は encode() 時で、shift_jis には U+FF5E に対応するのがない。まあ shift_jis には機種依存文字もないので良いが、cp932 は機種依存はあるが U+301C に対応するのがないので ? になる。

といっても実際問題になるケースは少なく、Mac から decode('utf-8') したのを encode('utf-8') して保存しておいて、場合によっては decode('cp932') で出すというケースとかだが、Encode::FB_HTMLCREF を使うのは嫌でどうしても出したい時は

# cp932 を使って encode() する場合。shift_jis でやる場合は逆にする
$text =~ s/x{301C}/x{FF5E}/g; # WAVE DASH to FULLWIDTH TILDE
print encode('cp932', $text);

みたいに置換してから encode() という方法がある。

しかしこれって「PC/携帯 でデータを共有するサービス」が想定されるケースであり、携帯に特化したエンコーディングパックを追加する Encode::JP::Mobileshift_jis 系エンコーディング(x-sjis-docomo)とかは cp932 ベースだが U+301C も特別に入っているので置換とかする必要がないので普通にこれを使えば良い。(今のところ波ダッシュ以外の違う奴はサポートしていないが)

その他、Unicode / Encode に関係あるけど割愛したもの

正規表現

Encode