「日本語ファイル名安全化」で何をしたいのかイメージする
まず、ゴールのイメージからはっきりさせます。
「請求書_2025年3月分(最終版).pdf」
「写真 ① 家族旅行@沖縄.jpg」
こういう“人間には読みやすい日本語ファイル名”を、そのままシステムで使うと、次のような問題が起きます。
OS によって使えない文字が混ざっている。
URL に載せたときに文字化けする。
メール添付やダウンロード時に、ブラウザごとに表示がバラバラになる。
そこでやりたいことは、ざっくり言うとこうです。
「人間にとって意味が分かる名前はなるべく残しつつ、
OS・URL・ブラウザにとって“安全な文字だけ”で構成されたファイル名に変換する」
これを小さなユーティリティ関数にしておくと、
アップロード、ダウンロード、ログ保存など、いろんな場面で再利用できます。
何が「危険なファイル名」なのかを整理する
OS 的に危ない文字
Windows を意識すると、次のような文字はファイル名に使えません。
/ \ : * ? " < > |
また、先頭や末尾のスペースやドットもトラブルの元になります。
「日本語だから危険」というよりは、「記号や制御文字が危険」です。
URL 的に気をつけたいポイント
URL にファイル名を含める場合、日本語やスペースは URL エンコードされます。
「請求書 2025年3月.pdf」
→ 請求書%202025年3月.pdf(ブラウザによってはさらにエンコードの差異)
ここは「ファイル名そのものを変える」のか、
「URL に載せるときだけエンコードする」のかを分けて考える必要があります。
今回のテーマは「ファイル名そのものを安全にする」なので、
ファイルシステムに保存する名前として安全。
URL に載せても極端に長くなりすぎない。
このあたりを目標にします。
実務で使える「日本語ファイル名安全化」の方針
方針を言葉で決めてからコードに落とす
いきなりコードを書くのではなく、先にルールを決めます。
- 全角・半角の揺れはある程度そろえる(全角スペース → 半角スペースなど)。
- 禁止文字(
/ \ : * ? " < > |など)は安全な文字(_など)に置き換える。 - 制御文字や見えない文字は削除する。
- 連続する空白やアンダースコアはまとめる。
- 長すぎるファイル名は、末尾を切って最大長に収める。
- 拡張子はできるだけ残す(
.pdf.jpgなど)。
この「ルール」をコードに落とし込んだものが、
“日本語ファイル名安全化ユーティリティ”になります。
基本形:ファイル名(拡張子込み)を安全化する関数
拡張子とベース名を分けて処理する
まずは、ファイル名全体から「ベース名」と「拡張子」を分けます。
/**
* 日本語ファイル名を「OS・URL的に安全な形」に整形する
* 例: "請求書 2025年3月分(最終版).pdf"
* → "請求書_2025年3月分_最終版.pdf"
*/
function sanitizeJapaneseFilename(string $filename, int $maxLength = 100): string
{
$info = pathinfo($filename);
$basename = $info['filename'] ?? '';
$extension = $info['extension'] ?? '';
// 1. 全角スペースを半角スペースに
$basename = str_replace(["\u{3000}"], ' ', $basename);
// 2. 制御文字を削除(0x00〜0x1F, 0x7F)
$basename = preg_replace('/[\x00-\x1F\x7F]/u', '', $basename);
// 3. 禁止文字をアンダースコアに置き換え
$basename = str_replace(
['\\', '/', ':', '*', '?', '"', '<', '>', '|'],
'_',
$basename
);
// 4. 連続する空白やアンダースコアをまとめる
$basename = preg_replace('/[ _]+/u', '_', $basename);
// 5. 先頭・末尾のアンダースコアやスペースを削る
$basename = trim($basename, " _");
// 6. 何も残らなかった場合の保険
if ($basename === '') {
$basename = 'file';
}
// 7. 長さ制限(マルチバイト対応)
if (mb_strlen($basename, 'UTF-8') > $maxLength) {
$basename = mb_substr($basename, 0, $maxLength, 'UTF-8');
}
// 8. 拡張子を戻す
if ($extension !== '') {
return $basename . '.' . $extension;
}
return $basename;
}
PHP動作例をいくつか見てみます。
echo sanitizeJapaneseFilename('請求書 2025年3月分(最終版).pdf');
// 請求書_2025年3月分_最終版.pdf
echo sanitizeJapaneseFilename('写真 ① 家族旅行@沖縄.jpg');
// 写真_①_家族旅行@沖縄.jpg (@などはそのまま)
echo sanitizeJapaneseFilename('レポート:/危険*な?名前.txt');
// レポート_危険_な_名前.txt
echo sanitizeJapaneseFilename(' " ');
// file (全部消えたので保険の名前)
ここでの重要ポイントは、「禁止文字だけを最小限いじって、日本語自体はできるだけ残す」ことです。
全部ローマ字にしてしまう、全部ハッシュ値にしてしまう、というのも一つの手ですが、
人間がログや画面で見たときに意味が分からなくなるので、まずはこのくらいのバランスが現実的です。
URL に載せるときの「安全化」との違い
ファイル名そのもの vs URL エンコード
今作った sanitizeJapaneseFilename は、「ファイルシステムに保存する名前」を整える関数です。
URL に載せるときは、さらに URL エンコードが必要です。
$safeName = sanitizeJapaneseFilename('請求書 2025年3月分(最終版).pdf');
$url = 'https://example.com/downloads/' . rawurlencode($safeName);
// https://example.com/downloads/%E8%AB%8B%E6%B1%82%E6%9B%B8_2025%E5%B9%B43%E6%9C%88%E5%88%86_%E6%9C%80%E7%B5%82%E7%89%88.pdf
PHPここで大事なのは、「ファイル名の安全化」と「URL エンコード」は別物だということです。
ファイル名安全化は一度だけ行い、その結果をファイルシステムに保存する。
URL エンコードは、「URL に載せるたびに」行う。
この二つを混ぜると、「二重エンコード」や「意図しない文字化け」の原因になります。
もう一歩踏み込んだ実務的な工夫
「識別子+元の名前」のハイブリッドにする
実務では、「同じ名前のファイルが大量にアップロードされる」ことがあります。
「請求書.pdf」
「請求書.pdf」
「請求書.pdf」
これを全部 請求書.pdf として保存しようとすると、当然ぶつかります。
そこでよくやるのが、
内部的な保存名:{UUID}_{安全化した元の名前}
表示名:元の名前(または安全化した名前)
というハイブリッドです。
function generateStoredFilename(string $originalName): string
{
$safe = sanitizeJapaneseFilename($originalName);
$id = bin2hex(random_bytes(8)); // 16文字のランダムID
return $id . '_' . $safe;
}
PHP例としては、こんな感じになります。
echo generateStoredFilename('請求書 2025年3月分(最終版).pdf');
// 9f3a1c2b4d6e7f80_請求書_2025年3月分_最終版.pdf
PHPこれなら、
ファイルシステム上では一意な名前になる。
ログやディレクトリを見ても、元の意味がある程度分かる。
という、実務的にちょうどいいバランスになります。
まとめ:今日からの「日本語ファイル名安全化」ユーティリティ
ここまでのポイントを整理すると、こうなります。
日本語ファイル名そのものが危険なのではなく、「禁止文字」「制御文字」「極端な長さ」が危険。
安全化の基本は、「禁止文字を安全な文字に置き換え」「制御文字を削除」「連続した空白や記号を整理」「長さを制限」の4つ。
拡張子は pathinfo で分けて、ベース名だけを加工し、最後に拡張子を戻すと安全で分かりやすい。
URL に載せるときは、ファイル名安全化とは別に rawurlencode などでエンコードする。
実務でまず一本持っておくといいのは、この関数です。
function sanitizeJapaneseFilename(string $filename, int $maxLength = 100): string
{
$info = pathinfo($filename);
$basename = $info['filename'] ?? '';
$extension = $info['extension'] ?? '';
$basename = str_replace(["\u{3000}"], ' ', $basename);
$basename = preg_replace('/[\x00-\x1F\x7F]/u', '', $basename);
$basename = str_replace(
['\\', '/', ':', '*', '?', '"', '<', '>', '|'],
'_',
$basename
);
$basename = preg_replace('/[ _]+/u', '_', $basename);
$basename = trim($basename, " _");
if ($basename === '') {
$basename = 'file';
}
if (mb_strlen($basename, 'UTF-8') > $maxLength) {
$basename = mb_substr($basename, 0, $maxLength, 'UTF-8');
}
if ($extension !== '') {
return $basename . '.' . $extension;
}
return $basename;
}
PHPもし、あなたのプロジェクトで「アップロードされたファイル名をそのまま保存している」箇所があるなら、そこがこのユーティリティの差し込みポイントです。
その一箇所にこの関数を挟むだけで、将来のトラブルをかなり先回りで潰せます。
