はじめに
与えられた文字列を含む文書を返す検索機能を実装しているところを想像してください。
検索語として「ページ」が与えられれば、「ページ」という文字列を含む文書を返します。これは特に難しいことではありません。
半角の「ページ」が与えられたらどうでしょう。「ページ」と「ページ」を区別する必要がないような、一般的な文書検索においては、「ページ」という文字列を含む文書を返すのが望ましいはずです(もちろん、この2つは常に同一視できるわけではありません。同一視できない例として本稿があります)。
もしかしたら、「㌻」で検索しようとする人がいるかもしれませんし、日本語を母国語としない人が、「ぺ」(「ヘ」と半角の半濁点「゚」)や「ヘ゜」(半角カナ「ヘ」と半濁点「゜」)を使うかもしれません。
人間なら簡単に対応できることですが、コンピュータで対応するには特別な処理が必要になります。例えばUnicodeにおいては、人間にとっては同じとみなせるような文字が複数登録され、それぞれに番号が振られています。Unicodeを使うシステムは、文字列を数字の列として扱うことがほとんどで、異なる数字列で表現された文字列が実は同じものなのだというような処理は、普通は行われないのです。このような問題は、日本語の濁点・半濁点や全角・半角以外にも、アクセントなどの修飾の付いた文字において起こりえます。
異なる方法で表現された文字列を同一視するために、Unicodeでは「正規化」という仕組みが用意されています(Unicode Normalization Formsを参照)。半角の「ページ」や「㌻」を「ページ」と同一視することができるのです。
正規化機能は複雑な処理ですが、多くのプログラミング言語であらかじめ用意されています。PHPならPEAR I18N_UnicodeNormalizer、.NET Framework 2.0以降ならString.Normalizeメソッド、Java 1.6以降ならjava.text.Normalizerによって正規化が可能です。
本稿では、PEAR I18N_UnicodeNormalizerを使ってPHPで正規化を行う方法を紹介します。
対象読者
- PHPの基本的な文法を知っている方
必要な環境
PHPにPEAR I18N_UnicodeNormalizer 1.0.0をインストールした環境で動作を確認しました(インストール方法は後述)。
用語
「全角」や「半角」という用語は、文字を表示する際に、その枠の幅が狭いものと広いものの2種類しかなかった時代のなごりです。広いものは狭いものの2倍の幅があったので、広いもので表示された文字は「全角文字」、狭い幅で表示された文字は「半角文字」と呼ばれました。この呼び名は今でも残っていますが(Halfwidth and Fullwidth Formsなどを参照)、文字枠の幅とは無関係です。本稿では、Unicodeの名前の付け方で呼び分けることにします。たとえば、ASCIIの「A」は単に「A」と呼びますが、「いわゆる全角のA」は「全角のA」と呼びます。同様に、カタカナの「ペ」(U+30da)は単に「ペ」と呼んで、「いわゆる半角のペ」を「半角のペ」と呼んでいます。
半角文字を「1バイト文字」、全角文字を「2バイト文字」呼ぶことがありますが、この用語は使うべきではありません(本稿でも使いません)。半角文字が1バイト、全角文字が2バイトで表現されるのは、Shift_JISなどの一部の文字コードにおいてだけだからです。例えばUTF-8においては、半角カナのすべての文字や漢字の大部分は3バイトです(4バイト必要な漢字もあります)。
準備
コマンドプロンプトから次のようにI18N_UnicodeNormalizerをインストールします。
なお、Windows VistaとXAMPPで環境構築した場合は、コマンドの実行前に「c:\xampp\php\pear.ini」の「"\xampp」を「"C:\xampp」に修正しておいてください。
c: cd \xampp\php pear install I18N_UnicodeNormalizer
PHPのファイルはUTF-8で作成し、次のように文字コードをUTF-8にしておきましょう。
mb_internal_encoding('UTF-8');
数値文字参照
本稿で扱う文字はUTF-8のファイルならそのまま記述できるのですが、画面では違いが分かりにくいものがあるので、数値文字参照と使うことにしましょう。例えば、コードポイントが30daの文字つまり「ペ」(U+30da)は、ペ
(16進表記)やペ
(10進表記)と書くことができます。
数値文字参照で表した1文字は、ブラウザ上では正しく表示されるのですが、PHPの内部では1文字として扱われません。長さを調べると分かります。
$str='ペ'; echo "<p>「{$str}」の長さは".mb_strlen($str).'</p>';//「ペ」の長さは8
正しく扱うために、関数html_entity_decode
を使って文字列に変換します。この関数には、文字列とシングルおよびダブルクォートの扱い方、文字コードを引数として与えます(今はクォート文字はどうでもいいので、ENT_NOQUOTES
として変換しないようにしています)。
$str=html_entity_decode('ペ',ENT_NOQUOTES,'UTF-8'); echo "<p>「{$str}」の長さは".mb_strlen($str).'</p>';//「ペ」の長さは1
関数html_entity_decode
とは逆に、文字列を構成する各文字のコードポイントを知るための関数codePoints
を用意しておきましょう。文字列を与えると、各文字のコードポイントの配列を返します(文字コードの変換に関数mb_convert_encoding
を使っています。関数iconv
でも文字コードを変換できますが、その際には、変換後にU+feffという画面には表示されない文字が勝手に挿入されてしまうので注意してください。$i=0
ではなく$i=1
でfor
ループを開始するといいでしょう)。
function codePoints($utf8_str){ //入力をUTF-32に変換 $utf32_str=mb_convert_encoding($utf8_str,'UTF-32','UTF-8'); for($i=0;$i<mb_strlen($utf32_str,'UTF-32');++$i) //1文字ずつ16進文字列に変換 $result[]=bin2hex(mb_substr($utf32_str,$i,1,'UTF-32')); return $result; } echo '<p>'; print_r(codePoints('ページ')); //Array ( [0] => 000030da [1] => 000030fc [2] => 000030b8 ) echo '</p>';
{ペ, ー, ジ}は{U+30da, U+30fc, U+30b8}であることが分かります。