雜記 › 技術筆記 › 同一句中文,UTF-8 存 12 位元組、GBK 只存 8 位元組:編碼是怎麼被判錯的

同一句中文,UTF-8 存 12 位元組、GBK 只存 8 位元組:編碼是怎麼被判錯的

位元組不是被「判錯」,是被「第一個解通的」判走

老系統匯出的日誌一開啟滿是方塊和問號,直覺是軟體認錯了編碼。判定那幾行其實很誠實:按固定順序試幾種解法,誰先解通就用誰,而且都是嚴格試——解碼器開著 fatal,位元組在該編碼下不合法就當場拋錯。它只保證「這串位元組在這個編碼下合法」,保證不了「這就是當初存它的編碼」。

四步是:① 開頭命中 BOM(EF BB BF)就判 utf-8 並把這三位元組削掉再解,字元數不會因 BOM 多 1;② 按 utf-8 嚴格解一次;③ 失敗再按 gbk 嚴格解;④ 兩次都失敗回落 utf-8,改用寬鬆模式,壞位元組解成替換字元。每份位元組各停一步:

樣本位元組停在哪步判定解出來
E4 B8 AD E6 96 87 E6 B5 8B E8 AF 95②解通utf-8中文測試,4 字元
上串加 EF BB BF①命中utf-8中文測試,15 位元組
D6 D0 CE C4 B2 E2 CA D4②拋錯③解通gbk中文測試,8 位元組
82 B1 82 F1 82 C9 82 BF 82 CD②拋錯③解通gbk偙傫偵偪偼,5 字元
E4 B8 AD FF②③都拋錯落④utf-8中加替換符,2 字元

第四行最能說明問題:那 10 個位元組是日文「こんにちは」的 Shift-JIS 碼,沒有 BOM、utf-8 嚴格解拋錯,輪到 gbk——它同時也是合法的 GBK 雙位元組序列,於是 auto 判成 gbk,螢幕上出現「偙傫偵偪偼」五個像模像樣的中文,字形無害語義全錯。手動切 shift-jis 才拿到「こんにちは」,同樣 5 個字元。判定只有 utf-8 與 gbk 兩個出口,auto 永遠不回傳後兩者,那句「支援 Shift-JIS」只在手動時成立。

同一句中文成本差一半:UTF-8 每字 3 位元組存成 12 位元組,GBK 每字 2 位元組存成 8 位元組,字元數都是 4、行數都是 1。換宣告就換字——那 8 位元組按 big5 解是「笢恅聆彸」,那 12 位元組按 gbk 解會得到 6 個字,其中一個落在私有區碼位上、螢幕裡就是個方塊,沒一個是你想讀的。

UTF-8 無 BOM 12 位元組 GBK 8 位元組 UTF-8 加 BOM 15 位元組

圖 1|同一句「中文測試」4 字元 1 行的三種存法:12 位元組、8 位元組、加 BOM 後 15 位元組而字元數仍是 4。

位元組數、字元數、行數:三把尺量不出同一個數

三個數各拿一把尺:大小取解碼前的 file.size,字元數取字串 length(UTF-16 碼元),行數取按 \n 切的段數。含一個 U+1F600 的那行最能把它們拆開:碼元 6、碼點 5、UTF-8 位元組 16,兩兩不等,介面「字元數」比人眼數出來的多 1——表情占 2 個碼元和 4 個位元組,漢字只占 1 個碼元和 3 個位元組。

切分只認 \n:a\r\nb\r\nc 得 3 段,前兩段各留一個看不見的 \r,它照樣被算進字元;末尾多一個換行就切出 4 段,文字還是 3 行而介面顯示 4 行;純 CR 的老檔案一段都切不開,整篇擠進第 1 行。行號是 i 加 1,從 1 起,第 N 行的行號就是 N。

寫法位元組數段數=行數留 \r 的段數
a\r\nb\r\nc732
a\r\nb\r\nc\r\n943
a\rb\rc510

兩個上限,先撞到的往往不是那個 10MB

