ICU: Unicode正規化 (icu::Normalizer2) [c++]

(2005.4.30 この節を追加。)

(2018.9) ページ分割、最新化.

Unicode における正規化とは

Unicodeは、既存の多くの文字集合の文字を収録しているため、複数の異なったコード列が同じ文字を表す場合がある。これらをそのまま扱うのは検索などで困難が多いので、正規化 (Normalization) して, 特定のコード列に揃える。

Unicodeでの正規化には、NFD, NFKD, NFC, NFKCの4種類がある。

正規化は, まず, 文字を一定のルールで分解する. 2種類のルールがあり, 正規分解 Canonical Decomposition は文字を変えることなく分解する。互換分解 Compatibility Decomposition は意味が同じならより基本的な文字に変更して分解する。

ここで止めると, 正規化形式 NFD (正規分解) か NFKD (互換分解) になる。

NFC/NFKC は, NFD/NFKD で正規化したうえで, 正規結合 Canonical Composition で結合する。

Unicode 正規化のルールは、こちらで定義されている;

実用では、通常は NFC で正規化する。例えば、ユーザから入力やペーストされた文字列を, 正規化したうえでデータベースに保存するなど。検索キーになるフィールドは, NFKC もよく使う。

(2018.9) さらに, 大文字・小文字を区別しない場合は, NFKC_Casefold もある。

例えば、U+0041 U+030Aというコード列をNFCまたはNFKCで正規化すると次のようになる。NFD/NFKDは逆向きの変換を行う。

A COMBINING RING ABOVE C/KC
Å
0041 030A 00c5

ひらがなでも同様。濁音を結合するか。

COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK C/KC
304b 3099 304c

NFKC / NFKD は、互換分解 Compatibility Decomposition を行う。互換分解は、互換文字をより基本的な文字に置換するほかに、丸付き文字などを展開する。

KC/KD
1
2460 0031
KC/KD
32a4 4e0a
KC/KD
3336 30d8 30af 30bf 30fc 30eb

互換漢字と variant

(2018.9 追加)

互換漢字は, 正規化形式が何であれ (NFKC/NFKDだけでなく), 統合漢字に変換される。しかし、正規化しつつグリフを区別したいという需要がある。人名など。

fa30 4fae

Unicode 10.0 の NormalizationTest.txt ファイルはこうなっている。どの正規化形式であれ、変換されることが分かる。

FA30;4FAE;4FAE;4FAE;4FAE; # (侮; 侮; 侮; 侮; 侮; ) CJK COMPATIBILITY IDEOGRAPH-FA30

Unicode 6.3 で, 互換漢字に対して Variation Selector (VS) が振られた。StandardizedVariants.txt には次の行がある. これにより 4FAE FE00 という列で, U+FA30 と同じグリフが表示されることが期待できる。

4FAE FE00; CJK COMPATIBILITY IDEOGRAPH-FA30;
4FAE FE01; CJK COMPATIBILITY IDEOGRAPH-2F805;

対応しているアプリケーションとフォントを組み合わせれば、確かにグリフの出し分けができる. この二文字は、両方とも U+4FAE で, 片方に VS を付けている;

しかしながら, U+FA30 を正規化したときに 4FAE FE00 になるわけではないようだ。NormalizationTest.txt ファイルを眺めると, 正規化で複数の Code Point に展開 (分解) しても構わないが, 互換漢字はそのように定められていない.

正規化サンプル

ICU で正規化するプログラムを書いてみる。Fedora 28 Linux には, ICU 60.2 のパッケージがある。このバージョンは Unicode 10.0 に対応する.

文字列全体を単に正規化するのは, 難しくない。normalize() などのメソッドを呼び出すだけ。以下では, ファイルを読み込みながら正規化したりできるように、少しずつ正規化するケースを作る。

しかし作ってから思ったが, 正規化するようなケースは極端に長いデータは考えにくく, 逆に短いなら文字列オブジェクトを作っても問題なく, あまり意味がなかった。

icu::Normalizer2クラス

