UTF8 フラグあれこれ

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

まとめ

  • スカラー変数の内部表象の状態を示すものとして UTF8 フラグというものがある
  • スカラー変数は(リファレンス等は別として)下記のものを格納できる
    • (A) 文字列(内部表象: UTF-8
    • (B) 文字列(内部表象: ISO-8859-1)
    • (C) バイナリ列
      1. 純粋なバイナリストリーム(画像ファイル等)かもしれないし,
      2. UTF-8 octet stream かもしれないし,
      3. CP932 octet stream かもしれないし,etc, etc ...
  • 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 "UTF8 flag"?

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

ざっくり要約すると

  • 一般人は文字列の内部表象や UTF8 フラグについては知る必要はありません><
  • 文字列の UTF8 フラグが立っているとき,内部表象は UTF-8エンコードされている
  • 文字列の UTF8 フラグが立っていないとき,内部表象は ISO-8859-1 でエンコードされている,と「見做される」
  • つまり,Perl の「文字列」の内部表象には以下の 2 つがありうる

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 Point UTF-8 representation
é U+00E9 c3-a9
U+6F22 e6-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 フラグつき文字列のみ扱う
    • ASCII 範囲や ISO-8859-1 範囲の文字列についても Encode モジュール等で UTF8 フラグつきにできますから
  • 入出力時にエンコーディングを変換する
  • (今回の知見から)既存モジュールとやりとりするときも,仕様に沿ってよしなに変換する

でやってます。

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

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

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