Perl Perl_5
Perl Unicode 02 正規化, NFD, NFC, NFKD, NFKC (0x284)
目次 - Perl Index
Theme
Perl について、復習を兼ねて断片的な情報を掲載して行く連載その 0x284 回。
Perl で、「 Unicode 」を扱うための基本的な知識を確認する。その 02。
今回は、Unicode の正規化形式と書記素を正規化する ( Normalization ) Perl モジュール「 Unicode::Normalize 」による正規化の結果を確認します。
NOTE: このブログでは、「 初めての Perl 第 6 版 」の「 付録 C 」に沿って Unicode の確認を進めているので、用語の利用方法等が正式な Unicode の用法と異なる場合があります。
より正確な Unicode の情報は 使いこなそうユニコード や Unicode Technical Reports 等を参照します。
Unicode の正規化
(0x283) で確認した通り、Unicode ではある書記素を表現するために、非マーク文字とマーク文字を組み合わせて表現する方法「 decomposed 」と予め 1 つの文字 ( atom ) として定義された文字を利用する方法「 composed 」があります。
例えばそれは、「 é 」という書記素で、「 e 」( U+0065 ) と「 ́ 」( U+0301 ) を組み合わせた「 decomposed 」と、「 é 」( U+00E9 ) 単体の「 composed 」で表現するものがあります。
これらは、書記素としては同じ意味を表現しますが内部的には異なるものとして扱われます。
例として、コードポイントでソートする関数「 sort 」での扱いを見てみます。
まず、composed と decomposed の書記素を用意して他の文字列と共に配列に格納します。
# é
my $composed = "\x{00E9}";
# é
my $decomposed = "\x{0065}\x{0301}";
# é é b d c f a
my @str = ($composed, $decomposed, qw(b d c f a));
# ソート
my @sorted = sort @str;
配列「 @chrs 」には 7 つの要素が「 é é b d c f a 」の順序で格納されることになりますが、この配列をソートした結果は次のようになります。
a b c d é f é
同じに見える「 é 」の位置が異なっています。
なぜなら、「 decomposed 」はコードポイント「 U+0065 」で評価され「 composed 」はコードポイント「 U+00E9 」で評価されるからです。
Unicode には、「 ligature 」( 合字 ) 等も含めて同じ表現でありながら内部的に扱いが異なる書記素が混在していますが、そのままでは上記のように意図しない結果が発生する場合があります。
これを解決するために、Unicode では文字の扱いを統一するための手法として「 正規化 」( nomalization ) が用意されています。
4 つの「 正規化形式 」
Unicode の正規化には次の 4 つの「 正規化形式 」( nomalization forms ) があります。
Normalization Froms Decomposition ( NFD : D : 正規化形式 正準分解 )
Normalization Forms Composition ( NFC : C : 正規化形式 正準分解 -> 正準合成 )
Normalization Forms Kompatibility Decomposition ( NFKD : KD : 正規化形式 互換分解 )
Normalization Forms Kompatibility Compostion ( NFKC : KC : 正規化形式 互換分解 -> 正準合成 )
NOTE: Kompatibility は Compatibility で Compostion の C と区別をつけるために s/C/K/ ( C を K に置換 ) しています。
「 NFD 」は「 正準分解 」( canonical decomposition )、「 NFKD 」は「 互換分解 」( k(c)ompatibility decomposition ) で、いずれも分解 ( decomposition ) して正規化します。
これに対して「 NFC 」は「 正準分解 」した後に「 正準合成 」( Canonical composition ) することで、「 NFKC 」は「 互換分解 」した後で「 正準合成 」することで正規化します。
なお、「 NFKD 」および「 NFKC 」は、必ずしも再合成出来るわけではない「 互換分解 」( k(c)ompatibility decomposition ) を用いるため、元の書記素がもっていた表現が壊れる場合があること ( fi と fi 等 ) に留意する必要があります。
「 正準分解 」( canonical decomposition ) によってコードポイントが変更になる書記素はそれほど多くありませんが、例えば、コードポイント「 U+1FFD 」の文字「 ´ 」は、「 正準分解 」によって「 U+00B4 」の「 ´ 」に *分解* されて正規化されます。
また、コードポイント「 U+1F71 」の「 Ά 」も「 正準分解 」によって「 U+0386 」の「 Á 」に正規化されます。
各正規化によるコードポイントの変化は Normalization Chars - unicode.org にまとめられています。
モジュール「 Unicode::Normalize 」による正規化
Perl で Unicode の正規化を行うには、モジュール「 Unicode::Normalize 」が有用だといわれています。
モジュール「 Unicode::Normalize 」には関数「 NFD 」, 「 NFC 」, 「 NFKD 」, 「 NFKC 」が用意されているので、これらの関数を利用して任意の書記素を任意の形式に正規化することが出来ます。
「 é 」を例にして Unicode の正規化を確認しますが、確認に際しては Unicode 正規化 - ABEKAWA Takeshi での例示を参考にした次のサブルーチン「 output 」を利用しています。
無名のハッシュリファレンスや関数「 unpack 」等このブログではまだ確認していない機能も利用していますが、ひとまずこの関数に任意の書記素を渡せば「 NFD 」, 「 NFC 」, 「 NFKD 」, 「 NFKC 」の各形式で正規化された文字列と正規化された際のコードポイントを出力します。
ちなみに、6 行目のハッシュスライスを利用した key/value のセッティングは (0x26e) で確認しました。
sub output {
my $str = shift;
my @nf_name = qw(RAW NFD NFC NFKD NfKC);
my @nf_value = ($str, NFD($str), NFC($str), NFKD($str), NFKC($str));
my $nf = {};
@${nf}{ @nf_name } = @nf_value;
map {
print "$_ :\t$nf->{$_}\t";
my @codes = map {
sprintf "%04x", unpack("U", $_)
} split //, $nf->{$_};
print join(',', @codes), "\n";
} sort (keys %$nf);
}
「 é 」はこれまで確認してきた通り、「 U+00E9 」単体のものと「 U+0065 and U+0301 」のものが混在しているので、まずはこれらの書記素を次のように定義します。
# é
my $composed = "\x{00E9}";
# é
my $decomposed = "\x{0065}\x{0301}";
次に、サブルーチン「 output 」にそれぞれの変数を渡します。
binmode STDOUT, ':utf8';
output($composed);
print "---\n";
output($decomposed);
print "---\n";
それからこのプログラムを実行すると、次の結果が得られます。
NFC : é 00e9
NFD : é 0065,0301
NFKD : é 0065,0301
NfKC : é 00e9
RAW : é 00e9
---
NFC : é 00e9
NFD : é 0065,0301
NFKD : é 0065,0301
NfKC : é 00e9
RAW : é 0065,0301
---
「 RAW 」の行がそれぞれの元のコードポイントです。
「 合成 」( composition ) を行う「 NFC 」と「 NFKC 」では 1 つのコードポイント「 U+00E9 」で正規化されています。
「 分解 」( decomposition ) を行う「 NFD 」と「 NFKD 」では 2 つのコードポイント「 U+0065 」と「 U+0301 」で正規化されています。
このように、いずれかの正規化形式を利用して文字列をあらかじめ正規化しておけば、関数「 sort 」等で統一した結果が得られます。
統一したソート
例えば、「 NFD 」( Normalization Froms Decomposition ) を利用した正規化でいくつかの書記素をソートしてみます。
binmode STDOUT, ':utf8';
my @str = qw(ä é fi ä fi ä é);
print "str : @str\n";
my @sorted = sort @str;
print "sorted : @sorted\n";
my @nfd = map { NFD( $_ ) } @str;
my @srt_nfd = sort @nfd;
print "sorted NFD : @srt_nfd\n";
上記コードを実行すると次の結果が得られます。
str : ä é fi ä fi ä é
sorted : ä é fi ä ä é fi
sorted NFD : ä ä ä é é fi fi
正規化せずにソートした「 sorted 」では、分解された ( decomposed ) 文字を結合している書記素と予め合成された ( composed ) 書記素の位置が異なりますが、「 NFD 」で正規化した後のソートでは位置が統一されています。
同じコードを利用しつつ正規化を「 NFC 」で行うと、コードポイントが上記と異なるためソートの結果も違うものになりますが、各書記素の位置は統一されます。
sorted NFD : ä ä ä é é fi fi
sorted NFC : fi ä ä ä é é fi
0x284 -> 0x285 へ
Unicode の正規化については 文字コード地獄秘話 第3話:後戻りの効かないUnicode正規化 - tech.albert2005.co.jp 等でより詳しく解説されています。
次回は、Perl で「 Unicode 」を正規化するモジュール「 Unicode::Normalize 」を確認するために、「 FCD 」( Fast C or D ) のドキュメントを *自家用* に翻訳します。
参考情報は書籍「 初めての Perl 第 6 版 」を中心に perldoc, Wikipedia および各 Web サイト。それと詳しい先輩。
目次 - Perl Index
Perl mp2 翻訳 Web コンテンツ圧縮の FAQ (d228)
Perl mp2 翻訳 既知のブラウザのバグの回避策をいくつか (d227)
Perl mp2 翻訳 Perl と Apache でのキュートなトリック (d226)
Perl mp2 翻訳 テンプレートシステムの選択 (d225)
Perl mp2 翻訳 大規模 E コマースサイトの構築 (d224)
Perl mp2 翻訳 チュートリアル (d223)
Perl mp2 翻訳 既知のブラウザのバグの回避策をいくつか (d227)
Perl mp2 翻訳 Perl と Apache でのキュートなトリック (d226)
Perl mp2 翻訳 テンプレートシステムの選択 (d225)
Perl mp2 翻訳 大規模 E コマースサイトの構築 (d224)
Perl mp2 翻訳 チュートリアル (d223)