URLエンコーディング完全ガイド:パーセントエンコーディングの仕組み

9 min2026年5月14日

URLエンコーディングとは何か

URLエンコーディング(正式にはパーセントエンコーディング)は、URLで安全に使用できない文字を%記号と16進数の組み合わせに変換する仕組みです。例えばスペースは%20に、日本語の「東京」はUTF-8バイト列を経て%E6%9D%B1%E4%BA%ACになります。この変換がなければ、検索クエリに空白や特殊文字を含めることすらできません。

RFC 3986(2005年)がURIの構文を定義しています。URLで特別な意味を持つ予約文字(:、/、?、#、@、!、$、&、(、)、*、+、,、;、=)と、エンコード不要な非予約文字(A-Z、a-z、0-9、-、_、.、~)が明確に規定されています。それ以外の文字はすべてパーセントエンコードする必要があります。

ここで重要なのは、URLエンコーディングはバイトレベルの操作だということです。文字ではなくバイトをエンコードします。日本語の「あ」はUTF-8で3バイト(E3 81 82)なので、%E3%81%82になります。文字エンコーディング(UTF-8、Shift_JIS、EUC-JPなど)によって同じ文字が異なるバイト列になり、結果としてパーセントエンコードも変わります。現代のWebではUTF-8が事実上の標準です。

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

アルゴリズムは単純です。1)文字をUTF-8バイト列に変換する。2)非予約文字以外の各バイトを%HHの形式(HHは16進数の大文字)に変換する。例えば「café」の「é」はUTF-8で2バイト(C3 A9)なので%C3%A9になります。結果は「caf%C3%A9」です。

予約文字の扱いはコンテキストに依存します。URLのパス部分では/は区切り文字として機能するためエンコードしません。しかしクエリパラメータの値として/を含める場合は%2Fにエンコードする必要があります。同様に、&はクエリ文字列のパラメータ区切りですが、パラメータの値に&を含める場合は%26にエンコードします。

スペースの扱いは歴史的に混乱の原因です。RFC 3986ではスペースは%20にエンコードされます。しかしHTMLフォーム(application/x-www-form-urlencoded)ではスペースは+に変換されます。これはRFC 3986ではなくHTML仕様の規則です。サーバーサイドでは両方のケースを処理する必要があります。JavaScriptのencodeURIComponent()は%20を使い、フォームのsubmitは+を使います。

デコードは逆の処理です。%HHを見つけたら16進数のHHをバイト値に変換し、連続する%HH列をUTF-8バイト列としてデコードします。+をスペースに変換するかどうかはコンテキスト次第です(URLのパスでは+はリテラルの+、クエリ文字列ではスペースを意味する場合がある)。

// パーセントエンコーディングの基本
const text = "東京 タワー";

// UTF-8バイト列に変換してからエンコード
encodeURIComponent(text);
// "%E6%9D%B1%E4%BA%AC%20%E3%82%BF%E3%83%AF%E3%83%BC"

// デコード
decodeURIComponent("%E6%9D%B1%E4%BA%AC%20%E3%82%BF%E3%83%AF%E3%83%BC");
// "東京 タワー"

// スペースの扱いの違い
encodeURIComponent("hello world"); // "hello%20world"
// HTMLフォームのsubmitでは: "hello+world"

// +とスペースの変換(フォームデータ用)
function decodeFormValue(str) {
  return decodeURIComponent(str.replace(/\+/g, '%20'));
}

encodeURIとencodeURIComponentの違い

