导入一份从 Windows 记事本导出的单词表,界面上出现了一堆 锟斤拷。排查发现,文件是 GBK 编码,而代码直接用 TextDecoder('utf-8') 解码。

这个问题在教育类应用里特别常见。老师用的电脑五花八门,导出的文本文件编码也不统一。easy-word 虽然内置词库是 JSON 格式(统一 UTF-8),但用户手动导入单词表的场景迟早要做。与其到时候一个个修 bug,不如先把编码检测的逻辑理清楚。

编码检测的优先级

拿到一个文件,先读前几个字节判断编码:

1
2
3
4
5
字节序列                    判断结果
EF BB BF UTF-8 with BOM
FF FE UTF-16 LE
FE FF UTF-16 BE
前 3 字节无 BOM → 尝试 UTF-8

BOM(Byte Order Mark)是编码格式的”身份证”。有 BOM 的文件,编码是确定的。问题出在没有 BOM 的文件上——这时候只能靠试探。

没有 BOM 时的判断逻辑

鸿蒙的 util.TextDecoder 只做解码,不帮你判断编码。判断需要自己写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { util } from '@kit.ArkTS';

function detectEncoding(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);

// BOM 检测
if (bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) {
return 'utf-8'; // 跳过 3 字节 BOM
}
if (bytes[0] === 0xFF && bytes[1] === 0xFE) {
return 'utf-16le';
}
if (bytes[0] === 0xFE && bytes[1] === 0xFF) {
return 'utf-16be';
}

// 无 BOM:尝试 UTF-8
const decoder = new util.TextDecoder('utf-8');
const str = decoder.decodeToString(buffer);
// U+FFFD 是 UTF-8 解码失败时的替换字符
if (!str.includes('\uFFFD')) {
return 'utf-8';
}

// UTF-8 失败,回退到 GBK
return 'gbk';
}

核心思路:先用 UTF-8 解码,检查是否出现替换字符 \uFFFD。没出现说明 UTF-8 解码正常;出现了说明字节序列不合法,大概率是 GBK。

为什么 UTF-8 优先

GBK 也可以解码 UTF-8 的中文,但反过来不行。UTF-8 的中文是 3 字节,GBK 的中文是 2 字节,UTF-8 解码器遇到 GBK 的 2 字节序列会产生无效序列。反过来,GBK 解码器遇到 UTF-8 的 3 字节序列会把它拆成”2字节 + 1字节”,结果是乱码但不报错。

所以判断顺序很重要:先试 UTF-8,失败了再试 GBK。不能反过来。

HarmonyOS 的 TextDecoder 限制

鸿蒙的 util.TextDecoder 支持的编码比 Node.js 少。实际可用的:

编码 是否支持
utf-8 ✅
utf-16le ✅
utf-16be ✅
gbk ⚠️ 需要系统级支持

GBK 的支持取决于设备系统。华为自有设备基本都支持,但模拟器和部分第三方设备可能不行。代码里需要兜底:

1
2
3
4
5
6
7
8
let decoder: util.TextDecoder;
try {
decoder = new util.TextDecoder(encoding);
} catch {
// 设备不支持该编码,回退到 utf-8
decoder = new util.TextDecoder('utf-8');
}
const text = decoder.decodeToString(buffer);

WordLibraryModel 的编码策略

当前 easy-word 的词库加载比较严格——所有 JSON 文件都用 UTF-8 编码打包进应用。这是正确的做法:

1
2
3
4
5
6
7
8
9
// entry/src/main/ets/service/WordLibraryModel.ets
private decoder: util.TextDecoder = new util.TextDecoder('utf-8');

private async readGradeWords(context: common.UIAbilityContext, grade: number): Promise<Word[]> {
const raw = await context.resourceManager.getRawFileContent(`words/grade${grade}.json`);
const content = this.decoder.decodeToString(raw);
const words = JSON.parse(content) as Word[];
return words;
}

内置资源不需要编码检测,因为开发者完全控制编码。多编码处理留给用户导入场景。

实际应用中的取舍

编码检测不是万能的。有些情况会误判:

  • 纯 ASCII 文件:UTF-8 和 GBK 解码结果完全一样,无法区分。实际上也不需要区分。
  • 混合编码文件:同一个文件里既有 UTF-8 又有 GBK。这种情况只能手动处理,没有通用解法。
  • 短文本:文件太小(比如只有几个英文单词),UTF-8 验证不够可靠。

对于 easy-word 来说,最实用的策略是:内置词库统一 UTF-8,用户导入时做编码检测,检测不出来就默认 UTF-8 让用户手动选择。

编码问题的本质是:字节本身没有意义,解释方式才有。同一个字节序列,UTF-8 和 GBK 会给出完全不同的解读。

下一篇会讲 tts-tool 如何处理 WAV 文件的 RIFF header 手写编码,以及 Base64 和二进制双响应的设计。