解决CSV中文乱码需全流程统一UTF-8 BOM与charset声明:文件开头加BOM(如Python用"utf-8-sig")、HTTP响应头设Content-Type为text/csv; charset=utf-8、前端Blob构造时手动编码并添加BOM字节。
导出 CSV 文件中文乱码:Excel 默认不认 UTF-8 BOM
windows 上的 excel 打开纯 utf-8 编码的 csv 文件,大概率显示为乱码——这不是你编码错了,是 excel 自己没按 utf-8 解码。它默认用系统 ansi(比如 gbk)去读,而你的文件没带 bom,它根本不知道该用啥解码。
实操上最稳的解法,是给 CSV 文件开头强制加 UTF-8 BOM(\uFEFF),让 Excel 主动识别为 UTF-8:
- Python 写文件时用
open(..., encoding="utf-8-sig"),别用"utf-8" - Node.js 用
fs.writeFileSync(path, "\uFEFF" + csvContent, "utf8") - PHP 输出前 echo
"\xEF\xBB\xBF"(即 BOM 的字节序列) - 注意:BOM 只能加在文件最开头,多一个空格、换行或注释都会失效
HTTP 响应头没设对:Content-Type 缺少 charset
通过浏览器下载 CSV 时乱码,常是因为后端返回的 Content-Type 是 text/csv 或 application/csv,但没声明 charset=utf-8。浏览器可能按页面编码或默认编码解析,结果不可控。
必须显式指定字符集:
- Express 中用
res.set("Content-Type", "text/csv; charset=utf-8") - Django 中用
HttpResponse(content, content_type="text/csv; charset=utf-8") - Nginx 静态 CSV 文件可通过
add_header Content-Type "text/csv; charset=utf-8";补充(但注意不能覆盖已有值) - 如果用了
application/octet-stream,浏览器会绕过 charset 判断,直接下载——这时 BOM 就更关键了
pandas.to_csv() 导出中文乱码:encoding 参数不是万能的
很多人以为 df.to_csv(..., encoding="utf-8") 就够了,但 Excel 依然乱码——因为 pandas 默认不写 BOM,且 encoding="utf-8" 和 "utf-8-sig" 行为完全不同。
正确姿势只有这一种:
- 必须用
encoding="utf-8-sig",不是"utf-8" - 不要加
line_terminator="\n"之类干扰默认行为的参数,除非你清楚影响 - 如果用
to_excel(),就完全不用操心编码,但文件体积大、兼容性略差 - 导出到内存(如
StringIO)再写入响应体时,同样要确保最终写入流时用了utf-8-sig
前端 JS 生成 CSV 下载:Blob 构造时 charset 容易被忽略
用 new Blob([csvStr], {type: "text/csv"}) 下载,中文照样乱码。因为 Blob 不解析字符串编码,它只按传入的原始字节存——而 JS 字符串是 UTF-16,直接塞进去,浏览器可能按 Latin-1 解析。
可靠做法是手动转成 UTF-8 字节再构造 Blob:
- 用
new TextEncoder().encode(csvStr)得到 Uint8Array(UTF-8 字节) - 再
new Blob([bomBytes, utf8Bytes], {type: "text/csv;charset=utf-8"}),其中bomBytes = new Uint8Array([0xEF, 0xBB, 0xBF]) - 千万别用
unescape(encodeURIComponent(csvStr))这类老方案,有长度限制且不保真 - 如果用
URL.createObjectURL(new Blob(...)),记得后续调用URL.revokeObjectURL()防内存泄漏
真正麻烦的不是加 BOM 或设 header,而是不同环节(生成、传输、打开)各自认一套规则。只要漏掉一环——比如后端写了 BOM,但反向代理清掉了二进制头;或者前端 Blob 类型写了 charset,但下载链接被 a 标签 href 直接触发——乱码就又回来了。











