正規化
準備ができたので、さっそく正規化の実験をしてみましょう。例として、「ページ」の「ペ」を取り上げます。
カタカナの「ヘ」にはいわゆる全角と半角の2種類があります。半濁点(マル)にはU+309a(KATAKANA-HIRAGANA SEMIVOICED SOUND MARK)とU+309c(COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK)、U+ff9f(HALFWIDTH KATAKANA SEMIVOICED SOUND MARK)の3種類があります(HiraganaやHalfwidth and Fullwidth Formsを参照)。
かなり特殊なものですが、CJK Compatibilityを見ると分かるように、1文字で「ページ」を表すU+333b(SQUARE PEEZI)もUnicodeには登録されています(個人的にはあまり使わない方がいいとは思いますが、一部(38文字。U+333bも含む)はJISにも登録されているので、「使うな」というわけにもいきません)。
ここは次のような8つの文字列を考えればよいでしょう。本稿執筆時点でのCodeZineの文字コードはいわゆるShift_JIS(正確にはWindows-31J)なので、U+309a (COMBINING KATAKANA-HIRAGANA SEMIVOICED SOUND MARK)を扱えません。そのため、U+309aに相当する部分は「●」としています(数値文字参照も使えないのです)。
文字(列) | 数値文字参照 | 補足 |
ペ | ペ | 最も標準的なもの |
ヘ● | ペ | 「ヘ」とCOMBINING KATAKANA-HIRAGANA SEMIVOICED SOUND MARK |
ヘ゜ | ヘ゜ | 「ヘ」とKATAKANA-HIRAGANA SEMI-VOICED SOUND MARK |
ペ | ペ | 半角の「ヘ」とHALFWIDTH KATAKANA SEMIVOICED SOUND MARK |
ヘ● | ペ | 半角の「ヘ」とCOMBINING KATAKANA-HIRAGANA SEMIVOICED SOUND MARK |
ヘ゜ | ヘ゜ | 半角の「ヘ」とKATAKANA-HIRAGANA SEMIVOICED SOUND MARK |
ペ | ペ | 「ヘ」とHALFWIDTH KATAKANA SEMIVOICED SOUND MARK |
㌻ | ㌻ | SQUARE PEEZI |
Unicodeのファイルにならそのまま書いてもいいのですが、目で見て区別しやすいように数値文字参照を使って定義します。
$tmp=array( 'ペ',//ペ 'ペ',//ヘ● 'ヘ゜',//ヘ゜ 'ペ',//ペ 'ペ',//ヘ● 'ヘ゜',//ヘ゜ 'ペ',//ペ '㌻'//㌻ ); $strs=array(); foreach($tmp as $s)//数値文字参照を文字列に変換 $strs[]=html_entity_decode($s,ENT_NOQUOTES,'UTF-8'); echo '<p>'; print_r($strs);//確認 //Array ( [0] => ペ [1] => ヘ● [2] => ヘ゜ [3] => ペ // [4] => ヘ● [5] => ヘ゜ [6] => ペ [7] => ㌻ ) echo '</p>';
実験用の文字列を配列に格納しました。
正規化の形式
一口に「正規化」と言っても、それには次の4つの形式があります。
- NFD(正規分解)
- NFC(正規分解とそれに続く正規合成)
- NFKD(互換分解)
- NFKC(互換分解とそれに続く正規合成)
先に定義したすべての文字列に対して、すべての正規化形式を試してみましょう。
$types=array('NFD','NFC','NFKD','NFKC');//正規化形式 $normalizer=new I18N_UnicodeNormalizer();//正規化用のオブジェクト foreach($strs as $str){//実験用の各文字列について、 echo "<p>「{$str}」 ";print_r(codePoints($str));echo ' の結果</p>'; echo '<table>'; foreach($types as $type){//各形式で、 $result=$normalizer->normalize($str,$type);//正規化する echo '<tr>'; echo "<td>$type</td>"; echo "<td>$result</td>"; echo '<td>';print_r(codePoints($result));echo '</td>'; echo "</tr>"; } echo '</table>'; }
正規分解と互換分解の違いは、「ペ」(U+ff8d,U+ff9f)の結果を見ると分かります。
NFD ペ Array ( [0] => 0000ff8d [1] => 0000ff9f ) NFC ペ Array ( [0] => 0000ff8d [1] => 0000ff9f ) NFKD ヘ● Array ( [0] => 000030d8 [1] => 0000309a ) NFKC ペ Array ( [0] => 000030da )
互換分解の際に、半角カナの「ヘ」は「ヘ」に、半角カナの半濁点「゚」は半濁点「●」に置き換えられています。正規分解ではこのようなことは起こりません。
他の結果も載せておきましょう。
「ペ」は「ヘ」(U+30d8)と「●」(U+309a)に分解することができます。
NFD ヘ● Array ( [0] => 000030d8 [1] => 0000309a ) NFC ペ Array ( [0] => 000030da ) NFKD ヘ● Array ( [0] => 000030d8 [1] => 0000309a ) NFKC ペ Array ( [0] => 000030da )
「ヘ」(U+30d8)と「●」(U+309a)は、合成すれば「ペ」になります。
NFD ヘ● Array ( [0] => 000030d8 [1] => 0000309a ) NFC ペ Array ( [0] => 000030da ) NFKD ヘ● Array ( [0] => 000030d8 [1] => 0000309a ) NFKC ペ Array ( [0] => 000030da )
「ヘ」(U+30d8)と「゜」(U+309c)は、合成しても「ペ」にはなりません。「゜」(U+309c)は半濁点そのものであって、他の文字と結合させて使うものではないからです。
NFD ヘ゜ Array ( [0] => 000030d8 [1] => 0000309c ) NFC ヘ゜ Array ( [0] => 000030d8 [1] => 0000309c ) NFKD ヘ ● Array ( [0] => 000030d8 [1] => 00000020 [2] => 0000309a ) NFKC ヘ ● Array ( [0] => 000030d8 [1] => 00000020 [2] => 0000309a )
半角カナの「ヘ」と半濁点でも、結合すれば「ペ」になります。
NFD ヘ● Array ( [0] => 0000ff8d [1] => 0000309a ) NFC ヘ● Array ( [0] => 0000ff8d [1] => 0000309a ) NFKD ヘ● Array ( [0] => 000030d8 [1] => 0000309a ) NFKC ペ Array ( [0] => 000030da )
「゜」(U+309c)ではうまくいかないのは、先の場合と同じです。
NFD ヘ゜ Array ( [0] => 0000ff8d [1] => 0000309c ) NFC ヘ゜ Array ( [0] => 0000ff8d [1] => 0000309c ) NFKD ヘ ● Array ( [0] => 000030d8 [1] => 00000020 [2] => 0000309a ) NFKC ヘ ● Array ( [0] => 000030d8 [1] => 00000020 [2] => 0000309a )
「ヘ」と半角カナの半濁点でも、結合すれば「ペ」になります。
NFD ペ Array ( [0] => 000030d8 [1] => 0000ff9f ) NFC ペ Array ( [0] => 000030d8 [1] => 0000ff9f ) NFKD ヘ● Array ( [0] => 000030d8 [1] => 0000309a ) NFKC ペ Array ( [0] => 000030da )
「㌻」でさえも、互換分解して結合すれば「ページ」になります。
NFD ㌻ Array ( [0] => 0000333b ) NFC ㌻ Array ( [0] => 0000333b ) NFKD ヘ●ーシ○ Array ( [0] => 000030d8 [1] => 0000309a [2] => 000030fc [3] => 000030b7 [4] => 00003099 ) NFKC ページ Array ( [0] => 000030da [1] => 000030fc [2] => 000030b8 )
ここではすべてを「ペ」(U+30da)に統一したかったのですが、NFKCで正規化すれば目的が達せられることが分かります。
おわりに
Unicodeにおいては、意味的には同じ文字を複数の方法で表現することができます。表現がバラバラなままだと、検索などにおいて問題が発生することは容易に想像できます。そのため、表記を統一する仕組みが必要になりますが、それが正規化です。
PHPにおいては、PEAR I18N_UnicodeNormalizerを用いることで簡単に正規化を行うことができます。本稿では、実際に複数の方法で表現した文字を、正規化によって統一する方法を紹介しました。
PHPの将来のバージョンには、正規化のためのクラス(Normalizer)が組み込まれる予定です。そうなれば、本稿で紹介したPEAR I18N_UnicodeNormalizerをあえて使う必要はなくなるでしょう(その際にはぜひPEAR I18N_UnicodeNormalizerはdeprecatedだと明言してほしいものです)。しかし、CVS版などを無理に持ってこない限りNormalizerが使えない現段階では、既に仕様が固まっているPEAR I18N_UnicodeNormalizerを使いましょう。