デジタル道具屋
デジタル道具屋
記事一覧へ戻る
更新: 読了 約 5 分

URL エンコードの仕様と「日本語が文字化けする」を本気で直すガイド

#development

執筆: ありがっさまりょうた

デジタル道具屋を個人で開発・運用しています。記事は実装時の検証結果と公式仕様を照合して執筆しています。

本記事の執筆方針と編集ポリシーについてはAbout ページをご覧ください。

導入:URL に日本語を入れると「%」だらけになる理由

日本語を含む URL をチャットなどに貼り付けると、https://example.com/?q=%E3%81%82%E3%81%84 のように 「%」と 16進数が並んだ形になることがあります。これは URL が壊れたわけではなく、percent-encoding(パーセントエンコード)と呼ばれる、ASCII 以外の文字を URL で安全に運ぶための正規の表現です。%E3%81%82 は「あ」を UTF-8 で表した 3バイト(0xE3 0x81 0x82)をそのまま 16進数で書いたものです。

URL の仕様(RFC 3986)では、ASCII の限られた文字以外は percent-encoding しなければ URL に含められないと定められています。 そのため日本語を URL に含める場合、この変換は必ず行われます。

1. percent-encoding の仕組み

URL では 「予約文字(reserved characters)」と「非予約文字(unreserved characters)」が区別されます。 非予約文字(英数字とごく一部の記号)以外を URL の意味のある場所に入れたい場合、各バイトを %XX という 16進数表記に変換します。

'あ'  → UTF-8: 0xE3 0x81 0x82  → "%E3%81%82"
'A'   → ASCII: 0x41             → "A"(変換不要)
' '   → ASCII: 0x20             → "%20" もしくは "+"
'?'   → ASCII: 0x3F             → "%3F"(クエリ区切りと混同しないため)

重要なのは、percent-encoding が変換するのは「文字」ではなく「バイト列」だという点です。 同じ文字でも文字コードが違えばバイト列が変わり、結果も変わります。 現在の URL 標準(WHATWG URL Standard)や JavaScript の encodeURIComponent は常に UTF-8 を使いますが、 古いシステムが Shift_JIS でエンコードした URL を UTF-8 として解釈すると文字化けが起こります。

encodeURIComponent('日本')
// → "%E6%97%A5%E6%9C%AC"   (UTF-8: 日 = E6 97 A5, 本 = E6 9C AC)

// 同じ「日本」を Shift_JIS のバイト列で percent-encoding した場合
// → "%93%FA%96%7B"          (Shift_JIS: 日 = 93 FA, 本 = 96 7B)

decodeURIComponent('%93%FA%96%7B')
// → URIError: URI malformed(UTF-8 として不正なバイト列のため)

「日本語のクエリが文字化けする」「デコードでエラーになる」という場合は、 送信側と受信側で前提としている文字コードが一致しているかを最初に確認します。

2. encodeURI と encodeURIComponent の違い

JavaScript には encodeURI と encodeURIComponent の 2 つの関数があります。 両者の違いは、パスやクエリの「区切り記号」をそのまま残すかどうかです。

関数対象? や /
encodeURIURL 全体そのまま残す
encodeURIComponentクエリの値、フラグメント等の「断片」エンコードする

区切り記号を含む値を渡すと、この違いが結果に直接表れます。

encodeURI('a?b&c')           // → "a?b&c"       (? と & はそのまま)
encodeURIComponent('a?b&c')  // → "a%3Fb%26c"   (? → %3F, & → %26)

// クエリの値に encodeURI を使うと、& が区切りとして解釈され値が分断される
'https://example.com/?q=' + encodeURI('a&b')           // → ?q=a&b   (q は "a" になる)
'https://example.com/?q=' + encodeURIComponent('a&b')  // → ?q=a%26b (q は "a&b" になる)

したがって、クエリパラメータの値には encodeURIComponent を使うのが原則です。encodeURI は「区切り記号を含む URL 全体をまとめて変換したい」という用途に限られます。 さらに安全な方法は、URL オブジェクトと URLSearchParams に組み立てを任せることです:

// 推奨:URLSearchParams を使う
const params = new URLSearchParams({ q: '東京 ラーメン', page: '2' });
const url = `https://example.com/search?${params.toString()}`;
// → https://example.com/search?q=%E6%9D%B1%E4%BA%AC+%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3&page=2

URLSearchParams はキーと値の両方に必要な percent-encoding を自動で適用するため、 文字列を手作業で連結する方法よりもエンコード漏れが起こりにくくなります。

3. 「+ はスペース」問題

URL のクエリ部分では、スペースが %20 ではなく + で表されることがあります。 これは application/x-www-form-urlencoded(HTML フォームの送信形式)の歴史的経緯です。パス部分では %20、クエリ部分では + または %20、というのが現在の事実上のルール。

デコード側でも注意が必要:

decodeURIComponent('東京+ラーメン')      // → "東京+ラーメン"(+ は変換されない)
new URLSearchParams('q=東京+ラーメン').get('q') // → "東京 ラーメン"(+ がスペースに)

4. 日本語ファイル名のダウンロード

Content-Disposition: attachment; filename="日本語.pdf" のような書き方は、 ASCII 以外の扱いが歴史的に混乱しています。 現在は RFC 5987 に従い、以下のように書くのが標準です:

Content-Disposition: attachment;
  filename="report.pdf";
  filename*=UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.pdf

filename(旧)に ASCII フォールバック、filename*(新)に RFC 5987 形式で日本語を入れます。 モダンブラウザは filename* を優先するため、日本語ファイル名で正しくダウンロードされます。

5. SNS シェア用 URL の組み立て

X(Twitter)や LINE のシェア URL を生成するとき、ユーザー入力をそのまま連結すると壊れます:

// 失敗:& や # が混入すると壊れる
const url = `https://twitter.com/intent/tweet?text=${title}&url=${pageUrl}`;

// 正しい
const url = new URL('https://twitter.com/intent/tweet');
url.searchParams.set('text', title);
url.searchParams.set('url', pageUrl);
window.open(url.toString());

6. 試して確かめる

URL に含まれる謎の %XX をデコードしたり、自分の文字列を percent-encoding したりするには、URL エンコード・デコードツールが便利です。 クエリパラメータのデバッグや、SNS シェア URL の構築にもどうぞ。

関連: HTML エンティティ変換(HTML 上で < を表示する用)、Base64 変換(バイナリを URL に乗せる別手段)。

よくある失敗パターン

OAuth の state や redirect_uri のように、+・=・& を含みうる値を エンコードせずに連結すると、受信側で値が分断されたり書き換わったりして認証フローが失敗します。 たとえば Base64 形式の state に含まれる + は、クエリではスペースとして解釈されます。 URL を組み立てるときは文字列連結ではなく URL オブジェクトとURLSearchParams を使うことで、この種のエンコード漏れは構造的に防げます。

参考にした一次情報

本記事の内容は、以下の公式仕様や一次情報を参照して執筆しています。