ゆるテックノート

URLの%20と+は、いつ同じ意味になる?

URLの検索語をコピーしたら、空白が%20だったり+だったりする。+を空白に置き換えたら、今度はC++という検索語が崩れた。どちらもURLまわりで見かける表記ですが、同じ規則だけで読み解こうとすると混乱します。

%の後ろの2桁は、文字ではなく1バイト

パーセントエンコードは、1バイトを%と16進数2桁で表します。日本語一文字が%一組に収まるとは限りません。たとえば「あ」はUTF-8でE3 81 82の3バイトなので、%E3%81%82になります。

UTF-8で変換する小さな例

%E3だけではUTF-8の「あ」を最後まで表せません。decodeURIComponentは、不完全な%表記や不正なUTF-8を読むとURIErrorになります。%を一つずつ消して文字化けを直すより、どの文字コードのバイトを符号化したのかから追う方が確実です。

encodeURIComponent("あ")
// "%E3%81%82"
decodeURIComponent("%E3%81%82")
// "あ"

+が空白になるのはフォーム形式の読み取り

RFC 3986のURI一般規則は、+を一律に空白とは扱いません。フォーム形式application/x-www-form-urlencodedのパーサーは、+を空白へ変換してからパーセントデコードします。ブラウザのURLSearchParamsもこの形式を使います。

同じ文字でも、関数が違うと結果が違う

C++という値を手でクエリへつなぐ代わりに、URLSearchParamsへ値として渡すと、プラス記号が%2Bへ変換されます。

処理 入力 結果
decodeURIComponent a+b a+b
URLSearchParamsでqを取得 q=a+b a b
URLSearchParamsでqを取得 q=C%2B%2B C++
URLSearchParamsでqを取得 q=C++ Cと空白2個

URL全体ではなく、部品として値を渡す

クエリの&や=は名前と値を区切る役目があります。値の中にも同じ記号が必要な場合は、値としてエンコードして区切りと混ざらないようにします。URL全体をencodeURIComponentへ渡すと、https:や/などの構造まで変わってしまいます。

値に+と空白と&を入れる

setに渡すのは元の文字列です。すでにC%2B%2Bへ変換した値を渡すと、%自体が%25になります。そのURLを一度読み取った受け手には、C++ではなくC%2B%2Bという文字列が渡ります。パスの区切り/と、クエリの値の中の/も同じ扱いとは限りません。

const u = new URL("https://example.com/search");
u.searchParams.set("q", "C++ & coffee");
u.href;
// "https://example.com/search?q=C%2B%2B+%26+coffee"
u.searchParams.get("q");
// "C++ & coffee"

%252Fが見えても、何度もデコードしない

%25は%という文字なので、%252Fを一度デコードすると%2Fになります。さらにもう一度で/になりますが、二度目が必要かは入力の約束次第です。勝手に繰り返すと、元は文字として渡したものが区切りへ変わり、意味を変えることがあります。

一段ずつ結果を見る

生成側が一度だけエンコードするはずなら、%252Fは二重変換の手がかりです。ただし、%2Fという文字列そのものをデータとして送った可能性もあります。URLの見た目だけでは断定せず、値を組み立てた箇所と受け手のデコード回数を合わせます。

decodeURIComponent("%252F")
// "%2F"
decodeURIComponent("%2F")
// "/"

同じ名前のクエリは複数存在できる

tag=php&tag=javascriptのようなクエリを、単純な名前→値の辞書にすると片方を落とす場合があります。URLSearchParamsは複数の組を保持でき、getは最初の値、getAllはすべての値を返します。受け手が複数値をどう使うかはサービス側の仕様です。

URL解析ツールで見える範囲

Online ToolsのURL解析はhttp・httpsの完全なURLをブラウザのURL APIで読み、クエリを複数の行として表示します。入力先のサイトへアクセスして結果を確かめるツールではありません。再構成では空白が+になるなど表記が正規化されるため、署名付きURLの文字列をそのまま保つ用途には向きません。

まずexample.comのURLで値を見比べる

URL解析・クエリ編集へhttps://example.com/search?q=C%2B%2B&tag=php&tag=javascriptを入れると、qのC++と二つのtagを見られます。単独の文字列の変換はエンコード・デコードのURLモードで試せます。このモードのdecodeURIComponentは、+を空白には変えません。

関連記事と仕様

文字とバイトの違いは文字コードの基礎、URLに載せる別形式はBase64とBase64urlの違いへ。根拠はRFC 3986 §§2.1–2.4、WHATWG URL Standard §5、ECMAScriptのdecodeURIComponentです。