daily dayflower

2008-02-19

UTF8 フラグあれこれ

UTF8 フラグについてわかってるつもりだったんですが, utf8::is_utf8 considered harmful - Bulknews::Subtech - subtech を読んで混乱したので,自分なりにまとめてみました。間違いがありましたらご指摘よろしく。

まとめ

  • スカラー変数の内部表象の状態を示すものとして UTF8 フラグというものがある
  • スカラー変数は(リファレンス等は別として)下記のものを格納できる
  • Perl は(後方互換性確保などの理由から)ISO-8859-1 の文字範囲内に収まる文字列リテラルを記述すると,デフォルトで UTF8 フラグなし文字列(in ISO-8859-1 encoding)とする
  • スカラー変数が……
    • UTF8 フラグが立っている場合,(A) であると断定可能
    • UTF8 フラグが立っていない場合,(B) か (C) は断定できない(開発者の意図次第)
    • ただし,「文字列」であるなら (A) か (B) である,とみなすべき(Perl の慣例にしたがうと)
  • もし (A), (B), (C) を引数にとる関数を書くのであれば,引数が文字列か否かによって API をわけるべき;つまり,(A) や (B) を受け取る関数と (C) を受け取る関数にわけるべき
    • (Web プログラマは特に)UTF8 フラグがついていない文字列を(その対称性から)UTF-8 octet stream とみなすと便利だなーと考えがちであるが,その認識は誤り(A と C-2 をごっちゃにして扱っているから)*1

UTF8 フラグとはなにか

What is "the UTF8 flag"?

Please, unless you're hacking the internals, or debugging weirdness, don't think about the UTF8 flag at all. That means that you very probably shouldn't use is_utf8, _utf8_on or _utf8_off at all.

The UTF8 flag, also called SvUTF8, is an internal flag that indicates that the current internal representation is UTF-8. Without the flag, it is assumed to be ISO-8859-1. Perl converts between these automatically.

One of Perl's internal formats happens to be UTF-8. Unfortunately, Perl can't keep a secret, so everyone knows about this. That is the source of much confusion. It's better to pretend that the internal format is some unknown encoding, and that you always have to encode and decode explicitly.

What is the %22UTF8 flag%22?

(下線部は筆者による強調)

ざっくり要約すると

ISO-8859-1 (aka Latin-1) とはなにか

U+0000 〜 U+00FF を 0x00 〜 0xff の1バイトにマッピングするエンコーディングです。

#
# $Id: 8859-1.ucm,v 2.0 2004/05/16 20:55:19 dankogai Exp $
#
# Original table can be obtained at
# http://www.unicode.org/Public/MAPPINGS/ISO8859/8859-1.TXT
#
<code_set_name> "iso-8859-1"
<mb_cur_min> 1
<mb_cur_max> 1
<subchar> \x3F
CHARMAP
<U0000> \x00 |0 # NULL
<U0001> \x01 |0 # START OF HEADING
<U0002> \x02 |0 # START OF TEXT
<U0003> \x03 |0 # END OF TEXT

...... snip ......

<U00FC> \xFC |0 # LATIN SMALL LETTER U WITH DIAERESIS
<U00FD> \xFD |0 # LATIN SMALL LETTER Y WITH ACUTE
<U00FE> \xFE |0 # LATIN SMALL LETTER THORN
<U00FF> \xFF |0 # LATIN SMALL LETTER Y WITH DIAERESIS
END CHARMAP

すべての文字が1バイトコーディングであり 0x00 〜 0xff を網羅しているので,8bit binary ⇔ ISO-8859-1 の相互変換が(文字列としての意味はなくなりますが)可能です。逆に U+0100 以上の文字を含む Unicode 文字列から変換するとかなりの文字が失われます。

実例

前提として,以下で頻出する文字の様々な representation を表記しておきます。

文字Unicode Character PointUTF-8 representation
éU+00E9c3-a9
U+6F22e6-bc-a2
#!/usr/bin/perl

use strict;
use warnings;
no utf8;

my $str;

print length "H\x{e9}llo", "\n";
# => 5
#
# utf8 flag: off (Internal: ISO-8859-1)
#
# {utf8=0}[H][\xe9][l][l][o]

print length "H\x{e9}llo \x{6f22}", "\n";
# => 7
#
# utf8 flag: on  (Internal: UTF-8)
#
# {utf8=1}[H][\xc3,\xa9][l][l][o][ ][\xe6,\xbc,\xa2]

print length "H\x{c3}\x{a9}llo", "\n"
# => 6
#
# utf8 flag: off (Internal: ISO-8859-1)
#
# {utf8=0}[H][\xc3][\xa9][l][l][o]
#   U+00C3 : LATIN CAPITAL LETTER A WITH TILDE
#   U+00A9 : COPYRIGHT SIGN
# 
# can also be recognized as UTF-8 octet stream (by user)

