百分號編碼檢測器

百分號編碼不是一條規則,而是好幾條,它們在大約十幾個字符上彼此衝突。本頁把同一段文本分別交給 encodeURIComponent、encodeURI 與 application/x-www-form-urlencoded 序列化並排跑一遍,標出它們意見不一致的字符,逐字符打印一行(含其 UTF-8 字節與 RFC 3986 分類),並說明一個字符串被編碼了幾次。解碼一側用嚴格或寬鬆規則讀取字符串,告訴你哪個轉義在哪裡出了問題,而不是悄悄返回一個錯誤結果。
所有處理都在你的瀏覽器本地完成——你的數據絕不會上傳到任何服務器。

不確定百分號編碼是什麼?用大白話解釋百分號編碼——它是什麼、為什麼空格會變成 %20 或 +、以及 URL 裡出現 %25 意味著什麼。
它的作用:
如何讀取這些轉義:
encodeURIComponent
encodeURI
表單(application/x-www-form-urlencoded)
在第一個輸入框裡按 Ctrl+Enter 會執行當前模式。
選一個模式並點選它的執行按鈕。編碼並檢測展示文本的三種編碼,外加每個字元一行;解碼把轉義讀回來並統計層數。「自檢」按鈕會重跑每一個記錄樣本、每一個可列印 ASCII 字元,以及本頁資料檔案裡記錄的層鏈,並把計數報在這裡。
編碼器們在哪裡不一致:只有下面這些字元會被三種方法區別對待。其餘的一切,要麼在三個編碼器裡都原樣通過,要麼被三個編碼器以完全相同的方式轉義。
每個字元一行:一個字元變成的 UTF-8 位元組、RFC 3986 把它歸入的類屬,以及每個上下文寫出的轉義。四個上下文列是 WHATWG URL 的百分號編碼集合,也就是瀏覽器構建路徑、查詢或片段時使用的那幾組;灰底行表示三個編碼器對它處理不同。
本頁為 URL 各部分轉義的確切集合:直接從編碼器本身打印出來,所以這張表與填充上面幾列的程式碼不可能脫節。每個集合還會轉義 C0 控制字元(U+0000 到 U+001F)以及所有高於 U+007E 的碼位,這正是非 ASCII 文本在任何上下文裡都會被轉義的原因。
這些集合由 WHATWG 的百分號編碼集合推導而來,並在兩處刻意與之不同,兩處都讓手工組裝更安全:查詢裡的井號會被轉義,因為裸井號會開啟片段;component 集合被釘死為 encodeURIComponent 所轉義的那些字符(包括撇號),而 form 集合則被釘死為 URLSearchParams 所轉義的那些。所以上面表格裡的兩列是平臺自身的行為,而不是對它的一次重新實現。這份清單還解釋了一個讓人意外的結果:百分號會被 encodeURIComponent 與 encodeURI 轉義,但它不在 WHATWG 的任何上下文集合裡,所以逐字符表的 path、query、fragment 三列會放過它。你手工組裝的 URL 裡如果有一個裸百分號,仍然有風險,因為標準要求輸入事先經過校驗:查詢中的 %41 對許多服務器框架來說就是單個 A,而本頁自己的嚴格解讀會把孤立的 % 視為損壞的轉義。自己把它寫成 %25 是更安全的習慣,逐字符表會並排展示這兩種寫法。
這段字串被編碼了多少次:每一輪解碼就是一層。被編碼過兩次的值以%25而單次編碼會留下%20,而一個被編碼過三次的值則以%2525.
URL 可以原樣承載的字元:RFC 3986 把 ASCII 範圍分成三組,而轉義規則只有對著這個劃分才說得通。
保留字元不是被禁止,而是被佔用了:它們對 URL 解析器有意義。從這份清單得出的規則很短 —— 如果一個字元是保留字元而你把它當資料用,就轉義它;如果它是非保留字元,你就永遠不需要轉義。
三種方法,以及各自原樣保留了什麼:
這些清單就是每種方法在 ASCII 範圍內原樣保留的確切集合;自檢會針對它們核對每一個可列印 ASCII 字元,所以這張表不可能與它所描述的編碼器脫節。
本頁能接受多大的輸入,以及為什麼:百分號編碼只是對文本的一次掃描,所以與本站點上那些進位制轉換器不同,開銷是直線增長而不是平方增長:不存在某個長度會讓算術本身變成問題。下面的上限之所以存在,是因為逐字元表是畫在文件裡的,而一張有一百萬行的表才是真正能拖死瀏覽器的東西。
什麼是百分號編碼?

百分號編碼 —— 也叫 URL 編碼,在較舊的文件裡叫轉義 —— 是讓 URL 能夠承載它本不允許包含的字元的那套規則。一個百分號轉義是三個 ASCII 字元:一個百分號加兩個十六進位制數字,代表一個位元組。%20 是位元組 0x20,也就是空格。%2F 是位元組 0x2F,也就是斜槓。不涉及別的任何東西:轉義命名的是位元組,不是字元。

