PHP 半角/全角 変換 mb_convert_kana()の濁点半濁点処理の罠について

Pocket

PHPで、文字列を半角/全角相互に変換する関数といえば、mb_convert_kana()ですが、先日濁点、半濁点の扱いについて勘違いしていたために、文字列の正規化処理(一定の規則で変換する処理。入力情報のゆらぎ解消などに行う)の実装に少し手間取ってしまったポイントが有り、その注意点について紹介したいと思います。

独立濁点、半濁点をmb_convert_kana()で扱う際の注意

mb_convert_kana()のオプションには、”V”オプションというものがあります。
マニュアルには以下のようにあります。

V 濁点付きの文字を一文字に変換します。”K”, “H” と共に使用します

ここでいう”K”, “H”のオプションは何かというと、以下のようになりますが、大抵の場合、Kオプションを用いるかと思います。

K 「半角カタカナ」を「全角カタカナ」に変換します。
H 「半角カタカナ」を「全角ひらがな」に変換します。

よくあるケースとしては、フォームのテキスト入力で送信された文字列を、全て全角カタカナに揃えるなどの処理です。
人の名前や会社名などを収集してデータベース化しているケースなどでは、表記が半角と全角で揺れがあったりすると、後日の集計で困るということもあって、正規化処理として、半角を全角に全て変換するであったり、その逆の処理を行うといった実装をすることが多いと思います。

さて、ここで見落としがちなのが、濁点、半濁点の扱いになります。
それぞれ注意事項を先に挙げます。

mb_convert_kana()関数のVオプションの意味をしっかりと理解しよう

先のマニュアルの引用にあるとおり、VオプションはK、またはHと共に利用するのですが、それはつまり、以下のようなコードなります。

$ cat test.php 
<?php
$str1 = 'バイオリン';
echo "KV: {$str1} => ". mb_convert_kana($str1, 'KV');
echo PHP_EOL;
echo "HV: {$str1} => ". mb_convert_kana($str1, 'HV');
echo PHP_EOL;
$ 
$ php test.php 
KV: バイオリン => バイオリン
HV: バイオリン => ばいおりん

これは、半角カタカナ+濁点(半濁点)を一文字の全角文字に変換してくれるオプションということです。
半角カタカナの場合は、濁点付きの一文字が存在しないために、2文字になってしまいますが、全角の場合は、濁点付き文字が1文字で表現できるために、こうしたオプションが存在しています。
ちなみに、Vオプションを指定しないと、以下のような不格好な状態になります。

K: バイオリン => ハ゛イオリン
H: バイオリン => は゛いおりん

Vオプションは全角カタカナ+濁点を1文字にはしません

そうです。Vオプション「半角カタカナ」+「濁点」を一文字にするのであって、「全角カタカナ」+「濁点」を1文字にはしません。

$ cat test2.php 
<?php

$str1 = 'ハ゛';
echo "KV: {$str1} => ". mb_convert_kana($str1, 'KV') =;
echo PHP_EOL;
$
$ php test2.php 
KV: ハ゛ => ハ゛

もし文字列の中に、「全角カタカナ」+「濁点」があるようであれば、これをreplaceする処理をしなくてはなりません。

mb_convert_kana()の異体字に気をつけよう

前述の「全角カタカナ」+「濁点」を置換する処理について、これを1文字にしないのなら、一旦全てを半角にしてから、全角にしてばいいだろうとのことで、一度以下のようなコードを書きました。

<?php

$str1 = 'ハ゛';
// 一旦半角に
$str2 = mb_convert_kana($str1, 'k');;
echo "k: {$str1} => ". $str2;
echo PHP_EOL;
echo "KV: {$str2} => ". mb_convert_kana($str2, 'KV');
echo PHP_EOL;

$ php test.php 
k: ハ゛ => バ
KV: バ => バ

見事に動作しているように思われます。
しかし、全角を半角にする”k”オプションは、実は以下のように、旧カナのような異体字を新カナに変換してしまうので、全角->半角->全角という処理は避けなくてはなりません。

$ cat test.php 
<?php

$str1 = 'ヱヒ゛ス';
// 一旦半角に
$str2 = mb_convert_kana($str1, 'k');;
echo "k: {$str1} => ". $str2;
echo PHP_EOL;
echo "KV: {$str2} => ". mb_convert_kana($str2, 'KV');
echo PHP_EOL;


$ php test.php 
k: ヱヒ゛ス => エビス
KV: エビス => エビス

半角の独立濁点、半濁点に気をつけましょう

mb_convert_kana()の機能自体とは直接関係ないのですが、実は一番濁点の処理で理解が出来ずに、時間がかかってしまったのが、「全角カタカナ」+「半角濁点(半角半濁点)」というケースです。

例えば、「ガ」と「ガ」という字ですが、モニター上では同一に見えると思いますが、実は前者は1文字、後者は2文字で構成されています。

この仕様を全く知らなかったのですが、通常日本語の表示処理では「全角カタカナ」+「半角独立濁点」という2文字を、電子情報では2文字扱いでも、表示上では1文字で扱うという仕様が存在するのです。

実際にコードを実行してみると、以下のように後者が2文字であることがわかります!!
びっくりですね。

$ cat test.php 
<?php

$char1 = 'ガ';
$char2 = 'ガ';
echo mb_strlen($char1);
echo PHP_EOL;
echo mb_strlen($char2);
echo PHP_EOL;

$ php test.php 
1
2

PHPでカタカナ+独立濁点、独立半角濁点を正規化する処理

最後に、私がたどり着いた変換処理について記載しておきます。コードの中に@seeで書いているURLがこの問題に対処する中で参考にさせていただいたURLとなります。

preg_replace_callback関数を用いたり、文字コード表との関連を用いたほうがよりスマートに記述できるかもしれませんが、動けばいいとの判断で、以下のような処理に落ち着きました。

    /*
     * 独立濁点、半濁点問題
     * @see http://www.mnhr.net/reports/index.php/20110530
     * @see http://d.hatena.ne.jp/hiko_s/20160606/p1
     */
    public function normalizeIndependentVoicedPoint($value)
    {
        $str1 = "ガギグゲゴザジズゼゾダヂヅデドバビブベボヴ";
        $str2 = "カキクケコサシスセソタチツテトハヒフヘホウ";
        $str1s = preg_split('//u', $str1, -1, PREG_SPLIT_NO_EMPTY);
        $str2s = preg_split('//u', $str2, -1, PREG_SPLIT_NO_EMPTY);
        for ($i = 0; $i < count($str2s); $i++) {
            $hexStrs = str_split(bin2hex($str2s[$i]), 2);
            $hexStr = '\x'. implode('\x', $hexStrs);
            $value = preg_replace('/('. $hexStr.'\xe3\x82[\x99\x9b])/', $str1s[$i], $value);
        }
        $str1 = "パピプペポ";
        $str2 = "ハヒフヘホ";
        $str1s = preg_split('//u', $str1, -1, PREG_SPLIT_NO_EMPTY);
        $str2s = preg_split('//u', $str2, -1, PREG_SPLIT_NO_EMPTY);
        for ($i = 0; $i < count($str2s); $i++) {
            $hexStrs = str_split(bin2hex($str2s[$i]), 2);
            $hexStr = '\x'. implode('\x', $hexStrs);
            $value = preg_replace('/('. $hexStr.'\xe3\x82[\x9a\x9c])/', $str1s[$i], $value);
        }
        return $value;
    }

それではみなさま良いPHPライフを。

Pocket