一段中文貼進三檔,吐出三條串
這頁六個函數體我逐字摘出來在純 Node 裡原樣跑(NODE=v24.19.0,非瀏覽器實測)。「你好 world」原始 10 碼元,component 與 uri 檔編成同一條 26 碼元的 %E4%BD%A0%E5%A5%BD%20world,php 檔 25 碼元——差別只在空格:1 碼元的 + 替掉 3 碼元的 %20。「PHP 模式為什麼短幾個字元」得把 128 個碼位逐個餵一遍才說得清。
128 個碼位上,三檔各自「不編碼」哪些字元
對 ASCII 0–127 全部 128 個碼位逐字元餵進三檔(探針 B 組),輸出與輸入相同就算該檔保留。component 檔保留 71 個、轉義 57 個;uri 檔保留 82 個、轉義 46 個;php 檔保留 66 個、轉義 62 個。三檔都轉義 46 枚;三檔都保留 66 枚,就是標題那個數——字母數字外只剩 - _ . ~。
- uri 比 component 多保留 11 枚:# $ & + , / : ; = ? @。
- component 比 php 多保留 5 枚:! ' ( ) *(碼位 33、39、40、41、42),php 檔收成
%21 %27 %28 %29 %2A。 - 十二字元 ;/?:@&=+$,#[] 裡十枚只在 uri 檔保留,[ ] 兩枚三檔全轉義成
%5B %5D。
差集對得上帳:66 = 71 − 5、82 = 71 + 11,uri 比 php 多留的 16 枚是上面兩組相加。
圖 1:三檔保留集在 128 枚碼位上的寬度對照
phpUrlencode 六次替換收走了什麼
函數體是 encodeURIComponent 之後串六次 replace。探針 C 組輸入 !']()()%20a~_-., +(16 碼元)先經 encodeURIComponent 吐成 24 碼元,之後五步各命中 1 次、每步 +2,串長走 24→26→28→30→32→34;末步 %20→+ 命中 1 次把 3 碼元換回 1 碼元、34→32。兩條鏈上 ~ 與 - _ . 全程未動,五枚符號的五步恰好把保留集從 71 收成 66。
把 %20→+ 挪到最前作對照(H 組):128 枚單字元逐一探測,兩序分歧 0/128,整串也逐字相等——本頁可測樣本上觀察不到換序差異,只按實測口徑寫。
輸入裡本來就寫著 %21 時會被自己改寫,這是本頁唯一真實的二次編碼路徑:phpUrlencode("%21") 得 %2521、phpUrlencode("%20") 得 %2520(手寫 %20 不會被第 6 步吃成 +,只有本次編出的真空格才變 +)、phpUrlencode("+") 得 %2B。"a b!" 回餵三連得 pass1 = a+b%21、pass2 = a%2Bb%2521、pass3 = a%252Bb%252521,phpUrldecode 每跑一次剝一層。URL 編解碼 這六個函數是無狀態純函數,同一輸入連調 1000 次輸出一致。
三檔編碼 × 三檔解碼:九格矩陣
探針 D 組三種編碼檔各配三種解碼檔,五樣本「你好 world」「a b」「a+b」「x=1&y=2」「50%」每格 5 條,9 格共 45 條實測:自還原 40、不自還原 5、異常 0。
| 編碼檔 | component 解 | uri 解 | php 解 |
|---|---|---|---|
| component | 5/5 | 5/5 | 5/5 |
| uri | 5/5 | 5/5 | 4/5 |
| php | 3/5 | 3/5 | 5/5 |
翻車全在 + 上。「你好 world」php 檔編成 %E4%BD%A0%E5%A5%BD+world,component 或 uri 檔解出「你好+world」——decodeURIComponent 對裸 + 原樣返回,這是矩陣裡 4 條不還原的成因;uri|php 格唯一翻車是 a+b 被 php 解碼的前置換成空格變「a b」。「50%」九格全還原。
編時不 trim、解時先 trim
urlEncode 讀原值不剪、urlDecode 先 trim,這處不對稱按「首尾有無空白」兩類給數(E 組 8 輸入 × 3 檔共 24 行)。「編碼再解碼」方向 24/24 自還原;「直接解碼再回編」方向首尾帶空白類 0/15 還原、無空白類 9/9 還原;24 行裡 trim 改變解碼結果的 15/24。JS 的 trim 連 Tab 與全形空格 U+3000 一起剪,「 a b 」php 檔編成 ++a+b++,Tab 則三檔都是 %09。
失敗時這一頁只發一句固定文案
解碼側 5 串非法 % 序列(%E4%B8、%ZZ、%、50%、%2)× 3 檔共 15 行全拋 URIError、e.message 都是 URI malformed,但 catch 發的是固定整句「解碼失敗,內容不是有效的URL編碼」、不含 e.message。編碼側 URIError 只能由孤立代理項觸發:\uD800、x\uD800y、\uDF00 × 3 檔共 9 行全拋,toast 拼成「編碼失敗:URI malformed」;正規代理對(\uD83D 起頭那對)三檔都編成 %F0%9F%98%80,攔下的是半截代理。兩處 catch 都不寫輸出框,瀏覽器呈現按未實測記。
對著 intro 逐條判
config/tools.php 第 61 行起是 url-encode 條目、intro 在第 70 行(1639 碼元 / 2129 位元組),逐條判定 13 條:[一致] 8、[衝突] 2、[部分一致] 3。兩條衝突一是「encodeURI 不編碼空格」——實測 encodeURI(' ') = %20;二是「即時雙向轉換」——模板沒有 oninput,轉換只由三枚 onclick 觸發。「相容 PHP urlencode 格式」按紅線只判到本頁六次 replace 的格式特徵吻合為止。參數拆解歸 URL 解析 頁,本篇不外推。
FAQ
Q:同一句中文,PHP 模式為什麼比別人少幾個字元?
少的是空格:php 檔空格編成 1 碼元的 +,另兩檔編成 3 碼元的 %20,「你好 world」26 對 25 就是這個差;其餘差在 ! ' ( ) * 那 5 枚,component 留著、php 收走。
Q:php 編完用 encodeURIComponent 解,+ 怎麼變回空格?
變不回來。decodeURIComponent 對裸 + 原樣返回,實測 php|component、php|uri 兩格各 3/5,「你好+world」解出來還帶 +。要回原值得配 php 檔解碼(先 +→%20 再解),矩陣對角三格都是 5/5。
Q:輸入裡已經寫著 %21,會被這一頁再編一次嗎?
會——phpUrlencode("%21") 得 %2521、% 被頂成 %25,這是本頁唯一真實的二次編碼路徑;手寫 %20 反而停在 %2520 不變 +。這組字元級實測舊文 URL 編碼是怎麼回事 沒做過。