URLのパーセントエンコーディングの仕組み

パーセントエンコーディングは、ひとつの具体的な問題を解決するために存在します。URLは限られた予測可能な文字の集まりでなければならないのに、人がそこに入れたいデータはほとんどの場合そうではない、という問題です。

URLに安全に含められる文字は限られている

スペースやアクセント付きの文字、非ラテン文字はURLで安全に使える文字の範囲に含まれておらず、一部の記号はURLの構文の中で特別な意味を持つため、どちらの種類の文字もそのままでは使えず変換が必要になります。

予約文字は構文上の意味を持ち、データとして使う場合はエンコードが必要

&、=、?、#、/といった文字はURLの構造そのものを表しています(クエリパラメータの区切りやフラグメントの指定など)。これらの文字を構造としてではなくデータの一部として値の中に含めたい場合は、URLの構文の一部として誤読されないようパーセントエンコードする必要があります。

非予約文字はエンコードする必要がない

英字、数字、そして一部の記号(-、.、_、~)は特別な構文上の意味を持たず、常に安全に使える文字の範囲に含まれているため、常にそのままURLに使うことができ、パーセントエンコードされることはありません。

%XX形式は16進数で表した1バイト分のエンコード結果

文字をパーセントエンコードするとは、その文字をUTF-8のバイト表現に変換し、各バイトをパーセント記号+16進数2桁で書き表すことです。スペースが%20になるのは、ASCII/UTF-8でスペースのバイト値が0x20だからです。

非ラテン文字はしばしば複数の%XXの組み合わせになる

基本的なASCIIの範囲外にある1文字(韓国語、日本語、中国語やアクセント付きのラテン文字など)は、UTF-8では通常2〜4バイトで表現されるため、2〜4個の%XXの並びになります。これが、非英語の文字を含むエンコード済みのURLが不釣り合いに長く見える理由です。

スペースは文脈によって%20にも+にもなりうる

%20は、URLのどこにあってもスペースを表す汎用的なパーセントエンコーディングです。一方で+記号でスペースを表す方法は、application/x-www-form-urlencodedというフォーム送信専用の古い別の慣習であり、すべてのURLに共通するルールではありません。

実例:「日本語」は9バイトの%XXになる

「日」「本」「語」はUTF-8でそれぞれ3バイトなので、「日本語」は合計9バイトになり、%E6%97%A5%E6%9C%AC%E8%AA%9E と変換されます。「é」は2バイトで %C3%A9、スペースを含む「a b&c=d」は a%20b%26c%3Dd になります。変換前の文字列に比べて、およそ3倍の長さになる点に注意してください。

encodeURIとencodeURIComponentは使う場面が違う

JavaScriptのencodeURIはURL全体を変換する関数で、/ ? & = # などの予約文字はそのまま残します。encodeURIComponentは1つの値(クエリの値やパスの1区間)を変換する関数で、予約文字もすべて変換します。クエリパラメータの値には encodeURIComponent を使うのが基本です。

16進数の大文字と小文字は同じ意味だが、大文字が推奨される

%e3 と %E3 は同じバイトを表しますが、標準仕様は大文字の使用を推奨しています。URLを文字列として比較・署名する場面では、大文字小文字の違いだけで別のURLとして扱われることがあるため、大文字に統一しておくのが安全です。

古いサイトではUTF-8以外の文字コードが使われていることがある

現在のURLはUTF-8が標準ですが、古い日本語サイトではShift_JISやEUC-JPで変換されたURLが残っています。たとえば「日」は、UTF-8では %E6%97%A5、Shift_JISでは %93%FA、EUC-JPでは %C6%FC になります。デコードして文字化けする場合は、文字コードの違いを疑ってください。

ドメイン名の日本語はパーセントエンコーディングではなくPunycodeで変換される

ホスト名(ドメイン名)に含まれる日本語は、%XXではなく xn-- で始まるPunycodeという別の方式で変換されます。パーセントエンコーディングが使われるのは、パス、クエリ、フラグメントの部分です。

URLの構造を表す記号まで変換すると別のURLになる

