字节不是被「判错」,是被「第一个解通的」判走
老系统导出的日志一打开满是方块和问号,直觉是软件认错了编码。判定那几行其实很诚实:按固定顺序试几种解法,谁先解通就用谁,而且都是严格试——解码器开着 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、来源系统和已知字段。