上周给 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 也要带上。