パーセントエンコーディングとは?(RFC 3986 をわかりやすく解説)
パーセントエンコーディングは、URI 内の特殊文字をパーセント記号(%)と 2 桁の 16 進数に置き換えてエンコードする仕組みで、RFC 3986 で定義されています。これにより、URL がインターネット上を安全に送信できる有効な ASCII 文字だけで構成されることが保証されます。
パーセントエンコーディングとは?
パーセントエンコーディングは URL エンコーディングとも呼ばれ、URI(Uniform Resource Identifier)内で許可されていない文字や特別な意味を持つ文字を表現するための標準的な方法です。エンコードが必要な各文字を、パーセント記号(%)とその文字のバイト値を表す 2 桁の 16 進数に置き換えることで機能します。
たとえば、スペース文字(バイト値 0x20)は %20 としてエンコードされます。アットマーク @(バイト値 0x40)は %40 としてエンコードされます。この仕組みにより、あらゆるデータを URI の構文と衝突させることなく安全に URI へ埋め込むことができます。
「パーセントエンコーディング」という用語は、エスケープの接頭辞としてパーセント文字を使用することに由来します。正式な仕様は RFC 3986 で定義されており、これは 2005 年 1 月に IETF(Internet Engineering Task Force)によって公開され、Tim Berners-Lee、Roy Fielding、Larry Masinter によって執筆されました。
パーセントエンコーディングの仕組み
エンコードの処理は次の手順で進みます。まず、文字コード(現代の Web ではほぼ常に UTF-8)を使って文字をバイト表現に変換します。次に、各バイトをパーセント記号と 2 桁の 16 進数で表します(慣例として大文字の A〜F を使いますが、デコーダーは小文字も受け付けるべきです)。
// スペース文字を段階的にエンコードする例
// 文字: ' ' (スペース)
// ASCII/UTF-8 のバイト値: 0x20 (10進数で 32)
// パーセントエンコード後: %20
// マルチバイト文字を段階的にエンコードする例: アキュートアクセント付きの e
// 文字: アキュートアクセント付きの e (U+00E9)
// UTF-8 バイト列: 0xC3 0xA9
// パーセントエンコード後: %C3%A9
// 3 バイト文字を段階的にエンコードする例
// 文字: CJK 漢字 (U+4E2D)
// UTF-8 バイト列: 0xE4 0xB8 0xAD
// パーセントエンコード後: %E4%B8%AD
16 進数は必ず 2 桁のペアでなければなりません。パーセント記号が単独で存在したり、パーセント記号の後に 16 進数以外の文字が続いたりすると、それは無効であり、デコード時にエラーを引き起こします。これは JavaScript で発生する「URI malformed」エラーのよくある原因です。
パーセントエンコーディングは常に可逆です。デコーダーはパーセント記号を読み取り、続く 2 文字を 16 進数のバイト値として解釈し、元の文字に戻します。マルチバイトの UTF-8 文字の場合、連続してエンコードされたバイトはまとめて結合され、一緒にデコードされます。
どの文字をエンコードしなければならないのか?
すべての文字にパーセントエンコーディングが必要なわけではありません。RFC 3986 は 2 つのカテゴリーを定義しています。エンコードが決して不要な非予約文字(unreserved characters)と、予約された用途で使われていないときにエンコードが必要な予約文字(reserved characters)です。それ以外のすべての文字(スペース、非 ASCII 文字、制御文字など)は常にエンコードしなければなりません。
エンコードが不要な文字(非予約文字): 大文字(A-Z)、小文字(a-z)、数字(0-9)、ハイフン(-)、ピリオド(.)、アンダースコア(_)、チルダ(~)。これら 66 文字はエンコードせずに URI のどこにでも使用できます。
場合によってエンコードが必要な文字(予約文字): : / ? # [ ] @ ! $ & ' ( ) * + , ; =。これらの文字は URI 内で特別な構文上の意味を持ちます。区切り文字(デリミタ)としてではなくデータとして使う場合にのみエンコードが必要です。
常にエンコードが必要な文字: スペース、非 ASCII 文字(127 を超えるすべての文字)、制御文字(0〜31 および 127)、そして < > { } | \ ^ ` " のような安全でない文字です。
RFC 3986 における予約文字と非予約文字
予約文字と非予約文字の区別は、URI の動作を理解するうえで基本となります。予約文字は URI の構造を定義する区切り文字(デリミタ)として機能します。たとえば、:// はスキームとオーソリティを区切り、/ はパスセグメントを区切り、? はクエリ文字列を開始し、# はフラグメントを開始します。
| カテゴリー | 文字 | エンコードするタイミング |
|---|---|---|
| 非予約文字 | A-Z a-z 0-9 - . _ ~ |
不要(決してエンコードしない) |
| 汎用デリミタ | : / ? # [ ] @ |
区切り文字としてではなくデータとして使うとき |
| サブデリミタ | ! $ & ' ( ) * + , ; = |
それらに意味を割り当てるコンポーネント内でデータとして使うとき |
| その他すべての文字 | スペース、非 ASCII、制御文字など | 常に |
RFC 3986 の重要な原則として、予約文字がパーセントエンコードされているかそのまま記載されているかだけが異なる URI は、同等ではないというものがあります。たとえば、/path/to と /path%2Fto は、%2F が / にデコードされるにもかかわらず、異なる URI です。前者は 2 つのパスセグメントを持ち、後者はリテラルのスラッシュを含む 1 つのパスセグメントを持ちます。
パーセントエンコーディングと URL エンコーディングは同じもの?
「パーセントエンコーディング」と「URL エンコーディング」はしばしば同じ意味で使われ、ほとんどの文脈では同じものを指します。しかし、歴史的にわずかな違いがあります。「URL エンコーディング」は、HTML フォームで使われる古い application/x-www-form-urlencoded 形式を指すことがあり、この形式ではスペースを %20 ではなく + としてエンコードします。
application/x-www-form-urlencoded 形式は HTML 仕様で定義され、RFC 3986 よりも前から存在します。この形式は少し異なるルールを持ちます。スペースは + になり、エンコードされない文字の集合も若干異なります。現代における「URL エンコーディング」という用語の使用は、ほぼ常にスペースを %20 とする RFC 3986 のパーセントエンコーディングを指します。
実務上は、すべての URI の文脈(パスセグメント、REST API のクエリパラメータ、フラグメント識別子)で RFC 3986 のパーセントエンコーディングを使用すべきです。application/x-www-form-urlencoded 形式は、HTML フォームの送信を扱う場合や、特定の API がそれを要求する場合にのみ使用してください。
// RFC 3986 パーセントエンコーディング(URI に推奨)
encodeURIComponent('hello world') // "hello%20world"
// application/x-www-form-urlencoded(HTML フォーム)
new URLSearchParams({q: 'hello world'}).toString() // "q=hello+world"
// Python での同等の処理
from urllib.parse import quote, quote_plus
quote('hello world', safe='') # "hello%20world"
quote_plus('hello world') # "hello+world"