上周给 DXTool 凭据保险库做 CSV 导出功能时遇到一个经典问题:导出的文件用记事本打开中文完全正常,用 Excel/WPS 双击打开却全是乱码。记录一下排查过程和根因。
现象
- 记事本打开:
姓名,账号显示正常 - Excel 双击打开:
濮撳悕,璐﹀彿之类的乱码 - 用 Excel「数据 → 从文本/CSV 导入」手动选 UTF-8 又正常
根因:UTF-8 没有 BOM
这个文件是纯 UTF-8 无 BOM 编码。Windows 的中文版 Excel 双击打开 CSV 时,默认按系统区域设置的代码页(GBK/CP936)解码文件头没有 BOM 的内容。UTF-8 编码的中文按 GBK 解读,就成了乱码。
记事本从 Windows 10 1903 起默认按 UTF-8 无 BOM 处理,所以"看起来正常"——这也是为什么用记事本验证编码具有误导性。
什么是 BOM
BOM(Byte Order Mark)是文件开头的几个特殊字节,用来声明编码格式:
| 编码 | BOM 字节 |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16 LE | FF FE |
| UTF-16 BE | FE FF |
UTF-8 的 BOM 是可选的,Linux/macOS 生态普遍不加,Windows 的 Excel/记事本体系则依赖它识别编码。
修复方案
在 CSV 内容最前面写入 UTF-8 BOM 三个字节即可,Excel 和 WPS 见到 BOM 就会按 UTF-8 解码:
// Go 示例:写文件前先写 BOM
bom := []byte{0xEF, 0xBB, 0xBF}
content := append(bom, []byte(csvText)...)
os.WriteFile(path, content, 0644)
// Java 示例
byte[] bom = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
Files.write(path, bom, StandardOpenOption.CREATE);
Files.write(path, csv.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
防回归
这类"编辑器差异"问题最容易在后续重构中被改回去,建议直接加一条单元测试,断言导出内容以 BOM 开头:
if !bytes.HasPrefix(data, []byte{0xEF, 0xBB, 0xBF}) {
t.Fatal("CSV 缺少 UTF-8 BOM,Excel 打开会乱码")
}
举一反三
- Java 读带 BOM 的文件:JDK 的
InputStreamReader不会自动剥离 UTF-8 BOM,首字段会带上不可见的(U+FEFF),导致equals比较失败,需要手动跳过或用BOMInputStream(commons-io)。 - Excel 导出选 UTF-16:如果是给纯 Windows 用户的报表,UTF-16 LE + BOM + Tab 分隔是最稳的格式,双击零配置打开。
- HTTP 下载场景:Web 导出除了内容加 BOM,记得响应头
Content-Type: text/csv; charset=UTF-8也要带上。