新しいプログラムでは, icu::Normalizer2 を使おう。ICU 4.4 で安定し、ICU 49 で拡充された。

C++
[RAW]
  1. #include <stdio.h>
  2. #include <assert.h>
  3. #include <unicode/normalizer2.h>
  4. #include <unicode/uclean.h> // u_cleanup()
  5. void dump(const UnicodeString& str)
  6. {
  7. printf("len=%d ", str.length());
  8. for ( int i = 0; i < str.length(); i = str.moveIndex32(i, +1) ) {
  9. UChar32 uc = str.char32At(i);
  10. printf("%x ", uc);
  11. }
  12. printf("\n");
  13. }
  14. static void norm_seg( const UnicodeString& seg, const icu::Normalizer2* normzr )
  15. {
  16. assert(normzr);
  17. UErrorCode errc = U_ZERO_ERROR; // 初期化重要
  18. UNormalizationCheckResult check = normzr->quickCheck(seg, errc);
  19. printf(" len=%d quick=%d quickYes=%d ",
  20. seg.length(), // サロゲートペアだと, 1 code point でも len = 2
  21. check,
  22. normzr->spanQuickCheckYes(seg, errc) );
  23. // 文字列に追加するなら normalizeSecondAndAppend() と組み合わせる.
  24. if (check == UNORM_YES)
  25. dump(seg);
  26. else {
  27. UnicodeString dest;
  28. normzr->normalize(seg, dest, errc);
  29. dump(dest);
  30. }
  31. //delete normzr; これは invalid pointer になる.
  32. }
  33. int main()
  34. {
  35. UErrorCode errc = U_ZERO_ERROR; // 初期化重要
  36. u_init(&errc);
  37. UnicodeString str = "あX㌶a\u0308\u0323か\u3099e\u0304\u0301\u0323\ufa30𠮟𩸽";
  38. errc = U_ZERO_ERROR;
  39. const icu::Normalizer2* normzr = icu::Normalizer2::getNFKCInstance(errc);
  40. assert(normzr);
  41. // spanQuickCheckYes() は 先頭から quickCheck() == YES となる範囲の長さを
  42. // 返す.
  43. printf("quickCheck = %d\n", normzr->spanQuickCheckYes(str, errc) ); //=>2
  44. int start = 0;
  45. int i;
  46. // i++ ではサロゲートペアで不味い.
  47. for ( i = 0; i < str.length(); i = str.moveIndex32(i, +1) ) {
  48. // より効率的にいくなら, spanQuickCheckYes() の長さ分だけ進めて, 進めら
  49. // れない場合に, 以下のコードで正規化する.
  50. UChar32 uc = str.char32At(i);
  51. // hasBoundaryBefore() は基底文字のとき true
  52. // 結合文字がどれだけ続くかは、読んでみないと分からない.
  53. if ( normzr->hasBoundaryBefore(uc) && i - start > 0 ) {
  54. // 基底文字の直前までを処理する.
  55. const UnicodeString& seg = str.tempSubString(start, i - start);
  56. norm_seg(seg, normzr);
  57. start = i;
  58. }
  59. printf("%d: %x\n", i, uc);
  60. }
  61. // 最後のセグメントを忘れずに.
  62. if ( i - start > 0 ) {
  63. const UnicodeString& seg = str.tempSubString(start, i - start);
  64. norm_seg(seg, normzr);
  65. }
  66. // delete normzr; これも invalid pointer になる.
  67. u_cleanup(); // 必須
  68. return 0;
  69. }

Unicode文字列は基底文字の後ろにいくらでも結合文字が続きうる。したがって, 基底文字が出てきたらその直前までを正規化する, の繰り返しで、少しずつ正規化できる。上のサンプルでは norm_seg() が1文字だけ正規化する.

hasBoundaryBefore() は引数の code point が基底文字のときに真を返す。ここが区切りになる。

quickCheck()UNORM_YES を返すような文字列は、正規化済みなので、そのまま利用できる。

実行結果:

