为什么用 File.ReadAllText
读 UTF-8 文件会多出“\ufeff”?
这是最常见的现象:你用
File.ReadAllText("test.txt") 读一个看似普通的 UTF-8 文本,开头却意外出现 \ufeff(即 Unicode 的 ZERO WIDTH NO-BREAK SPACE)。这不是乱码,而是 UTF-8 BOM(Byte Order Mark)被 .NET 当作有效字符读进来了。.NET 的
File.ReadAllText默认使用
Encoding.Default(通常是系统 ANSI 编码),**不是 UTF-8**;即使文件是 UTF-8 带 BOM,它也可能误判编码,或把 BOM 当普通字符保留。
用 Encoding.UTF8
显式指定编码仍读出 BOM?
这是因为
Encoding.UTF8实例默认 **不忽略 BOM**。虽然它能正确解码 UTF-8 字节流,但遇到开头的
0xEF, 0xBB, 0xBF时,仍会将其映射为
\ufeff字符。解决方法是使用带参数的
StreamReader构造函数: 用
new StreamReader(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))—— 这个
UTF8Encoding实例不会写入 BOM,但读取时仍会识别并跳过 BOM 更直接:用
new StreamReader(path, Encoding.UTF8, true)—— 第三个参数
detectEncodingFromByteOrderMarks = true是关键,它会让
StreamReader自动检测并跳过 BOM(包括 UTF-8、UTF-16、UTF-32 的 BOM) 然后调用
reader.ReadToEnd(),返回的字符串就不会含
\ufeff
如何安全地移除已有文件的 UTF-8 BOM 并保存?
不能简单地用
string.Replace("\ufeff", ""),因为 BOM 只应在文件开头存在,且必须从字节层面移除,否则可能误删正文里的合法 \ufeff(极少见但合法)。正确做法是:先读取原始字节,判断是否以 UTF-8 BOM 开头,再截取后保存:
byte[] bytes = File.ReadAllBytes(path);
if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
{
File.WriteAllBytes(path, bytes.Skip(3).ToArray());
}
注意:
File.WriteAllText默认不写 BOM;若你希望新文件**也不带 BOM**,务必用
new UTF8Encoding(false):
File.WriteAllText(path, content, new UTF8Encoding(false))不要用
Encoding.UTF8直接传入——它的
GetPreamble()返回 BOM 字节,
File.WriteAllText会自动写入
读取时跳过 BOM 和写入时不带 BOM 必须配对使用
否则容易陷入“读出来干净,一保存又带上了”的循环。尤其在配置文件、JSON、XML 等对开头空白敏感的场景,BOM 会导致解析失败(比如 JSON 解析器报 “Unexpected token \ufeff”)。最稳妥的组合是:
读:用StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true)写:用
File.WriteAllText(path, content, new UTF8Encoding(false))或
File.WriteAllLines(..., new UTF8Encoding(false))如果用
StreamWriter,也需传入
new UTF8Encoding(false),否则默认带 BOM
BOM 不是必需项,UTF-8 标准本身不要求它;多数现代工具(VS Code、git、.NET Core+)默认不生成,但某些编辑器(如老版 Notepad)或 Windows API 仍可能插入。处理时别假设“有没有都一样”,得明确控制字节层面行为。