同じ \x{e9} という expression を書きながらも,2つめの内部表象[\xc3, \xa9] に変換されているところが注目です。

Unicode 文字列の透過性 (1)

こと Latin-1 の範囲(U+0000 〜 U+00FF)に収まる Character で構成される文字列に限っていえば,同じ文字列を内部 Latin-1 でも 内部 UTF-8 でも表象できます。

このとき,eq オペレータで比較すると,(内部表象はどうであれ)等価な文字列かどうかを比較することができます。

#!/usr/bin/perl

use strict;
use warnings;

my $str1 = "H\x{e9}llo";
my $str2 = $str1;
utf8::upgrade($str2);

# Now, $str1 is internally represented in Latin-1 encoding
#      $str2 is internally represented in UTF-8   encoding

# $str1: {utf8=0}[H][\xe9]     [e][l][l][o]
# $str2: {utf8=1}[H][\xc3,\xa9][e][l][l][o]

# 内的エンコーディングが違うだけで両者の意味するものは同じ

print $str1 eq $str2 ? 1:0, "\n";
# => 1

一方,Latin-1 を超える(U+0100 〜)文字を含む文字列は,Perl「文字列」としては UTF-8表象することしかできません。UTF-8 octet stream なバイナリ列として表現することは可能ですが,そのような意味づけは Perl には伝わりません。

my $str1 = "\x{e6}\x{bc}\x{a2}";    # 漢 in UTF-8 octet stream
my $str2 = "\x{6f22}";              # 漢 in Unicode String

# $str1 is in Latin-1 encoding
# ( and can be considered as UTF-8 octet stream )
# $str2 is in UTF-8 encoding

# $str1: {utf8=0}[\xe6]          [\xbc][\xa2]
# $str2: {utf8=1}[\xe6,\xbc,\xa2]

# $str1 は(プログラマの意図するコンテキストを無視すると)
#   U+00E6 LATIN SMALL LETTER AE       (ae)
#   U+00BC VULGAR FRACTION ONE QUARTER (1/4)
#   U+00A2 CENT SIGN                   (¢)
# の結合にすぎない
# たまたま内的なストレージのバイナリ表象が一致しているだけ

print $str1 eq $str2 ? 1:0, "\n";
# => 0

あんまりいい例ではないですが,文字列の比較は内部表象ではなく意味的な比較が行われていることがわかると思います。

Unicode 文字列の透過性 (2)

既存の文字列を切り出したり,くっつけたりした場合にどうなるか。

#!/usr/bin/perl

use strict;
use warnings;
no utf8;

my $str;

$str = "H\x{e9}llo \x{6f22}";
print utf8::is_utf8($str) ? 1:0, "\n";
# => 1

$str = substr $str, 0, 5;
print utf8::is_utf8($str) ? 1:0, "\n";
# => 1

$str = "H\x{e9}llo";
print utf8::is_utf8($str) ? 1:0, "\n";
# => 0

$str .= " \x{6f22}";
print utf8::is_utf8($str) ? 1:0, "\n";
# => 1
  • 2番目の例から,UTF8 フラグつき文字列を切り抜いても UTF8 フラグつき文字列となることがわかる
  • 4番目の例から,UTF8 フラグなし文字列に UTF8 フラグつき文字列を結合すると,前者を Latin-1 文字列と見なし,自動的に UTF-8 文字列にアップグレードし,結合することとなる
    • このことは Perl が UTF8 フラグなし文字列を Latin-1 文字列とみなしている証左ともいえる

既存モジュール戻り値

一部プログラムを改変しますが引用します。

問題となるのは例えば以下のような場合、

use HTML::Entities;

my $s1 = "H&eacute;llo";
my $s2 = "H&eacute;llo &#x6f22;";

my $t1 = decode_entities($s1);
my $t2 = decode_entities($s2);

warn utf8::is_utf8($t1);
warn utf8::is_utf8($t2);

$t1, $t2 はどちらも HTML を decode する、という処理でえられた変数であるが、$t1 は utf-8 フラグがなく(latin-1 レンジのみで構成されているため)、$t2 には UTF-8 フラグがついている。このような2つの変数に対し、utf8::is_utf8フラグをチェックして encode_utf8 するかどうかをきめるというのは間違っている。($t1 は latin-1 エンコーディングのままになってしまう)

utf8::is_utf8 considered harmful - Bulknews::Subtech - subtech

ここを一読したとき,最初は「それって HTML::Entities の出力が悪い(仕様が悪い)だけじゃん」と思いました。でも上記で書いたようなことを考えるとそうでもないことがわかりました。

先ほどまでの表記を流用すると,

# $t1 := {utf8=0}[H][\xe9]     [l][l][o]
# $t2 := {utf8=1}[H][\xc3,\xa9][l][l][o][ ][\xe6,\xbc,\xa2]

