位元組不是被「判錯」,是被「第一個解通的」判走
老系統匯出的日誌一開啟滿是方塊和問號,直覺是軟體認錯了編碼。判定那幾行其實很誠實:按固定順序試幾種解法,誰先解通就用誰,而且都是嚴格試——解碼器開著 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 個字,其中一個落在私有區碼位上、螢幕裡就是個方塊,沒一個是你想讀的。
圖 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\nc | 7 | 3 | 2 |
a\r\nb\r\nc\r\n | 9 | 4 | 3 |
a\rb\rc | 5 | 1 | 0 |
兩個上限,先撞到的往往不是那個 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 段,正好不觸發。
圖 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:為什麼搜尖括號標籤搜不到,頁面上明明有?
頁面上那個尖括號是轉義後的顯示結果,比對用的串裡它已是以 < 開頭的實體;反過來 & 命中 5 處而原文只有 1 個。要定位它,只能搜旁邊沒被轉義的那段普通文字。
Q:能不能靠位元組本身把編碼認出來?
不能唯一認出。同一段位元組能在幾種編碼規則下都合法,判定鏈只是把多解壓成一解;外證只剩 BOM、來源系統和已知欄位。