PHP Tips | 文字列処理:実務向け便利系 - Unicode 正規化

PHP PHP
スポンサーリンク

なぜ「Unicode 正規化」が実務で問題になるのか

同じ「見た目」の文字でも、内部的には“別の文字列”として扱われてしまうことがあります。
これが Unicode 正規化の話です。

例えば、次の 2 つは、見た目はどちらも「ガ」です。

  • 「ガ」1 文字
  • 「カ」+「゛」の 2 文字の組み合わせ

人間の目には同じですが、コンピュータ的には別物です。
このまま比較すると、=== では「違う」と判定されます。

検索でヒットしない。
ユニークキーの判定がすり抜ける。
「同じはずの文字列」が一致しない。

こういう“気持ち悪いバグ”を防ぐために、「Unicode 正規化」で
「同じ意味の文字列は、同じバイト列にそろえる」ということをします。


PHP で使うのは Normalizer(intl 拡張)

Normalizer::normalize の基本

PHP には intl 拡張があり、その中の Normalizer クラスで Unicode 正規化ができます。

use Normalizer;

$text = "ガ"; // 例

$normalized = Normalizer::normalize($text, Normalizer::FORM_C);
PHP

第二引数の Normalizer::FORM_C が「どの正規化形式にするか」です。
よく使うのはこの 2 つだけ覚えておけば十分です。

  • FORM_C … NFC(合成済み)
  • FORM_D … NFD(分解形)

実務では、まず「全部 NFC にそろえる」と決めてしまうのが一番シンプルです。


例題① 「ガ」が一致しない問題を正規化でそろえる

わざと「見た目だけ同じ」文字列を作る

// パターン A: 1 文字の「ガ」
$a = "ガ";

// パターン B: 「カ」 + 「゛」
$b = "カ゛";

var_dump($a === $b); // bool(false)
PHP

見た目は同じでも、=== では false です。

正規化してから比較する

use Normalizer;

$aN = Normalizer::normalize($a, Normalizer::FORM_C);
$bN = Normalizer::normalize($b, Normalizer::FORM_C);

var_dump($aN === $bN); // bool(true)
PHP

NFC にそろえることで、「同じ意味の文字列」が本当に同じバイト列になります。
検索や比較、ハッシュ化の前に「正規化しておく」ことで、こうした差異を吸収できます。


例題② ユーザー名・タグ名などを正規化して保存する

ユースケースのイメージ

ユーザー名やタグ名を DB のユニークキーにしているとします。
見た目は同じなのに、内部表現が違うせいで「重複登録」がすり抜けると困ります。

そこで、「保存前に必ず Unicode 正規化する」というルールを入れます。

実装例

use Normalizer;

function normalize_for_key(string $value): string
{
    // まず Unicode 正規化(NFC)
    $value = Normalizer::normalize($value, Normalizer::FORM_C);

    // ついでに前後の空白もトリムしておく
    return trim($value);
}

// 保存前
$username = $_POST['username'] ?? '';

$username = normalize_for_key($username);

// あとは $username をそのままユニークキーとして扱う
PHP

ここでの重要ポイントは、「“キーになる文字列”は必ず正規化してから使う」と決めてしまうことです。
これを徹底するだけで、「見た目は同じなのに別ユーザー扱い」という事故をかなり防げます。


どのタイミングで正規化するか

基本方針は「入口で正規化」

実務で一番扱いやすいのは、次のような方針です。

  • 外部から入ってくる文字列(フォーム入力、API、CSV など)は、
    「アプリに入った瞬間」に正規化してしまう。
  • DB に保存するときも、保存前に正規化しておく。
  • 比較・検索・ハッシュ化など、「同一性」が絡む処理の前には、
    正規化済みであることを前提にする。

つまり、「中に入ってきた時点で、もう全部 NFC になっている世界」を作るイメージです。
そうしておけば、アプリ内部では「正規化されている前提」でシンプルに書けます。


まとめ:今日からの「Unicode 正規化」ユーティリティ

Unicode 正規化の本質は、「見た目が同じ文字列を、本当に同じバイト列にそろえる」ことです。
特に日本語+記号が混ざる環境では、「濁点付き」「結合文字」「コピー&ペースト由来の微妙な違い」が地味に効いてきます。

PHP では、Normalizer::normalize($str, Normalizer::FORM_C) を 1 本覚えておけば十分です。
実務ユーティリティとしては、例えばこんな関数を用意しておくと使いやすくなります。

use Normalizer;

function normalize_unicode_nfc(string $value): string
{
    $normalized = Normalizer::normalize($value, Normalizer::FORM_C);

    // 正規化に失敗した場合は、元の文字列をそのまま返す(お好みで)
    return $normalized === false ? $value : $normalized;
}
PHP

これを「入力の入口」「保存前」「キーに使う前」に必ず通す——
それだけで、「なんでこれ一致しないの?」という謎バグをかなり減らせます。

タイトルとURLをコピーしました