warn $t1 eq substr($t2, 0, 5);

のようになります。文字列の表象として $t1$t2 も間違っているわけではないので,HTML::Entities に非はないことがわかります。むろん考えようによって不便といえば不便なのですが,$t1表象として「{utf8=1}[H][\xc3,\xa9][l][l][o]」が帰ってくるはず,と仮定(断定)することに(Perl の流儀として)問題があるのかな,と。

Web アプリケーションの開発者として

なんだかごちゃごちゃと難しそうだけど,じゃあどうすればいいのか。あくまで個人的な指針ですが,

でやってます。

他の言語ユーザからは蛇蝎のごとく嫌われてる UTF8 フラグですが(そうでもない?),こと文字列を扱うアプリケーション(Web アプリも含む)を組むうえでは,central charset として Unicode がサポートされているのは非常に助かってます。またその表象が(基本的に)UTF-8 であったため,サロゲートペアなどの地獄にはまることもありませんし。

フレームワークを使った場合にどうすればいいのか,は,わかりません。すいません。

*1:ただし,その仕様をドキュメントに明示してあれば一応不可ではない

miyagawamiyagawa 2008/02/20 18:16 すばらしいですね。

記事にも書いたんですが、HTML::Entities の例は、$t1, $t2 に対して utf8::upgrade() を呼ぶことで UTF-8 フラグありであることをプログラマ側が保証することができます。ので、「バイト列であれば UTF-8 bytes として扱い、フラグがついていれば文字列として扱う。latin-1 レンジのみで構成されていてフラグがついているかわからないなら、utf8::upgrade を事前にしろ」という風にドキュメントに書いておけば、「間違いではない」のかもしれませんが、自動アップグレードなどの仕組みを考えると自然でないというか、ユーザに不便をしいているだけな気がします。

use bytes プラグマがここで使えるような気がするんですけどね。あるメソッドを呼ぶとき、通常は引数を文字列として扱いつつ use bytes されていたらバイト列としてあつかう、というような。%H ハッシュみればできるのかな。CPAN モジュールとして標準的な作法があるとうれしいかもしれませんね。

dayflowerdayflower 2008/02/21 22:43 コメントありがとうございます。Lv.2 だと知ってしょぼーんとしてて返事が遅れました(嘘です

実は長すぎるし論点がボケるのでカットしたのですが,元の原稿では演習問題として uri_escape を再実装する,というのをやりました。で,やってみて思ったのは,仕様として明記するのはアリといえばアリなんですが,やはり他のモジュールとの整合性がとりづらくなるし,システムの仕様として「抜け」があるなぁ,と。
(miyagawa さんがおっしゃってるのと同じことですが)

use bytes や Juerd さんの考える BLOB,面白いですね。でもエンドユーザサイドの書き方も煩雑になりそう。あと,それらをモジュールサイドがサポートしているかどうか(既存のモジュールは未サポートなわけですし)というのをどう利用者に伝えるか,など難しいかなぁ,と。もちろんドキュメントで,というのはあたりまえですが,たとえば TT って透過的に様々なモジュール・メソッドを使えますけど,そういったリフレクション機構みたいなのにどう対応するか。って色々考えると結局プログラマに外部入出力か内部かを意識したほうがいいよ,と啓蒙するのがいいのか,とか。考えがまとまらないまま書いてるのでちょっとgdgdですが,また日を改めて考えてみたいです。

nobuokanobuoka 2008/03/11 21:15 こんにちは。”[Perl] Perl の Unicode 対応について” のエントリでトラックバックさせて頂きました nobuoka です。

内部表象 (内部形式: internal format) について気になる点があったのでいろいろ調べていたのですが、「内部形式は UTF-8 ではなく Unicode コードポイントをバイナリ化したものである」という結論に達しました。たとえば「é」という文字は内部形式では ¥xE9 というバイナリデータとして保持されているという結論に達しました。それは utf8 フラグが付いていても付いていなくても同様です。
つまり、このエントリで述べられている
(A) 文字列(内部表象: UTF-8)
(B) 文字列(内部表象: ISO-8859-1)
ですが、utf8 フラグが付いているかいないかの違いだけで内部的なデータは同じもの ((B) は U+0000 〜 U+00FF の範囲のみですが) だと思うのです。だからこそ eq 演算子の比較でもきちんと比較されるのではないか、と。

詳しいことは
http://www.r-definition.com/program/perl/internalformat.htm
に書いてますのでよければまた目を通してください。
一般的に内部形式は UTF-8 だと言われてますし perldoc にもそんなことを書いてるのであんまり自信がないのですが。。

dayflowerdayflower 2008/03/13 12:57 コメントありがとうございます>nobuoka さん

返信が長くなりそうだったので,別建ての記事にしました。
http://d.hatena.ne.jp/dayflower/20080313/1205380179

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証