因為它命名的是位元組,所以文本必須先被轉成位元組,而位元組編碼就是 UTF-8。長的轉義就是這麼來的:一個漢字是三個 UTF-8 位元組,因此是三個轉義;一個 emoji 是四個位元組,因此是十二個 ASCII 字元。

30 秒版本:空格作為 URL 值是 %20,在表單資料裡則是 +;# 是 %23;字面百分號是 %25。本頁上的三個編碼器對字母、數字以及 -_.~ 意見一致,卻對大約十來個標點字元意見不一致 —— 這正是 encodeURIComponent 與 encodeURI 對同一段文本產生不同字串的原因,也正是用錯其中一個會把一個值拆成兩個查詢引數的原因。
為什麼 URL 根本需要它

一個 URI 被定義為一串 ASCII 字元(RFC 3986 第 2.1 節),而在這個 ASCII 範圍內,有些字元是保留的:它們就是賦予 URL 結構的那些標點。冒號加兩條斜線分隔協議,@ 分隔使用者資訊與主機,問號開啟查詢,# 開啟片段,& 分隔查詢引數,而百分號則引入一個轉義。

這既造成了一個問題,也留下了一個後門。問題是:一個本身就含有這些字元的值 —— 含有 "a&b" 的搜尋框、含有斜槓的檔名、含有冒號的密碼 —— 本來會改變它所在 URL 的結構。後門則是百分號本身,於是編碼器可以直接擴充套件自己的字母表。% 寫成 %25,而它後面那兩個十六進位制數字是普通的非保留字元。

可用的示例

這些是本頁執行的三個編碼器的確切輸出,輸入之所以這樣選,是因為它們彼此表現不同。本頁會在你面前重新計算它們,而它的自檢會針對平臺自帶的實現重新核對每一行。

三種方法,以及各自該在什麼時候用

能省掉大多數 bug 的簡短版本:要放進單個引數值的任何東西,都經過 encodeURIComponent(或者經過表單序列化器,它用不同的安全集合做同一件事),而已經組裝好的完整 URL 如果需要轉義,才經過 encodeURI。抓錯這兩個中的一個,是 Web 程式碼裡最常見的百分號編碼錯誤。

URL 可以承載的字元,完整版
兩個加號,兩種含義

空格是唯一一個編碼方式取決於文本去向的字元,而加號就是原因。

陷阱就藏在那張表的中間。在表單資料裡,加號表示空格,所以一個真的含有加號的值 —— 寫成 +44 20 7946 0958 的電話號碼、一個算術表示式、一個帶加號標籤的郵箱地址 —— 必須以 %2B 的形式傳輸。如果以字面加號傳輸,接收方讀回來就是空格,+44 會變成“ 44”。

被編碼了兩次,以及怎麼認出來

二次編碼發生在一段字串已經被轉義、然後又被轉義一次的時候。第一次轉義裡的百分號也是一個普通字元,所以它會變成 %25,而下面那一層就以可見文本的形式存活下來。

認出它靠的是數:把字串解碼一次,看結果。如果裡面仍有看起來是刻意留下的轉義 —— 一個合法的 %XX,或者本該是空格的位置上有一個表單加號 —— 那就還有一層。如果裡面含有 %25 後跟兩個十六進位制數字,那麼下一層一定是轉義。

兩點實用提示。第一,解碼兩次並非無害:這是一個讓安全過濾器與它後面的程式碼看到不同字串的老辦法,所以在一個過濾器之後再解碼一次,那過濾器過濾的就是錯的文本。第二,這裡的層數是字串自身的屬性,不是對意圖的承諾:一個合法的值可以只有一層深,而一個壞了的值可以有兩層。

本頁要抓的那些錯誤
  • 用 encodeURI 編碼的值。那些分隔符會存活下來,所以一個含有 & 或 = 的值,在另一端會被拆成好幾個引數。把輸入 a&b=c 在本頁頂部的兩列上對比一下:component 列會轉義這兩個字元,encodeURI 列則保留它們。
  • 作為表單資料傳送的字面加號。它到達時是一個空格。請用 %2B。
  • 「為了保險」而被轉義兩次的值。它並不會更安全。接收方解碼一次,得到的字串裡仍然含有百分號轉義 —— 這與字元集錯誤是同一種失敗,只是深了一層。
  • 解碼從未被編碼過的東西。文本里的一個字面百分號 —— 100% sure —— 並不是轉義。嚴格讀取下它是一個損壞的轉義,位置會由本頁報告;寬鬆讀取下它則能存活,這也正是這裡預設是寬鬆而不是相反的原因。
  • 以為轉義必須大寫。解碼把 %2f 與 %2F 當成同一個位元組,兩者都合法。大寫只是 RFC 3986 推薦的規範寫法,也是其他工具會產生的那種。
  • 以為百分號編碼能讓文本變安全。它只是讓文本在 URL 裡不含歧義。它擋不住注入,不是加密,也不是淨化器:轉義必須是組裝之前的最後一步,而且必須針對文本將要進入的那個上下文。
