メインコンテンツまでスキップ

2. 文字 (Characters)

本章では、URIで使用される文字セット、エンコーディング機構、および処理規則を定義します。


文字エンコーディングの基礎

URI構文は、データ(リソースの識別に使用される可能性がある)を文字列にエンコードする方法を提供します。

エンコーディング階層:

リソース → URI文字 → オクテット → 転送/ストレージ

文字セット: URIは、数字、文字、およびいくつかのグラフィック記号で構成されるUS-ASCII文字セットに基づいています


2.1. パーセントエンコーディング (Percent-Encoding)

目的

パーセントエンコーディング機構は、オクテットに対応する文字が許可されたセットの外にある場合、または区切り文字として使用されている場合に、コンポーネント内のデータオクテットを表すために使用されます。

エンコーディング形式

pct-encoded = "%" HEXDIG HEXDIG

形式: パーセント文字 "%" の後に、そのオクテットの数値を表す2桁の16進数が続きます

文字2進数16進数パーセントエンコード
スペース001000000x20%20
!001000010x21%21
#001000110x23%23
(日本語)-0xE38182%E3%81%82

大文字小文字規則

等価性: 大文字の16進数字 'A' から 'F' は、小文字の数字 'a' から 'f' と等価です

正規化: パーセントエンコードされたオクテットで使用される16進数字の大文字小文字のみが異なる2つのURIは等価です

推奨事項: URIプロデューサーとノーマライザーは、すべてのパーセントエンコーディングに大文字の16進数字を使用すべきです (SHOULD)

推奨: %2F %3A %5B
非推奨: %2f %3a %5b

2.2. 予約文字 (Reserved Characters)

定義

URIには、「予約」セット内の文字で区切られたコンポーネントとサブコンポーネントが含まれます。これらの文字は、区切り文字として定義される可能性がある(または定義されない可能性がある)ため、「予約」と呼ばれます。

予約文字セット

reserved    = gen-delims / sub-delims

gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"

sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="

分類

汎用デリミタ (gen-delims)

文字目的
:スキームとオーソリティの区切りhttp:
/パス区切り文字/path/to/resource
?クエリ区切り文字?key=value
#フラグメント区切り文字#section
[ ]IPv6アドレス境界[2001:db8::1]
@ユーザー情報区切り文字user@host

サブデリミタ (sub-delims)

文字一般的な用途
! $ ' ( ) *パスまたはクエリ内のサブコンポーネント区切り
+スペースの代替表現
,リスト区切り
;パラメータ区切り
=キーと値の区切り
&クエリパラメータ区切り

エンコーディング規則

競合処理: URIコンポーネントのデータが予約文字の区切り文字としての目的と競合する場合、URIを形成する前に競合するデータをパーセントエンコードしなければなりません (MUST)

:

"?"文字を含むパス:
元: /path/file?.txt
エンコード: /path/file%3F.txt

"&"文字を含むクエリ:
元: ?name=Tom&Jerry
正しい: ?name=Tom%26Jerry (&が区切り文字でない場合)
または: ?name=Tom&name=Jerry (&が区切り文字の場合)

等価性

重要: 予約文字を対応するパーセントエンコードされたオクテットに置き換える点で異なるURIは等価ではありません

http://example.com/path?key=value
http://example.com/path%3Fkey=value

これら2つのURIは等価ではありません

2.3. 非予約文字 (Unreserved Characters)

定義

URIで許可されているが、予約された目的を持たない文字は、非予約文字と呼ばれます。

非予約文字セット

unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"

含まれるもの:

  • ALPHA: 大文字と小文字 (A-Z, a-z)
  • DIGIT: 10進数字 (0-9)
  • -: ハイフン
  • .: ピリオド
  • _: アンダースコア
  • ~: チルダ

エンコーディング規則

等価性: 非予約文字を対応するパーセントエンコードされたUS-ASCIIオクテットに置き換える点で異なるURIは等価です

正規化: 非予約文字に対応するパーセントエンコードされたオクテットはデコードすべきです (SHOULD)

等価なURI:
http://example.com/~user
http://example.com/%7Euser

正規化後:
http://example.com/~user

パーセントエンコーディング範囲

作成すべきでない (SHOULD NOT):

  • ALPHA: %41-%5A (A-Z), %61-%7A (a-z)
  • DIGIT: %30-%39 (0-9)
  • ハイフン: %2D
  • ピリオド: %2E
  • アンダースコア: %5F
  • チルダ: %7E

デコードすべき: URI内でこれらのエンコーディングが見つかった場合、ノーマライザーは対応する非予約文字にデコードすべきです (SHOULD)


2.4. エンコードまたはデコードのタイミング (When to Encode or Decode)

エンコードのタイミング

URIプロデューサー:

  1. URIを生成する際、許可されていない文字をパーセントエンコードしなければなりません (MUST)
  2. 予約文字は区切り文字として使用される場合のみエンコードされません
  3. 非予約文字はエンコードすべきではありません (SHOULD NOT)

