什麼是 URL 編碼?
URL 編碼(URL Encoding),也稱為百分號編碼(Percent Encoding),是一種將特殊字元轉換為安全格式的機制,使其能夠在 URL 中正確傳輸。這個機制是網際網路運作的基礎技術之一,每次我們在瀏覽器中輸入包含中文、空格或特殊符號的網址時,URL 編碼都在背後默默運作。
舉例來說,當你搜尋「台北 101」時,瀏覽器實際發送的 URL 會是:
https://www.google.com/search?q=%E5%8F%B0%E5%8C%97+101
這裡的 %E5%8F%B0%E5%8C%97 就是「台北」經過 URL 編碼後的結果,而空格則被編碼為 + 符號。
歷史背景與技術需求
為什麼需要 URL 編碼?
URL 編碼的出現源於三個核心技術需求:
- ASCII 限制:早期的網際網路協定設計時,只考慮了 ASCII 字元集(0-127),無法直接處理非英文字元。
- 特殊字元衝突:URL 本身使用某些字元作為分隔符,如
?、&、=,這些字元如果出現在參數值中,會導致解析錯誤。 - 安全傳輸:某些字元在傳輸過程中可能被錯誤解釋或修改,需要轉換為安全格式。
RFC 3986 標準
目前的 URL 編碼規範由 RFC 3986(2005年發布)定義,取代了較舊的 RFC 1738 和 RFC 2396。這個標準明確規定了:
- 哪些字元可以直接使用(保留字元 vs 非保留字元)
- 如何對不安全字元進行編碼
- 不同 URL 組成部分的編碼規則
URL 結構深入解析
理解 URL 編碼之前,我們需要先了解 URL 的完整結構。一個標準 URL 包含以下組成部分:
scheme://user:password@host:port/path?query#fragment
範例:
https://user:[email protected]:8080/search/results?q=URL+encoding&page=1#section2
拆解:
- scheme(協定): https
- user(使用者): user
- password(密碼): pass
- host(主機): example.com
- port(端口): 8080
- path(路徑): /search/results
- query(查詢參數): q=URL+編碼&page=1
- fragment(錨點): section2
💡 重要觀念
不同的 URL 組成部分有不同的編碼規則。例如,路徑(path)中的斜線 / 不應該被編碼,但在查詢參數(query)中則需要編碼為 %2F。
編碼規則與字元集
保留字元(Reserved Characters)
RFC 3986 定義了以下保留字元,它們在 URL 中具有特殊意義:
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
這些字元在作為數據內容時必須被編碼,但在作為 URL 結構分隔符時則不應編碼。
非保留字元(Unreserved Characters)
這些字元可以直接使用,無需編碼:
A-Z a-z 0-9 - _ . ~
編碼格式
URL 編碼使用百分號 % 後接兩個十六進制數字來表示字元。編碼步驟:
- 將字元轉換為 UTF-8 字節序列
- 將每個字節轉換為 %XX 格式(XX 為十六進制)
編碼範例
字元: 「中」
UTF-8 編碼: E4 B8 AD(3個字節)
URL 編碼: %E4%B8%AD
字元: 空格
ASCII: 32(0x20)
URL 編碼: %20(或在查詢字串中使用 +)
字元: @
ASCII: 64(0x40)
URL 編碼: %40(當作為數據而非分隔符時)
常見字元編碼對照表
| 字元 | 說明 | 編碼結果 |
|---|---|---|
| 空格 | 最常見的編碼字元 | %20 或 + |
| ! | 驚嘆號 | %21 |
| " | 雙引號 | %22 |
| # | 井號(錨點標記) | %23 |
| $ | 美元符號 | %24 |
| % | 百分號 | %25 |
| & | And符號(參數分隔) | %26 |
| = | 等號(鍵值分隔) | %3D |
| ? | 問號(查詢開始) | %3F |
| / | 斜線(路徑分隔) | %2F |
常見應用場景
1. 表單提交
當使用者提交包含特殊字元的表單時,瀏覽器會自動進行 URL 編碼:
姓名:張三
電子郵件:[email protected]
https://example.com/submit?name=%E5%BC%B5%E4%B8%89&email=user%40example.com
2. API 請求
呼叫 RESTful API 時,參數值必須正確編碼:
// 原始參數
{
search: "iPhone 15 Pro",
category: "手機&平板"
}
// 正確的 API URL
https://api.example.com/products?search=iPhone+15+Pro&category=%E6%89%8B%E6%A9%9F%26%E5%B9%B3%E6%9D%BF
3. 動態 URL 生成
在生成包含用戶輸入的 URL 時,必須進行編碼以防止注入攻擊:
// 用戶輸入
const userInput = "../../etc/passwd";
// 錯誤做法(易受攻擊)
const badUrl = `/file/${userInput}`;
// 結果: /file/../../etc/passwd (路徑穿越攻擊)
// 正確做法
const goodUrl = `/file/${encodeURIComponent(userInput)}`;
// 結果: /file/..%2F..%2Fetc%2Fpasswd (安全)
程式實作範例
JavaScript 實作
// 1. 基本編碼/解碼
const original = "搜尋:JavaScript & Node.js";
const encoded = encodeURIComponent(original);
const decoded = decodeURIComponent(encoded);
console.log(encoded);
// %E6%90%9C%E5%B0%8B%EF%BC%9AJavaScript%20%26%20Node.js
console.log(decoded);
// 搜尋:JavaScript & Node.js
// 2. 完整 URL 編碼(保留協定和域名)
const fullUrl = "https://example.com/搜尋?q=測試";
const encodedUrl = encodeURI(fullUrl);
console.log(encodedUrl);
// https://example.com/%E6%90%9C%E5%B0%8B?q=%E6%B8%AC%E8%A9%A6
// 3. 物件轉查詢字串
function objectToQueryString(params) {
return Object.keys(params)
.map(key => {
const value = params[key];
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join('&');
}
const params = {
搜尋: "URL 編碼",
分類: "技術文章",
頁碼: 1
};
console.log(objectToQueryString(params));
// %E6%90%9C%E5%B0%8B=URL+%E7%B7%A8%E7%A2%BC&...
Python 實作
# 1. 基本編碼/解碼
from urllib.parse import quote, unquote, urlencode
original = "搜尋:Python & Django"
encoded = quote(original)
decoded = unquote(encoded)
print(encoded)
# %E6%90%9C%E5%B0%8B%EF%BC%9APython%20%26%20Django
print(decoded)
# 搜尋:Python & Django
# 2. 字典轉查詢字串
params = {
'搜尋': 'URL 編碼',
'分類': '技術文章',
'頁碼': 1
}
query_string = urlencode(params)
print(query_string)
# 3. 完整 URL 建構
from urllib.parse import urlparse, urlunparse, parse_qs
def build_url(base_url, params):
parsed = urlparse(base_url)
query = urlencode(params)
return urlunparse((
parsed.scheme,
parsed.netloc,
parsed.path,
parsed.params,
query,
parsed.fragment
))
url = build_url('https://example.com/api', {'q': '測試', 'lang': 'zh'})
print(url)
# https://example.com/api?q=%E6%B8%AC%E8%A9%A6&lang=zh
PHP 實作
<?php
// 1. 基本編碼/解碼
$original = "搜尋:PHP & Laravel";
$encoded = urlencode($original);
$decoded = urldecode($encoded);
echo $encoded;
// %E6%90%9C%E5%B0%8B%EF%BC%9APHP+%26+Laravel
echo $decoded;
// 搜尋:PHP & Laravel
// 2. 陣列轉查詢字串
$params = [
'搜尋' => 'URL 編碼',
'分類' => '技術文章',
'頁碼' => 1
];
$query_string = http_build_query($params);
echo $query_string;
// 3. 安全的 URL 建構
function buildSafeUrl($base, $params) {
$query = http_build_query($params);
$separator = (strpos($base, '?') === false) ? '?' : '&';
return $base . $separator . $query;
}
$url = buildSafeUrl('https://example.com/api', ['q' => '測試', 'lang' => 'zh']);
echo $url;
// https://example.com/api?q=%E6%B8%AC%E8%A9%A6&lang=zh
?>
最佳實踐與安全考量
1. 選擇正確的編碼函數
JavaScript
encodeURI():編碼完整 URL,保留: / ? # [ ] @ ! $ & ' ( ) * + , ; =encodeURIComponent():編碼 URL 組件(參數值),編碼所有特殊字元- 建議:參數值使用
encodeURIComponent()
Python
urllib.parse.quote():基本編碼,可指定安全字元urllib.parse.quote_plus():將空格編碼為+- 建議:查詢字串使用
urlencode(),路徑使用quote()
2. 防止雙重編碼
// 錯誤:重複編碼
const text = "測試";
const encoded1 = encodeURIComponent(text); // %E6%B8%AC%E8%A9%A6
const encoded2 = encodeURIComponent(encoded1); // %25E6%2598%258C%25E8%25A9%25A6
// 正確:檢查是否已編碼
function safeEncode(str) {
// 簡單檢查:如果包含 %XX 格式,可能已編碼
if (/%[0-9A-F]{2}/i.test(str)) {
return str; // 已編碼,直接返回
}
return encodeURIComponent(str);
}
3. 處理特殊情況
// 空值處理
function encodeParam(value) {
if (value === null || value === undefined) {
return '';
}
return encodeURIComponent(String(value));
}
// 陣列參數處理
const filters = ['JavaScript', 'Python', 'PHP'];
const query = filters.map(f => `lang=${encodeURIComponent(f)}`).join('&');
// lang=JavaScript&lang=Python&lang=PHP
4. 安全性注意事項
⚠️ 安全警告
- 永遠編碼用戶輸入:防止 URL 注入攻擊
- 驗證解碼後的數據:不要盲目信任解碼後的內容
- 使用白名單:對關鍵參數進行格式驗證
- 避免敏感資訊:密碼等敏感資料不應出現在 URL 中
5. 效能優化建議
- 快取編碼結果:對於重複使用的字串,快取編碼結果
- 批次處理:使用專門的函數一次處理整個參數物件
- 延遲編碼:只在真正需要發送請求時才進行編碼
結論
URL 編碼是 Web 開發中不可或缺的技術。正確理解和應用 URL 編碼,不僅能確保應用程式的正確運作,還能有效防範安全漏洞。
核心要點回顧
- URL 編碼使用
%XX格式,遵循 RFC 3986 標準 - 不同 URL 組件有不同的編碼規則
- 選擇適合的編碼函數(encodeURIComponent vs encodeURI)
- 永遠對用戶輸入進行編碼,防止安全漏洞
- 注意雙重編碼和解碼問題