快速問答

「URL 裡的加號是空格嗎?」

只在表單資料裡,而且只在表單提交的查詢部分。在路徑、片段,以及普通連結的查詢裡,加號就是字面加號。這正是本頁為它準備一個開關、而不是去猜的原因。

「為什麼我的連結裡顯示 %25?」

因為有東西把一個百分號編碼了。要麼是一個字面百分號經過了編碼器 —— 100% sure 變成 100%25%20sure —— 要麼是一段已經含有轉義的字串被第二次編碼。把字串解碼一次,看看回來的是什麼:如果結果裡仍有 %XX,就還有一層。

「查詢值裡的斜槓寫成 %2F 還是 / 有關係嗎?」

對 URL 解析器來說沒關係:兩者到達伺服器時都是斜槓,而 WHATWG 的查詢集合會放過斜槓。對互操作性來說有時有關係:不少服務端框架會對裸查詢串跑自己的解碼,其中少數對從未被轉義過的字元意見不一致。當一個值是手工組裝而不是由序列化器產生時,顯式轉義保留字元是更安全的習慣。

「為什麼表單列會轉義另外兩個都放過的波浪號?」

因為表單編碼器的安全集合更窄:只有字母、數字和 -_.*。WHATWG 表單編碼器會轉義 ~ ! ' ( ),儘管 RFC 3986 把 ~ 列為非保留字元。兩種寫法解碼後是同一個值;差別只在逐位元組比較字串時才重要。

「同樣的字元能以未轉義的形式出現在瀏覽器位址列裡嗎?」

是的,但那是位址列,不是 URL。瀏覽器會顯示 URL 的可讀形式,並在傳送時把它編碼回去,所以連結裡的 中文 線上路上會變成 %E4%B8%AD%E6%96%87。把 URL 從位址列裡拷出來,你會拿到哪一種,取決於瀏覽器。

「這裡的每個轉義看起來都合法,但出來的字元卻不是我預期的那個。為什麼?」

「解碼錯了」這句話背後藏著兩種不同的失敗,而本頁對它們的報告方式不同。第一種是損壞的轉義:一個後面沒有跟兩個十六進位制數字的百分號,比如結尾的 100% 或 %zz。那是語法問題,嚴格讀取會拒絕它並說明它在哪裡,寬鬆讀取則把它作為字面文本保留並列出。第二種是位元組本身完全合法、但那些位元組拼不出合法 UTF-8 字元的轉義,比如單獨的 %FF,或者被截斷成 %E4%B8 的三位元組字元。那不是語法問題,所以兩種讀取都會返回文本,把無法解碼的位元組替換成替換字元 U+FFFD("�"),並列出一條說明這一點的警告。警告才是有用的部分:以替換字元形式到達的文本已經不是原文了。

「本頁不做什麼?」

它不會把 URL 拆成協議、主機、路徑和查詢 —— 那是URL 解析器的活。它也不檢查一條連結是否安全可開啟,不聯絡任何東西,也不去猜轉義背後位元組的字元集:它一律按 UTF-8 讀取,這正是 URL 標準的要求。

「解碼需要哪一種兜底,我又怎麼知道它是對的?」

按自檢。它會把本頁自己的編碼器與平臺自帶的 encodeURIComponent、encodeURI 和 URLSearchParams 就每一個可列印 ASCII 字元做對比,重跑上面表格裡的每一個示例,並重新核對記錄下來的層鏈,然後把檢查項數報在這裡。

標準與適用範圍

百分號編碼規定於 RFC 3986 第 2.1 節,保留與非保留字元的定義在第 2.2 與 2.3 節,大小寫規則在第 6.2.2.1 節。瀏覽器構建 URL 時實際使用的集合在 WHATWG URL 標準裡,它按上下文分成幾組:片段一組、查詢一組、特殊查詢一組、路徑一組、使用者資訊一組,還有一個用於單個元件的更窄的集合。表單提交(包括加號)在 HTML 標準的 application/x-www-form-urlencoded 部分。而 encodeURI 與 encodeURIComponent 這兩個函式屬於 ECMAScript 本身。

本頁用自己逐上下文的集合填充 path、query、fragment 三列,並在結果旁邊列印每個上下文的確切清單。這些集合由 WHATWG 的集合推導而來,並在兩處刻意與之不同:查詢裡的 # 會被轉義,因為裸 # 會開啟片段;而 component 集合被釘死為 encodeURIComponent 會轉義的那些字元。凡是平臺函式本身就是答案定義的地方 —— encodeURIComponent、encodeURI 與表單序列化 —— 本頁就直接呼叫那個函式,所以那幾列是瀏覽器自己的行為,而不是對它的一次重新實現。

本頁正文由本站撰寫。表格裡的每個值都由本頁自己的代碼在加載時生成,並已在本站的測試工具中與平臺實現核對;自檢會在你的瀏覽器裡重複這一核對。