「伏字変換」でやりたいことを言葉にしてみる
ここでの「伏字変換」は、特定の単語やNGワードを、画面に出すときだけ隠す(置き換える)処理のことです。
ログにはそのまま残したいけど、ユーザーには見せたくない。
チャットやコメントで、禁止ワードは伏字にしたい。
名前や社名の一部だけ伏せて表示したい。
つまり、「元の文字列を壊さずに、表示用だけ“伏せた版”を生成するユーティリティ」を作る、という話です。
一番基本の「NGワードを全部伏字にする」パターン
単純な単語置き換えから始める
まずは、NGワードのリストを持っておいて、
それに一致した単語を全部 *** のような伏字に変えるパターンです。
function censorWords(text, ngWords, mask = "***") {
if (text == null) return "";
let result = String(text);
for (const word of ngWords) {
if (!word) continue;
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(escaped, "g");
result = result.replace(re, mask);
}
return result;
}
JavaScriptポイントを噛み砕きます。
NGワードは配列で渡す(["NGワード1", "NGワード2"] のように)。
正規表現で使うために、NGワードの中の記号をエスケープしている。new RegExp(escaped, "g") で「その単語に一致するところを全部」探す。result.replace(re, mask) で、見つかった部分を伏字に置き換える。
実際の動きはこんな感じです。
const ng = ["バカ", "死ね"];
censorWords("お前バカだな", ng); // "お前***だな"
censorWords("そんなこと言うなよ死ねとか", ng); // "そんなこと言うなよ***とか"
JavaScriptまずはこの「単語を丸ごと伏字にする」形が基本になります。
伏字の形を「文字数に合わせる」パターン
「全部 *** だと長さが分からない」問題
さっきの実装だと、"バカ" も "死ね" も *** になってしまい、
元の文字数が分からなくなります。
「文字数だけは残したい」という場合は、
元の単語の長さに合わせて伏字を作るのが自然です。
function censorWordsByLength(text, ngWords, maskChar = "*") {
if (text == null) return "";
let result = String(text);
for (const word of ngWords) {
if (!word) continue;
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(escaped, "g");
result = result.replace(re, (matched) => {
return maskChar.repeat(matched.length);
});
}
return result;
}
JavaScriptここが重要ポイントです。
replace に関数を渡すと、「見つかった文字列ごとにどう置き換えるか」を動的に決められる。matched.length を使って、元の単語の長さ分だけ伏字文字を並べている。
動きはこうなります。
const ng = ["バカ", "死ね"];
censorWordsByLength("お前バカだな", ng); // "お前**だな"
censorWordsByLength("そんなこと言うなよ死ねとか", ng); // "そんなこと言うなよ***とか"
JavaScript「どのくらいの長さの単語だったか」は残しつつ、中身は見えない、というバランスになります。
部分伏字:「単語の一部だけ隠す」パターン
名前や社名の一部だけ伏せたいとき
例えば、名前や社名をログや画面に出すときに、
「最初の1文字だけ見せて、残りは伏字」
「最後の1文字だけ見せて、間を伏字」
のようにしたいことがあります。
これは、NGワードというより「特定の文字列を部分マスクする」話なので、
マスクユーティリティと組み合わせるときれいに書けます。
function censorName(name, maskChar = "*") {
if (name == null) return "";
const s = String(name);
if (s.length <= 1) return maskChar.repeat(s.length);
const first = s[0];
const masked = maskChar.repeat(s.length - 1);
return first + masked;
}
JavaScript動きはこうです。
censorName("山田"); // "山*"
censorName("山田太郎"); // "山***"
censorName("田中"); // "田*"
JavaScript「伏字変換」というより「部分マスク」ですが、
実務ではこのパターンも“伏字”としてよく使われます。
実務寄り:NGワードリストをどう扱うか
コードにベタ書きしないほうがいいケース
NGワードは、ビジネス側の判断で増えたり減ったりします。
最初は3個だったのが、運用しているうちに10個、20個と増えていく。
国や言語によってNGワードが違う。
こういう場合、コードにベタ書きするより、設定ファイルやDBから読み込むほうが現実的です。
ユーティリティ側は「NGワードの配列を受け取る」だけにしておき、
「その配列をどこから持ってくるか」は別レイヤーに任せる、という分担がきれいです。
// ユーティリティ側
export function censorWordsByLength(text, ngWords, maskChar = "*") { ... }
// 呼び出し側
const ngWords = loadNgWordsFromConfig(); // どこかから読み込む想定
const safeText = censorWordsByLength(comment, ngWords);
JavaScript設計として意識してほしいこと
「伏字にするだけ」と「保存値を変える」は分ける
伏字変換は、基本的に「表示用の加工」です。
DB に保存する値はそのまま
画面に出すときだけ伏字にする
という構造にしておくと、後から「誰が何を書いたか」を正しく追えるし、
監査ログにも本当の値を残せます。
逆に、「保存するときに伏字にしてしまう」と、
後から何が書かれていたか分からなくなり、
トラブル調査や不正検知が難しくなります。
ユーティリティの責務は「表示用の伏字文字列を生成すること」。
保存値をどうするかは、別の設計の話だと切り分けておくと安全です。
「どこまで伏せるか」はビジネス側と決める
伏字は、単なる見た目ではなく、情報の見せ方のポリシーです。
NGワードは全部 *** にするのか。
文字数に合わせて伏字にするのか。
名前やメールアドレスをどこまで伏せるのか。
これはエンジニアだけで決めるのではなく、
ビジネス・法務・セキュリティと一緒に決めるべき内容です。
ユーティリティは、その決まったルールを正確に実装する場所、として設計しておくと、
役割分担がとてもクリアになります。
ちょっとだけ手を動かしてみる
コンソールで、次のあたりを試してみてください。
const ng = ["バカ", "死ね"];
censorWords("お前バカだな", ng);
censorWordsByLength("そんなこと言うなよ死ねとか", ng);
censorName("山田太郎");
censorName("田中");
JavaScript「どこがどう伏字になるか」を目で確認してみてください。
そのうえで、自分のプロジェクトに
export function censorWords(...) { ... }
export function censorWordsByLength(...) { ... }
export function censorName(...) { ... }
JavaScriptのようなユーティリティを置いて、
「伏字にしたいときは必ずここを通す」というルールにしてみてください。
その瞬間、あなたの伏字処理は
場当たり的な *** 置き換えから、
意図とポリシーを持った「伏字変換ユーティリティ」に一段レベルアップします。