quickCheck = 2
0: 3042
    len=1 quick=1 quickYes=1 len=1 3042 
1: 58
    len=1 quick=1 quickYes=1 len=1 58 
2: 3336
    len=1 quick=0 quickYes=0 len=5 30d8 30af 30bf 30fc 30eb 
3: 61
4: 308
5: 323
    len=3 quick=0 quickYes=0 len=2 1ea1 308 
6: 304b
7: 3099
    len=2 quick=2 quickYes=0 len=1 304c 
8: 65
9: 304
10: 301
11: 323
    len=4 quick=0 quickYes=0 len=3 1eb9 304 301 
12: fa30
    len=1 quick=0 quickYes=0 len=1 4fae 
13: 20b9f
    len=2 quick=1 quickYes=2 len=2 20b9f 
15: 29e3d
    len=2 quick=1 quickYes=2 len=2 29e3d 

icu::Normalizerクラス

icu::Normalizer クラスは古い。ICU 56 で非推奨 (deprecated) になった。

下のサンプルでは icu::Normalizer#first()next() で少しずつ正規化しているように見えるが, UCharCharacterIterator クラスが確定した長さの文字列を取るため, 実際にはそうではない。

本当に不定長の文字列データを少しずつ正規化するには, CharacterIterator 抽象クラスをがっつり実装しなければならず, かなり大変そう。

C++
[RAW]
  1. #include <stdio.h>
  2. #include <assert.h>
  3. #include <unicode/normlzr.h>
  4. #include <unicode/schriter.h> // StringCharacterIterator
  5. // 直接 CharacterIterator から派生させるのは大変
  6. class MyChIter: public UCharCharacterIterator
  7. {
  8. typedef UCharCharacterIterator super;
  9. public:
  10. MyChIter(const UnicodeString& str): super(str.getBuffer(), str.length()),
  11. m_str(str), m_pos(0), m_end(str.length()) { }
  12. virtual ~MyChIter() { }
  13. // 必要に応じて override する.
  14. private:
  15. const UnicodeString m_str;
  16. int m_pos;
  17. const int m_end;
  18. };
  19. int main()
  20. {
  21. // ◆合字
  22. // ヘクタール NFC ではママ
  23. // ◆結合文字列 combining sequence <=> 合成済み文字 precomposed character
  24. // a, U+0308 (DIAERESIS), U+0323 (DOT BELOW)
  25. // => 1EA1 'LATIN SMALL LETTER A WITH DOT BELOW' 0308
  26. // か, U+3099 (濁点) => 304C
  27. // e, U+0304 (MACRON), U+0301 (ACUTE), U+0323 (DOT BELOW)
  28. // => 1EB9 'LATIN SMALL LETTER E WITH DOT BELOW' 0304 0301
  29. // 0304 0301 をスキップして結合してることに注目!
  30. // ◆互換漢字
  31. // 侮 U+FA30 => どの正規化形式でも変換される.
  32. UnicodeString str = "㌶a\u0308\u0323か\u3099e\u0304\u0301\u0323\ufa30𠮟𩸽";
  33. // もちろん単に文字列を渡すコンストラクタもあるが, イテレータを試す.
  34. // CharacterIterator インスタンスを渡す.
  35. icu::Normalizer norm(MyChIter(str), UNORM_NFC);
  36. // サロゲートペアになる範囲も, 二つにならず, 32bit 単位で得られる.
  37. for (UChar32 uc = norm.first(); uc != norm.DONE; uc = norm.next() )
  38. printf("%x ", uc);
  39. printf("\n");
  40. return 0;
  41. }

実行結果:

3336 1ea1 308 304c 1eb9 304 301 4fae 20b9f 29e3d 

Normalizer のコンストラクタに正規化方法を渡す。

UNORM_NFD 正規分解
UNORM_NFKD 互換分解
UNORM_NFC 正規分解したうえで正規結合
UNORM_NFKC 互換分解したうえで正規結合

リンク

Unicode正規化 もっとも詳しい解説。