PHP Tips | 文字列処理:URL・パス系 – 日本語ファイル名安全化

PHP PHP
スポンサーリンク

「日本語ファイル名安全化」で何をしたいのかイメージする

まず、ゴールのイメージからはっきりさせます。

「請求書_2025年3月分(最終版).pdf」
「写真 ① 家族旅行@沖縄.jpg」

こういう“人間には読みやすい日本語ファイル名”を、そのままシステムで使うと、次のような問題が起きます。

OS によって使えない文字が混ざっている。
URL に載せたときに文字化けする。
メール添付やダウンロード時に、ブラウザごとに表示がバラバラになる。

そこでやりたいことは、ざっくり言うとこうです。

「人間にとって意味が分かる名前はなるべく残しつつ、
OS・URL・ブラウザにとって“安全な文字だけ”で構成されたファイル名に変換する」

これを小さなユーティリティ関数にしておくと、
アップロード、ダウンロード、ログ保存など、いろんな場面で再利用できます。


何が「危険なファイル名」なのかを整理する

OS 的に危ない文字

Windows を意識すると、次のような文字はファイル名に使えません。

/ \ : * ? " < > |

また、先頭や末尾のスペースやドットもトラブルの元になります。

「日本語だから危険」というよりは、「記号や制御文字が危険」です。

URL 的に気をつけたいポイント

URL にファイル名を含める場合、日本語やスペースは URL エンコードされます。

「請求書 2025年3月.pdf」
請求書%202025年3月.pdf(ブラウザによってはさらにエンコードの差異)

ここは「ファイル名そのものを変える」のか、
「URL に載せるときだけエンコードする」のかを分けて考える必要があります。

今回のテーマは「ファイル名そのものを安全にする」なので、

ファイルシステムに保存する名前として安全。
URL に載せても極端に長くなりすぎない。

このあたりを目標にします。


実務で使える「日本語ファイル名安全化」の方針

方針を言葉で決めてからコードに落とす

いきなりコードを書くのではなく、先にルールを決めます。

  1. 全角・半角の揺れはある程度そろえる(全角スペース → 半角スペースなど)。
  2. 禁止文字(/ \ : * ? " < > | など)は安全な文字(_ など)に置き換える。
  3. 制御文字や見えない文字は削除する。
  4. 連続する空白やアンダースコアはまとめる。
  5. 長すぎるファイル名は、末尾を切って最大長に収める。
  6. 拡張子はできるだけ残す(.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

もし、あなたのプロジェクトで「アップロードされたファイル名をそのまま保存している」箇所があるなら、そこがこのユーティリティの差し込みポイントです。
その一箇所にこの関数を挟むだけで、将来のトラブルをかなり先回りで潰せます。

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