Webサイトにコメント機能や掲示板など、ユーザーが自由に文章を投稿できる機能を設ける際、不適切な単語(禁止ワード)の投稿を防ぐ仕組みは、サイトの健全性を保つために不可欠です。
しかし、単純にNGワードが含まれているかをチェックするだけでは、「悪意のない単語」までブロックしてしまう誤検知が頻繁に発生します。
今回は、このような誤検知を減らし、より実用的な禁止ワードチェック機能をPHPで実装するための3つのステップを、初心者の方にも分かりやすく解説します。
なぜ単純なチェックではダメなのか?
例えば、「ナイフ」という単語を禁止ワードに設定したとします。この時、「ディナーナイフ」という無害な単語までブロックされてしまう可能性があります。
また、ユーザーは「ナイ フ」のように空白を入れたり、「ナイフ」のように半角カナを使ったりして、チェックをすり抜けようとするかもしれません。
これらの問題を解決するため、以下の3ステップで処理を組み立てます。
- テキストの正規化: 大文字・小文字、全角・半角、空白などを統一し、検索しやすい形に整える。
- 例外(許可ワード)の除外: 「ディナーナイフ」のような問題ない複合語を、あらかじめチェック対象から外す。
- 禁止ワードのチェック: 整形後のテキストに、禁止ワードが含まれているかを最終確認する。
ステップ1: テキストの正規化(検索しやすい形に整える)
まず、ユーザーが入力した文字列を、チェックしやすいように「正規化(Normalize)」します。これにより、表記の揺れを吸収し、チェックの精度を高めます。
<?php
$original_text = "ミルク コーヒーでも飲みませんか?";
// 1. 小文字に統一
$normalized_text = mb_strtolower($original_text, "UTF-8");
// 2. 全角英数カナを半角に、全角スペースを半角に変換 ("KVas"オプション)
$normalized_text = mb_convert_kana($normalized_text, "KVas", "UTF-8");
// 3. 空白や句読点をすべて削除
$normalized_text = preg_replace('/[\s\p{P}]/u', '', $normalized_text);
// 正規化後の文字列: "みるくこーひーでものみませんか"
echo $normalized_text;
preg_replace
の[\s\p{P}]
というパターンは、あらゆる空白文字(\s
)と、句読点(\p{P}
)をまとめて削除するための便利な正規表現です。
ステップ2: 例外(許可ワード)の除外
次に、誤検知を防ぐための最も重要なステップです。「禁止ワード」を含むけれども「許可したい複合語」のリスト(ホワイトリスト)をあらかじめ用意しておきます。
そして、正規化したテキストの中から、この許可ワードに一致する部分を、意味のない文字(ここでは*
)に置き換えてしまいます。
<?php
// ...正規化処理の続き...
$target_text = $normalized_text; // 正規化後のテキストを$target_textとして扱う
// 許可ワードのリスト(ホワイトリスト)
$ok_words = ["コーヒーゼリー", "バターナイフ"];
foreach ($ok_words as $ok_word) {
// 許可ワードが見つかったら、その部分を "*" に置換
if (mb_strpos($target_text, $ok_word) !== false) {
$target_text = str_replace($ok_word, str_repeat('*', mb_strlen($ok_word)), $target_text);
}
}
// もし元の文が"コーヒーゼリー"なら、$target_textは "********" になる
この処理により、例えば「コーヒーゼリー」という文字列はチェックの対象から外れ、「コーヒー」という禁止ワードに引っかかることがなくなります。
ステップ3: 禁止ワードのチェック
正規化され、許可ワードが除外されたテキストに対して、いよいよ本命の禁止ワードリスト(ブラックリスト)を使って最終チェックを行います。
テキストの中に禁止ワードが一つでも見つかった時点で、ループをbreak
で中断すれば、効率的に処理を終えることができます。
<?php
// ...前ステップの続き...
$is_ng = false; // 禁止ワードが見つかったかどうかのフラグ
// 禁止ワードのリスト(ブラックリスト)
$ng_words = ["ばか", "あほ", "ナイフ", "コーヒー"];
foreach ($ng_words as $ng_word) {
// テキスト内に禁止ワードが見つかったか
if (mb_strpos($target_text, $ng_word) !== false) {
$is_ng = true; // フラグを立てる
break; // ループを中断
}
}
// 結果の表示
if ($is_ng) {
echo "「" . htmlspecialchars($original_text, ENT_QUOTES, 'UTF-8') . "」には、禁止ワードが含まれています。";
} else {
echo "「" . htmlspecialchars($original_text, ENT_QUOTES, 'UTF-8') . "」は問題のない文字列です。";
}
完成版コード(関数化)
これまでの3ステップを、再利用しやすいように一つの関数にまとめます。この関数を使えば、どんな文字列でも簡単かつ高精度に禁止ワードをチェックできます。
<?php
/**
* 文字列に禁止ワードが含まれているかチェックする関数
* @param string $text チェック対象の文字列
* @return bool 禁止ワードが含まれていればtrue, そうでなければfalse
*/
function containsNgWord(string $text): bool
{
// ===== ステップ1: テキストの正規化 =====
$normalized_text = mb_strtolower($text, "UTF-8");
$normalized_text = mb_convert_kana($normalized_text, "KVas", "UTF-8");
$normalized_text = preg_replace('/[\s\p{P}]/u', '', $normalized_text);
// ===== ステップ2: 例外(許可ワード)の除外 =====
$ok_words = ["コーヒーゼリー", "バターナイフ"];
$target_text = $normalized_text;
foreach ($ok_words as $ok_word) {
if (mb_strpos($target_text, $ok_word) !== false) {
$target_text = str_replace($ok_word, str_repeat('*', mb_strlen($ok_word)), $target_text);
}
}
// ===== ステップ3: 禁止ワードのチェック =====
$ng_words = ["ばか", "あほ", "ナイフ", "コーヒー"];
foreach ($ng_words as $ng_word) {
if (mb_strpos($target_text, $ng_word) !== false) {
return true; // 禁止ワードが見つかった
}
}
return false; // 禁止ワードは見つからなかった
}
// ===== 関数の使用例 =====
$comment1 = "おいしいコーヒーゼリーですね!";
$comment2 = "このコーヒーはうまい。";
$comment3 = "馬鹿げた話だ!";
var_dump(containsNgWord($comment1)); // bool(false) - 許可
var_dump(containsNgWord($comment2)); // bool(true) - 禁止
var_dump(containsNgWord($comment3)); // bool(true) - 禁止
?>
まとめ
今回は、誤検知を減らした実用的な禁止ワードチェック機能の実装方法を学びました。
【ポイントの振り返り】
- 正規化: 表記の揺れをなくし、チェックの精度を上げる。
- ホワイトリスト: 許可する複合語を先に除外し、誤検知を防ぐ。
- ブラックリスト: 最後に、整形されたテキストに対して禁止ワードをチェックする。
この**「正規化 → ホワイトリスト → ブラックリスト」**という多層的なアプローチは、より高度なテキストフィルタリングの基本となる考え方です。完璧な禁止ワードチェックシステムを自作するのは非常に難しいですが、この基本パターンを応用するだけで、多くのウェブサイトで十分実用的なレベルの機能を実装できるでしょう。