https://example.com/a/b のURL全体に encodeURIComponent をかけると、:// や / まで変換されて、アクセスできない文字列になります。変換するのは「値」の部分だけで、構造を示す区切り文字は変換しません。

パーセントエンコーディングが実際に解決している問題

URLは本来、ブラウザやサーバー、メールクライアントなど数え切れないシステムの間を、確実に、コンパクトなテキストとして行き来できることを目的としており、その多くは歴史的に限られたASCII文字しか安全に扱えませんでした。パーセントエンコーディングは、その安全な範囲の外にある文字と、データとして使われている予約文字を、その文字のUTF-8バイト表現にもとづいた標準的な%XX形式に変換することで、どのシステムが扱っても曖昧さのない、安全にやり取りできる文字列であることを保証しています。

エンコードされたURLを実際に壊してしまうミス

二重エンコード、つまりすでにパーセントエンコードされた文字列にもう一度パーセントエンコードをかけてしまうミスは、実際によくある不具合のひとつです。既存の%記号が%25に変換されてしまい、見た目だけでは気づきにくい形でURLが壊れてしまいます。もうひとつよくある混同は、+と%20をどこでも同じ意味だと思い込むことです。+が「スペース」を意味するのはフォームエンコードされたクエリデータの中だけであり、URLのパス部分などで使うと、スペースではなく文字どおりのプラス記号として誤解されることがあります。

実装するときの基本方針

URLを手作業で文字列連結して組み立てるより、URLSearchParamsやURLクラス、各言語のURL組み立て用ライブラリを使うほうが安全です。変換するのは、値を組み立てる最後のタイミングで一度だけにします。受け取る側では、フレームワークがすでにデコードしている場合が多いので、自分でもう一度デコードしていないかを確認してください。なお、URLSearchParamsはスペースを + に変換する点で %20 を使う関数とは結果が異なります。

トラブルの切り分け方

リンクが開けない、検索キーワードが途中で切れる、といった場合は、まずURLに %25 が混ざっていないかを確認します。%25 は % 自体を変換した結果なので、二重エンコードのサインです。次に、&や#を含む値が変換されずに入っていないかを確認します。&は次のパラメータの始まりとして、#はそこから先をフラグメントとして扱われてしまいます。

よくある質問

なぜ韓国語や日本語、中国語の1文字をエンコードすると、長いパーセント記号の並びになるのですか?

これらの文字は基本的なASCIIの範囲外にあり、UTF-8では複数のバイト(多くのCJK文字では一般的に3バイト)で表現されるためです。そのバイトひとつひとつが別々の%XXとしてエンコードされるため、短い非ラテン文字の文字列でも、エンコード後にはかなり長い文字列になります。

スペースを表す%20と+の実際の違いは何ですか?

%20はスペースを表す汎用的なパーセントエンコーディングで、URLのどこでも有効です。+でスペースを表すのは、application/x-www-form-urlencoded形式のフォームデータの中だけに当てはまる、より限定的で古い慣習であるため、その文脈以外で+を使うと、スペースではなく文字どおりのプラス記号として解釈されてしまうことがあります。

%25 は何を表していますか?

%記号そのものをエンコードした結果です。URLに %25 が見えるときは、%XXの%がもう一度エンコードされている二重エンコードの可能性が高く、元の値を一度デコードしてから確認すると原因を見つけやすくなります。

ブラウザのアドレスバーには日本語が表示されるのに、コピーすると%だらけになるのはなぜですか?

ブラウザが見やすさのためにデコードして表示しているだけで、実際に通信で使われるのはエンコードされたURLだからです。コピーしたときや、メール・チャットに貼り付けたときに%XXの形で表示されるのは、そのためです。

URL全体を encodeURIComponent で変換してもよいですか?

いいえ。:// や / も変換されてURLとして使えなくなります。URL全体には encodeURI、クエリの値やパスの1区間には encodeURIComponent と使い分けてください。ただし encodeURI は & や = を変換しないため、値の中にそれらが含まれる場合は適しません。

「~」(チルダ)はエンコードが必要ですか?

現在の標準では、チルダは非予約文字なのでエンコードは不要です。古い仕様や一部のライブラリでは %7E に変換されることがありますが、どちらも同じ意味で扱われます。