JavaScriptにはURL関連のエンコード関数が2つあります。encodeURI()はURL全体をエンコードするために設計されており、URLの構造文字(:、/、?、#、@など)をエンコードしません。encodeURIComponent()はURLの一部分(クエリパラメータの値など)をエンコードするために設計されており、予約文字もエンコードします。

実務上の使い分けは明確です。クエリパラメータの値をエンコードする場合はencodeURIComponent()を使います。URL全体を人が読める形式からエンコードする場合はencodeURI()を使います。間違えた場合の結果:encodeURI()でパラメータ値をエンコードすると、値に含まれる&や=がパラメータ区切りとして誤解されます。encodeURIComponent()でURL全体をエンコードすると、://がエンコードされてURLとして機能しなくなります。

古い関数のescape()とunescape()は非推奨です。これらはUTF-8ではなくLatin-1を想定しており、日本語などのマルチバイト文字を正しく処理しません。レガシーコードで見かけても、encodeURIComponent()/decodeURIComponent()に置き換えてください。

Node.jsではquerystring.escape()(非推奨)の代わりにURLSearchParamsを使うのが現代的です。ブラウザでも同様に、クエリ文字列の構築にはURLSearchParamsが最も安全です。手動でのエンコード忘れを防げます。

// encodeURI vs encodeURIComponent の違い
const baseUrl = "https://example.com/search";
const query = "price >= 100 & category = 電子機器";

// ❌ encodeURIではパラメータ値のエンコードが不十分
encodeURI(baseUrl + "?q=" + query);
// "https://example.com/search?q=price%20%3E=%20100%20&%20category%20=%20%E9%9B%BB..."
// &がそのまま残り、意図しないパラメータ区切りになる

// ✅ encodeURIComponentでパラメータ値をエンコード
baseUrl + "?q=" + encodeURIComponent(query);
// "https://example.com/search?q=price%20%3E%3D%20100%20%26%20category..."

// ✅ URLSearchParamsが最も安全
const url = new URL(baseUrl);
url.searchParams.set("q", query);
url.toString();
// "https://example.com/search?q=price+%3E%3D+100+%26+category+%3D+%E9%9B%BB..."

日本語URLの扱い方

日本語を含むURLは特別な注意が必要です。ブラウザのアドレスバーでは「https://example.com/東京」と表示されても、実際のHTTPリクエストでは「https://example.com/%E6%9D%B1%E4%BA%AC」として送信されます。これはIRI(Internationalized Resource Identifier、RFC 3987)からURI(RFC 3986)への変換です。

ドメイン名に日本語を使う場合はPunycode(RFC 3492)が使われます。「日本語.jp」は内部的に「xn--wgv71a309e.jp」に変換されます。これはDNSが元々ASCII専用に設計されているためです。ブラウザは表示上は日本語ドメインを見せますが、通信ではPunycodeが使われます。Node.jsではurl.domainToASCII()で変換できます。

APIを設計する際、パスに日本語を含める場合はUTF-8でパーセントエンコードするのが標準です。フレームワーク(Express、Next.js、Djangoなど)は通常、リクエスト受信時に自動デコードしてくれます。ただし、リバースプロキシ(Nginx、Apache)が二重デコードする場合があるので注意が必要です。リクエストが%252Fを含む場合、それは%2F(/のエンコード)をさらにエンコードしたもので、二重エンコードの兆候です。

ファイル名に日本語を含むファイルをダウンロードさせる場合、Content-Dispositionヘッダーの指定が問題になります。RFC 6266ではfilename*=UTF-8''%E6%9D%B1%E4%BA%AC.pdfの形式が定められていますが、古いブラウザはこれをサポートしていない場合があります。filenameとfilename*の両方を指定するのが安全なアプローチです。

よくある落とし穴と対処法

二重エンコード問題:最も頻繁に遭遇するバグです。既にエンコードされたURLをもう一度エンコードすると、%が%25になります。「%20」が「%2520」になり、サーバーでデコードすると「%20」という文字列(スペースではなく)が得られます。対策:エンコードする前に、入力が既にエンコード済みかどうかを確認するか、常に生の文字列からエンコードするワークフローを徹底してください。

パス区切りのエンコード問題:encodeURIComponent()はスラッシュもエンコードするため、パス全体をこの関数に渡すと壊れます。「/api/users/田中太郎」をencodeURIComponent()に渡すと「%2Fapi%2Fusers%2F%E7%94%B0%E4%B8%AD%E5%A4%AA%E9%83%8E」になり、サーバーはこれを単一のパスセグメントとして解釈します。対策:パスの各セグメントを個別にエンコードしてから結合してください。

フラグメント(#)の問題:URLの#以降はブラウザがサーバーに送信しません。クエリパラメータに#を含めたい場合は%23にエンコードする必要があります。encodeURIComponent("#section")は"%23section"を返すので正しく動作しますが、手動でURL文字列を構築する場合は忘れがちです。

プラス記号(+)の曖昧性:クエリ文字列中の+はapplication/x-www-form-urlencodedではスペースを意味しますが、RFC 3986ではリテラルの+です。APIにGETリクエストで「C++」を送信する場合、+がスペースに変換されて「C 」になる可能性があります。encodeURIComponent("C++")は"C%2B%2B"を返すため、明示的にエンコードすれば安全です。

各言語でのURLエンコーディング

JavaScript:encodeURIComponent()とdecodeURIComponent()が標準です。URLSearchParamsクラスはクエリ文字列の構築とパースに最適で、エンコード/デコードを自動的に処理します。新しいURLオブジェクトとの組み合わせにより、手動エンコードがほぼ不要になります。

Python:urllib.parse.quote()とurllib.parse.unquote()を使います。quote()のデフォルトではスラッシュをエンコードしません(safe="/")。すべての予約文字をエンコードするにはquote(string, safe="")を指定します。requestsライブラリはパラメータのエンコードを自動処理するため、params辞書を渡すのが安全です。

Go:net/urlパッケージのurl.QueryEscape()でクエリ値をエンコード、url.PathEscape()でパスセグメントをエンコードします。url.Values型を使ってクエリ文字列を構築するのが推奨パターンです。

Java:URLEncoder.encode()は実はapplication/x-www-form-urlencodedのエンコードで、スペースを+に変換します。RFC 3986準拠のエンコードにはURI.toASCIIString()か、Spring FrameworkのUriUtils.encodePathSegment()を使います。Java 11以降のHttpClientはURI構築時にエンコードを自動処理してくれます。

// 各場面での正しいエンコード方法

// 1. クエリパラメータの値
const searchTerm = "東京 タワー & スカイツリー";
const url1 = `https://api.example.com/search?q=${encodeURIComponent(searchTerm)}`;
// "https://api.example.com/search?q=%E6%9D%B1%E4%BA%AC%20%E3%82%BF..."

// 2. パスセグメント(各セグメントを個別にエンコード)
const segments = ["users", "田中太郎", "profile"];
const path = segments.map(s => encodeURIComponent(s)).join("/");
// "users/%E7%94%B0%E4%B8%AD%E5%A4%AA%E9%83%8E/profile"

// 3. URLSearchParamsを使う(最も安全)
const params = new URLSearchParams({
  q: "東京 タワー",
  category: "観光 & グルメ",
  page: "1"
});
params.toString();
// "q=%E6%9D%B1%E4%BA%AC+%E3%82%BF%E3%83%AF%E3%83%BC&category=..."

// 4. URLオブジェクトで完全なURL構築
const fullUrl = new URL("https://example.com/search");
fullUrl.searchParams.set("q", "C++ プログラミング");
fullUrl.toString();
// "https://example.com/search?q=C%2B%2B+%E3%83%97%E3%83%AD..."

セキュリティ上の注意点

パストラバーサル攻撃:攻撃者がURLに「..%2F..%2F..%2Fetc%2Fpasswd」を含めることで、パス検証をバイパスしようとします。サーバーが1回デコードすると「../../../etc/passwd」になります。対策:デコード後にパスの正規化とバリデーションを行い、許可されたディレクトリ外へのアクセスを防いでください。

ダブルエンコーディング攻撃:一部のWAF(Web Application Firewall)は%252Fのような二重エンコードを正しく処理できず、バイパスされる可能性があります。アプリケーション層で完全にデコードしてからバリデーションを行うか、WAFとアプリケーションのデコード動作を一致させることが重要です。

オープンリダイレクト:URLパラメータにリダイレクト先を含める場合(?redirect=https%3A%2F%2Fevil.com)、必ずホワイトリストで許可されたドメインかどうかを検証してください。エンコードされた状態でもデコードした状態でも検証する必要があります。攻撃者はエンコーディングのバリエーションを使って検証をバイパスしようとします。

HTTPヘッダーインジェクション:URLの一部がHTTPヘッダーに反映される場合、改行文字(%0D%0A)のインジェクションでヘッダーを追加される可能性があります。ユーザー入力をHTTPヘッダーに含める前に、改行文字を除去またはエスケープしてください。モダンなHTTPライブラリの多くはこれを自動的に防いでくれますが、低レベルのHTTP操作では注意が必要です。

デバッグのヒント

エンコード/デコードの問題を調査する際の最初のステップは、実際にネットワークを流れているバイト列を確認することです。ブラウザのDevToolsのNetworkタブでリクエストURLを確認し、「view source」でエンコードされた形式を見てください。表示が人間に読みやすく変換されている場合があるので注意が必要です。

curlでのテスト時は--data-urlencodeオプションが便利です。これはパラメータ値を自動的にURLエンコードします。手動でエンコードしたURLをcurlに渡す場合は、シェルによる展開を防ぐためにシングルクォートで囲んでください。

文字化けの原因切り分け:レスポンスが文字化けする場合、1)サーバーのエンコーディング設定(Content-Type: charset=utf-8)を確認、2)データベースのエンコーディング設定を確認、3)URLデコード時のエンコーディング指定を確認してください。UTF-8以外のエンコーディング(Shift_JISなど)でエンコードされたURLパラメータを、UTF-8としてデコードしようとすると文字化けします。

当サイトのURLエンコーダーツールを使えば、文字列のエンコード/デコード結果をすぐに確認できます。問題が発生した場合は、各段階(入力→エンコード→送信→受信→デコード→出力)のどこで変換が壊れているかを特定することが解決への近道です。