URL結構分解圖,顯示協議、域名、路徑、查詢參數等組成部分
URL結構分解圖,顯示協議、域名、路徑、查詢參數等組成部分

什麼是 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 編碼的出現源於三個核心技術需求:

  1. ASCII 限制:早期的網際網路協定設計時,只考慮了 ASCII 字元集(0-127),無法直接處理非英文字元。
  2. 特殊字元衝突:URL 本身使用某些字元作為分隔符,如 ?&=,這些字元如果出現在參數值中,會導致解析錯誤。
  3. 安全傳輸:某些字元在傳輸過程中可能被錯誤解釋或修改,需要轉換為安全格式。

RFC 3986 標準

目前的 URL 編碼規範由 RFC 3986(2005年發布)定義,取代了較舊的 RFC 1738 和 RFC 2396。這個標準明確規定了:

  • 哪些字元可以直接使用(保留字元 vs 非保留字元)
  • 如何對不安全字元進行編碼
  • 不同 URL 組成部分的編碼規則
並排比較原始文本、錯誤編碼和正確編碼的URL示例
並排比較原始文本、錯誤編碼和正確編碼的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 編碼使用百分號 % 後接兩個十六進制數字來表示字元。編碼步驟:

  1. 將字元轉換為 UTF-8 字節序列
  2. 將每個字節轉換為 %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 編碼,不僅能確保應用程式的正確運作,還能有效防範安全漏洞。

核心要點回顧

  1. URL 編碼使用 %XX 格式,遵循 RFC 3986 標準
  2. 不同 URL 組件有不同的編碼規則
  3. 選擇適合的編碼函數(encodeURIComponent vs encodeURI)
  4. 永遠對用戶輸入進行編碼,防止安全漏洞
  5. 注意雙重編碼和解碼問題