大小門檻寫成 10 * 1024 * 1024 即 10485760 位元組,文案叫「最大 10MB」:這是 1024 口徑,精確等於 10 MiB,按 1000 口徑是 10.48576 MB。判據是嚴格大於,10485760 放行、10485761 被拒並彈「檔案超過 10MB 限制」,兩者在資訊欄裡都顯示 10.00 MB。

渲染另有門檻,只畫前 50000 段。50000 行、每行 60 位元組(59 個字元加 1 個 \n)的檔案共 3000000 位元組 = 2.8610 MiB,只佔上限的 28.61%,大小那關照樣過,但按 \n 切出 50001 段——多的正是末尾那個空段——仍然超限;同樣內容不帶末尾換行是 2999999 位元組、50000 段,正好不觸發。

大小上限 10485760 位元組 50000 行樣本 3000000 位元組 · 佔 28.61%

圖 2|上限 10485760 位元組即 10 MiB;那 3000000 位元組的樣本只佔 28.61%,先撞到的卻是 50000 段這道渲染上限。

截斷時頁尾逐字寫著「... 檔案過大,僅顯示前 50000 行(共 50002 行)」,資訊欄「行數」卻寫全量 50002——兩處數字打架,讀日誌得先確認在看哪一個。

體積顯示的進位邊界

  • 1023 是「1023 B」,1024 是「1.0 KB」:B 檔不帶小數,KB 檔留 1 位。
  • 1048575 是「1024.0 KB」而不是「1.00 MB」,1048576 才跳;1073741823 是「1024.00 MB」,1073741824 是「1.00 GB」。

搜尋高亮數的是轉義之後的那串

行內容進 DOM 前先過一遍轉義,它有 4 個替換(比 json-to-csv 多一個雙引號),而比對就在這個轉義後的串上做。拿「line <b>bold</b> & more」實測:搜 <b> 命中 0 處,原文卻有 1 處;搜 & 命中 5 處,原文只有 1 個 &,多出的 4 個是轉義新寫的 < 與 >;搜 LINE 命中 1 處,靠的是帶 gi;2 個雙引號變成實體後,搜雙引號命中 0 處。高亮標籤還會把實體攔腰切開,位置整體錯亂。

這些結論能在同一處反覆核對:文字檔讀取的下拉選單有 6 項(auto、utf-8、gbk、gb2312、big5、shift-jis),auto 只給前兩種,另外三種只有手動選才生效,gb2312 不參與判定;同一份檔案只換宣告,看同樣的位元組落到不同的字上,是理解這條鏈最快的一步。那句「編碼不支援,已回退 UTF-8」也取不到:Chrome 154 實測 5 個真實編碼值全能建構出解碼器,是死路徑。按碼點核對某段位元組對應哪個字,可配著中文與 Unicode 對照;把超限日誌切開逐段讀,大文字拆分比跟那 50000 段較勁省事。

Q:為什麼切到 GBK 就正常,auto 卻不行?

分兩種。真 GBK 的中文(D6 D0 CE C4 B2 E2 CA D4)在第②步必然拋錯、第③步解通,auto 本來就沒判錯;救不回來的是日文那份:它在 GBK 這關已經解通,鏈子走不到 shift-jis 或 big5,那兩個出口在程式碼裡不存在。

Q:為什麼只顯示前 50000 行,資訊欄卻說 50002 行?

兩套口徑:渲染只取前 50000 段,而資訊欄的行數來自另一次不受 50000 約束的按 \n 切分。數總行數看資訊欄,判斷少了哪一截看這兩個數之差。

Q:為什麼搜尖括號標籤搜不到,頁面上明明有?

頁面上那個尖括號是轉義後的顯示結果,比對用的串裡它已是以 &lt; 開頭的實體;反過來 & 命中 5 處而原文只有 1 個。要定位它,只能搜旁邊沒被轉義的那段普通文字。

Q:能不能靠位元組本身把編碼認出來?

不能唯一認出。同一段位元組能在幾種編碼規則下都合法,判定鏈只是把多解壓成一解;外證只剩 BOM、來源系統和已知欄位。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22