UTF-16 与字节序问题
UTF-16 把基本平面字符编成 2 字节,增补平面字符编成代理对共 4 字节。多字节单位带来字节序问题:同样的码位在小端机器上写作低字节在前,在大端机器上高字节在前,两者字节序列完全相反。UTF-16 的解决办法是在流首写一个 U+FEFF 作为字节序标记(BOM),解码器读到 FF FE 判为小端、FE FF 判为大端;外部已有声明(如 HTTP 头、XML 声明)时,BOM 只是冗余确认。
基本信息
| 编码单位 | 2 字节基本单元,增补平面用 4 字节代理对 |
|---|---|
| 小端 BOM | FF FE(Windows 记事本「Unicode」默认格式) |
| 大端 BOM | FE FF(网络字节序习惯,部分协议使用) |
| 无 BOM 写法 | 显式指定 UTF-16LE 或 UTF-16BE 才能安全解码 |
| 内存表示 | Java 与 .NET 的字符串内部使用 UTF-16;JavaScript 字符串是 UTF-16 码元序列 |
| 典型场景 | Windows 宽字符 API、Java/.NET 字符串、部分旧协议报文 |
典型使用场景
- 读取 Windows 导出的文本:记事本另存为「Unicode」即 UTF-16LE 带 BOM
- Java/.NET 程序与文件交互:字符串内部是 UTF-16,落盘时需明确指定编码与字节序
- 对接旧协议或老硬件:报文按 UTF-16BE 定义并要求字节数对齐
- 排查乱码:看到「字符之间夹着不可见字符」或成对反向字节,先怀疑字节序弄反
- 跨语言数据交换:确认前后端与语言实现都是 UTF-8,避免 UTF-16 环节引入 BOM
识别与转换示例
识别与转换示例
xxd -l 16 sample.txt # 看开头是 ff fe(小端)还是 fe ff(大端)
file sample.txt # 输出 Little-endian UTF-16 with BOM 等提示
iconv -f UTF-16 -t UTF-8 sample.txt -o out.txt # 有 BOM 时由 BOM 决定字节序
iconv -f UTF-16LE -t UTF-8 sample.txt -o out.txt # 无 BOM 时必须显式指定序别
python -c "print(open('sample.txt', encoding='utf-16').read()[:20])" # utf-16 会按 BOM 自动判定
python -c "b=open('sample.txt','rb').read(8); print(b)" # 直接看原始字节,避免被自动判定掩盖问题字节序与标识对照
| 场景 | 常见字节序 | 首两字节 |
|---|---|---|
| Windows 文本文件(记事本 Unicode) | 小端 | FF FE |
| Windows 宽字符 API 内存布局 | 小端 | 不适用(内存中无标记) |
| Java / .NET 字符串内存 | UTF-16 码元序列 | 不适用 |
| XML/HTML 规范建议 | 由 BOM 或声明决定 | FE FF 或 FF FE |
| 网络协议习惯 | 大端 | FE FF |
同一份文件里 BOM 只能出现一次,且必须在最开头。文本合并时若把两个带 BOM 的文件拼起来,第二个 BOM 会变成正文里的零宽不换行空格(U+FEFF),表现为「看不见但比较失败」的诡异问题。
Web 与配置场景优先 UTF-8:JSON 规范要求 UTF-8,HTTP 与大多数服务端框架默认也是 UTF-8。在中间某环引入 UTF-16 与 BOM,会让下游解析器把 BOM 当内容读入(例如 JSON 首个字符不是花括号而报解析错误)。
编程语言里用 utf-16 与 utf-16le 语义不同:前者会读并消耗 BOM、写出时也补 BOM;后者把 BOM 当普通字符处理。省略 BOM 的文件用前者打开会得到一个不可见字符开头的字符串,这类问题在日志里最难定位。