:

# パスのエンコード
path = "/files/my document.pdf"
encoded = "/files/my%20document.pdf"

# クエリのエンコード
query = "?name=John Doe&age=30"
encoded = "?name=John%20Doe&age=30"

デコードのタイミング

URIコンシューマー:

  1. URIを解析した後、必要に応じてコンポーネントをデコードします
  2. 早すぎるデコードは避けてください(URI構造が変わる可能性があります)
  3. 各コンポーネントを1回だけデコードします

危険な例:

元: /path%2Fto%2Ffile
早すぎるデコード: /path/to/file (パス構造が変わりました!)

正しい: 最初に解析し、次に各セグメントをデコード
セグメント1: "path%2Fto%2Ffile" → デコード → "path/to/file"

二重エンコーディング問題

元データ: "100%"
最初のエンコード: "100%25"
誤った2回目のエンコード: "100%2525"

デコード時:
"100%2525" → "100%25" → "100%"

2.5. データの識別 (Identifying Data)

文字セットとエンコーディング

文字 vs オクテット:

  • URIは文字列です
  • 文字は転送/ストレージのためにオクテットとしてエンコードされます
  • UTF-8が推奨される文字エンコーディングです

国際化リソース識別子 (IRI)

IRI拡張: RFC 3987はIRIを定義し、Unicode文字の使用を許可します

変換:

IRI: http://例え.jp/引き出し
↓ UTF-8にエンコードしてパーセントエンコード
URI: http://xn--r8jz45g.jp/%E5%BC%95%E3%81%8D%E5%87%BA%E3%81%97

ベストプラクティス

URI生成:

  1. 非ASCII文字をUTF-8でエンコードします
  2. 結果のオクテットをパーセントエンコードします
  3. 大文字の16進数字を使用します
  4. 非予約文字はエンコードしません

URI消費:

  1. コンポーネントごとに解析します
  2. パーセントエンコーディングをデコードします
  3. UTF-8を使用してオクテットを解釈します
  4. 無効なエンコーディングを処理します

文字セットクイックリファレンス

完全な文字分類

URI文字
├── 非予約文字 (unreserved)
│ ├── ALPHA: A-Z, a-z
│ ├── DIGIT: 0-9
│ └── 記号: - . _ ~

├── 予約文字 (reserved)
│ ├── 汎用デリミタ (gen-delims): : / ? # [ ] @
│ └── サブデリミタ (sub-delims): ! $ & ' ( ) * + , ; =

└── パーセントエンコード (pct-encoded): %HEXDIG HEXDIG

エンコーディング決定木

文字をURIに含める必要がある?
├─ 非予約文字? → 直接使用
├─ 予約文字?
│ ├─ 区切り文字として使用? → 直接使用
│ └─ データとして使用? → パーセントエンコード
└─ その他の文字? → パーセントエンコード

一般的な文字エンコーディング表

文字目的エンコーディング
スペース区切り%20 または + (クエリ内)
!サブデリミタ%21 (エンコードが必要な場合)
"引用符%22
#フラグメント区切り%23 (データ内)
$サブデリミタ%24 (エンコードが必要な場合)
%エンコーディングマーカー%25
&パラメータ区切り%26 (データ内)
'サブデリミタ%27 (エンコードが必要な場合)
( )サブデリミタ%28 %29
+スペース/サブデリミタ%2B (データ内)
,リスト区切り%2C (エンコードが必要な場合)
/パス区切り%2F (データ内)
:スキーム区切り%3A (データ内)
;パラメータ区切り%3B (エンコードが必要な場合)
=キー値区切り%3D (エンコードが必要な場合)
?クエリ区切り%3F (データ内)
@ユーザー情報区切り%40 (データ内)
[ ]IPv6境界%5B %5D

実装の推奨事項

エンコーディング実装

def percent_encode(text, safe=''):
"""テキストをパーセントエンコードする"""
result = []
for char in text:
if char in safe or is_unreserved(char):
result.append(char)
else:
# UTF-8エンコードしてパーセントエンコード
for byte in char.encode('utf-8'):
result.append(f'%{byte:02X}')
return ''.join(result)

def is_unreserved(char):
"""文字が非予約文字かどうかをチェック"""
return (char.isalnum() or
char in '-._~')

デコーディング実装

def percent_decode(text):
"""テキストをパーセントデコードする"""
result = bytearray()
i = 0
while i < len(text):
if text[i] == '%' and i + 2 < len(text):
try:
byte = int(text[i+1:i+3], 16)
result.append(byte)
i += 3
except ValueError:
result.extend(text[i].encode('utf-8'))
i += 1
else:
result.extend(text[i].encode('utf-8'))
i += 1
return result.decode('utf-8', errors='replace')

次の章: 3. 構文コンポーネント (Syntax Components) - URIの構造コンポーネント