URL 編碼(URL Encoding),又稱百分比編碼(Percent-Encoding),是一種將字元轉換為 URL 安全格式的機制。由 IETF 在 RFC 3986 標準中定義,目的是讓所有字元都能在 URL 中安全傳輸,而不產生解析歧義。
URL 中只允許使用 ASCII 字元的一個子集。對於其他字元(如中文、日文、特殊符號),必須先將字元轉換為位元組序列,再以 %XX 的格式表示每個位元組,其中 XX 是兩位十六進位數字。
例如:中文「你好」的 UTF-8 位元組為 E4 BD A0 E5 A5 BD,URL 編碼結果為 %E4%BD%A0%E5%A5%BD。
RFC 3986(Uniform Resource Identifier: Generic Syntax)是 URL/URI 的核心標準文件,於 2005 年由 IETF 發布,至今仍是業界最權威的規範。它定義了 URI 的語法、保留字元的分類,以及何時需要進行百分比編碼。
根據 RFC 3986,URI 字元分為三類:
RFC 3986 將保留字元分為兩組:
| 字元 | 用途 | URL 編碼 |
|---|---|---|
: | 協議與主機分隔、port 分隔 | %3A |
/ | 路徑分隔符 | %2F |
? | query string 起始 | %3F |
# | fragment 起始 | %23 |
[ | IPv6 位址 | %5B |
] | IPv6 位址 | %5D |
@ | 使用者資訊分隔 | %40 |
| 字元 | 常見用途 | URL 編碼 |
|---|---|---|
! | 特殊標記 | %21 |
$ | 路徑參數 | %24 |
& | query 參數分隔 | %26 |
' | 字串標記 | %27 |
( | 群組標記 | %28 |
) | 群組標記 | %29 |
* | 萬用字元 | %2A |
+ | 空格(舊式) | %2B |
, | 列表分隔 | %2C |
; | 路徑參數 | %3B |
= | key=value 分隔 | %3D |
| 字元 | 原因 | URL 編碼 |
|---|---|---|
| 空格 | URL 不允許空格 | %20 或 + |
" | HTML 屬性引號衝突 | %22 |
< | HTML 標籤衝突 | %3C |
> | HTML 標籤衝突 | %3E |
{ } | 不安全字元 | %7B / %7D |
| | 不安全字元 | %7C |
\ | 路徑衝突 | %5C |
^ | 不安全字元 | %5E |
` | 不安全字元 | %60 |
中文等非 ASCII 字元的 URL 編碼分為兩個步驟:
「中」= UTF-8: E4 B8 AD → URL 編碼: %E4%B8%AD
「文」= UTF-8: E6 96 87 → URL 編碼: %E6%96%87
// encodeURIComponent - 最嚴格,連 URL 結構字元也編碼(推薦用於參數值)
encodeURIComponent('你好 world!')
// → "%E4%BD%A0%E5%A5%BD%20world!"
// encodeURI - 保留 URL 結構字元(用於完整 URL)
encodeURI('https://example.com/搜尋?q=你好')
// → "https://example.com/%E6%90%9C%E5%B0%8B?q=%E4%BD%A0%E5%A5%BD"
// 解碼
decodeURIComponent('%E4%BD%A0%E5%A5%BD')
// → "你好"
from urllib.parse import quote, unquote, urlencode
# 編碼單一值(safe='' 代表連 / 也要編碼)
quote('你好 world!', safe='')
# → '%E4%BD%A0%E5%A5%BD%20world%21'
# 解碼
unquote('%E4%BD%A0%E5%A5%BD')
# → '你好'
# 編碼整個 query string
params = {'name': '張三', 'city': '台北市'}
urlencode(params)
# → 'name=%E5%BC%B5%E4%B8%89&city=%E5%8F%B0%E5%8C%97%E5%B8%82'
<?php
// rawurlencode - 遵循 RFC 3986(推薦)
rawurlencode('你好 world!');
// → '%E4%BD%A0%E5%A5%BD%20world%21'
// urlencode - 將空格編碼為 + 而非 %20(用於 HTML form 資料)
urlencode('你好 world!');
// → '%E4%BD%A0%E5%A5%BD+world%21'
// 解碼
rawurldecode('%E4%BD%A0%E5%A5%BD');
// → '你好'
?>
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
// 編碼
String encoded = URLEncoder.encode("你好 world!", StandardCharsets.UTF_8);
// → "%E4%BD%A0%E5%A5%BD+world%21"(注意:Java 將空格編碼為 +)
// 解碼
String decoded = URLDecoder.decode("%E4%BD%A0%E5%A5%BD", StandardCharsets.UTF_8);
// → "你好"
import "net/url"
// 編碼
encoded := url.QueryEscape("你好 world!")
// → "%E4%BD%A0%E5%A5%BD+world%21"
// 路徑編碼(空格編碼為 %20)
pathEncoded := url.PathEscape("你好 world!")
// → "%E4%BD%A0%E5%A5%BD%20world%21"
// 解碼
decoded, _ := url.QueryUnescape("%E4%BD%A0%E5%A5%BD")
// → "你好"
錯誤 1:雙重編碼
將已編碼的字串再次編碼,導致 %25 出現(%25 是 % 的編碼)。
錯誤:encodeURIComponent('%E4%BD%A0') → %25E4%25BD%25A0
正確:只對原始未編碼的字串執行一次編碼。
錯誤 2:使用過時的 escape() 函式
escape() 不會正確處理非 ASCII 字元(如中文),且已被標準廢棄。
請改用 encodeURIComponent() 或 encodeURI()。
錯誤 3:搞混 encodeURI 和 encodeURIComponent
在 query string 中使用 encodeURI() 會導致 &、=、? 等分隔符未被編碼,造成參數解析錯誤。
query string 的參數值請務必使用 encodeURIComponent()。