URL文字化けの修復フロー — Shift_JIS時代の遺産を解読する
メルマガ解除URLや古いCMSのリンクで「%82%B1%82%F1%82%C9%82%BF%82%CD」のような文字列に出会ったら、Shift_JISでエンコードされた「こんにちは」かもしれません。UTF-8で復号する手軽屋のツールではそのままだと壊れた文字が出るので、修復手順を順を追って解説します。
1. なぜ文字化けが起きるのか
パーセントエンコーディング自体は「バイト列を%XXに変換するだけ」のシンプルな仕組み。問題は元の文字をバイト列にする時のエンコーディング(文字符号化方式)。RFC 3986はUTF-8を強く推奨していますが、現実には日本国内のサイトで2010年頃までShift_JISがよく使われていて、その時代の生成URLが今もメルマガ・ブックマーク・古い記事リンクで生き残っています。
手軽屋のツールはUTF-8で復号するため、Shift_JIS由来の%XX列は「変換できませんでした」または文字化けで返ってきます。
2. UTF-8とShift_JISの見分け方(バイトパターン)
%表現の最初の2バイトを見れば、ほぼ判別できます:
- ・UTF-8(日本語ひらがな・カタカナ):1文字3バイトで
%E3%81〜%E3%83あたりが頻出 - ・UTF-8(漢字常用):
%E4〜%E9で始まる3バイトが多い - ・Shift_JIS(ひらがな):1文字2バイトで
%82%A0〜%82%F1あたり(先頭が%82・%83) - ・Shift_JIS(漢字):
%88〜%9Fまたは%E0〜%FCの2バイト - ・EUC-JP:
%A4%A2〜%A4%F3(ひらがな・%A4ブロック)、漢字は%B0〜%FE
最初の%が3つ続いて%E3などが多い → UTF-8、2バイト周期で%82が多い → Shift_JIS、%A4が多い → EUC-JP、と当たりがつきます。
3. ターミナル(macOS/Linux)での修復手順
シェルでさっと変換するならnkf(漢字フィルタ)が定番:
# 1. %XXをバイトに戻す(python3使用)
echo "%82%B1%82%F1%82%C9%82%BF%82%CD" | python3 -c \
"import sys,urllib.parse; sys.stdout.buffer.write(urllib.parse.unquote_to_bytes(sys.stdin.read()))" \
> /tmp/sjis.bin
# 2. nkfでShift_JIS→UTF-8に変換
nkf -w /tmp/sjis.bin
# → こんにちはnkf -wはUTF-8出力。自動判別なので、EUC-JPでもShift_JISでも同じコマンドで通ります。
4. ブラウザのJavaScript(TextDecoder)で復号
ブラウザのTextDecoderはShift_JIS・EUC-JPもサポート(Encoding Living Standard):
// %XXをUint8Arrayに戻してから shift_jis でデコード
const fromPercentSjis = (s) => {
const bytes = new Uint8Array(
s.match(/%[0-9A-F]{2}/gi).map((h) => parseInt(h.slice(1), 16))
);
return new TextDecoder("shift_jis").decode(bytes);
};
fromPercentSjis("%82%B1%82%F1%82%C9%82%BF%82%CD");
// → "こんにちは"ラベルshift_jis / euc-jp / iso-2022-jp はEncoding Standardの命名に従います。fatal: trueオプションをつけると、壊れたバイト列に対して例外を投げて検知できます。
5. 実例:メルマガ解除URL
想定シナリオ:「https://example.jp/unsubscribe?name=%97L%8C%F8」
手軽屋のURLデコードでは「−Lè−Hø」のような壊れた文字に。Shift_JISで復号すると「有効」と読めます。%97L = 「有」(0x97 0x4C)、%8C%F8 = 「効」(0x8C 0xF8)。
この種のURLは2005年〜2010年頃のCMS(MovableType・XOOPS・古いWordPressのpermalink)で頻出。記事の永続URLには現在もShift_JIS時代のものが残っていることがあります。
6. 復元できないケース・誤検知
- ・ハッシュ化された値:MD5・SHA1のHEX文字列は%XXに見えても意味のある文字に戻りません
- ・UUID:
550e8400-e29b-41d4-a716-446655440000のような形は文字列としては読めるので変換不要 - ・暗号化された値:AES等で暗号化された値はバイト列としてランダムなのでどんなエンコーディングでも復号不可
- ・Base64でラップされた値:URLパラメータがBase64URL(
-/_含む)の場合、まずBase64URL→Base64→Base64デコードの順で復元
7. 「文字化けしない」URLを作る側のベストプラクティス
- ・UTF-8で統一:Webサーバの出力エンコーディング、テンプレートの
<meta charset="utf-8">、データベースの照合順を全部UTF-8で揃える - ・encodeURIComponent経由でURLを生成:手書きで%XXを書かない
- ・不可逆な値はBase64URLでラップ:トークンや内部IDはBase64URLで安全化
- ・古いURLはリダイレクト:Shift_JIS時代の旧URLは301でUTF-8版に飛ばす
8. 関連ツール・記事
- ・URLエンコード・Base64変換:UTF-8でのデコード
- ・パーセントエンコーディング仕様詳解:RFC 3986の根拠
- ・Base64の仕組みとUTF-8対応:Base64URLとの違い
- ・全角・半角変換:入力前の文字種を統一