从 ByteOrderMarks (BOM) 检测编码






4.80/5 (13投票s)
在使用 StreamReader / StreamWriter 时避免陷阱
引言
你们都知道,要将原始字节转换为可读字符串,反之亦然,你需要一个转换算法,即“编码”。有几种编码可用,当你用不同于写入文件的编码来读取文件时,问题就会出现。
![]() 非常常见的编码不匹配:用 Utf8 写入,然后用 Ansi 读取 - 你们中的大多数人会认出前面三个字符——这就是 Ansi 尝试显示 Utf8 的字节顺序标记的方式。 |
![]() 我的最爱 |
在“Windows 世界”中,有一种非常普遍的技术,将关于应用编码的信息与文件本身一起发送,称为“ByteOrderMarking
”(BOM)。
文件的前 2、3 或 4 个字节并非 meant to be readable `char`s,而是用来指示使用的编码。
不幸的是,并非所有编码都有定义的 BOM。但最重要的编码都有,即 Utf8、Utf16、Utf32。在“Windows 世界”中,还有一个“最重要的”编码:Ansi - 它的 Web 名称是“Windows-1252”。例如,Visual Studio 用 Ansi 保存其文件。而 Ansi 没有定义 BOM。 :(
因此,一种不安全、启发式地猜测编码的方法是检查 BOM,如果没有 BOM,则假设 Encoding.Ansi。我承认:这简直是“粗制滥造”——我只在可以强烈预期文件是用常见编码之一写入的情况下推荐这种方法。
(要了解一种更高级的直接从原始字节猜测编码的方法,请参阅 [^] Carsten Zeumer 的文章)。
如何不做
using(var r = new StreamReader(filename)) {
richtextBox1.Text = r.ReadToEnd();
}
尽管 `StreamReader` 默认情况下足够智能,可以检测 BOM,但这会导致两个缺点:
- 如果没有检测到 BOM,`StreamReader` 默认猜测 Utf8。IMO 这是一个次优的选择,因为 Utf8 是可以被检测到的。如我所说,在没有 BOM 的情况下,最好假设 `Encoding.Ansi`。
- 检测到/猜测到的编码会丢失。现在当你保存文件时,`StreamWriter` 默认会选择 Utf8,所以你的文件编码可能会被更改,而你却没注意到。
如何做
告诉 `StreamReader` 默认猜测 `Encoding.Ansi`,并存储检测到/猜测到的编码,以便在写入文件时重新使用它。这样文件编码就会保持不变,和之前一样。
private Encoding _Detected;
//...
using(var r = new StreamReader(filename, Encoding.Default)) {
richtextBox1.Text = r.ReadToEnd();
_Detected = r.CurrentEncoding;
}
//...
File.WriteAllText(filename, richtextBox1.Text, _Detected);
//...
注意! 因为 BOM 在读取时会被检查,所以 `StreamReader.CurrentEncoding` 仅在尝试读取至少一个 `Char` 之后才有意义。
关于陷阱
也许主要的陷阱是 `StreamReader` 智能性的文档记录不佳。在 `Objectbrowser` 中检查 `StreamReader` 的构造函数:
public StreamReader(string path, System.Text.Encoding encoding)
System.IO.StreamReader 的成员
摘要:为指定的文件名初始化 `StreamReader` 类的新实例,并指定字符编码。
你现在会期望字节顺序标记检测已启用,并且指定的编码可能会被忽略吗?
在某种程度上,摘要欺骗了我们:是的,它用给定的编码实例化一个 StreamReader,但如果检测到 BOM,它将不使用它。
要获得更正确的描述,你必须参考 [^] 在线 MSDN,因为你的离线 MSDN 可能不是最新的。(例如:在我的系统上,缺少以下提示)
备注
此构造函数使用指定的编码参数初始化编码,并将内部缓冲区设置为默认大小。StreamReader 对象通过检查流的前三个字节来尝试检测编码。如果文件以适当的字节顺序标记开头,它会自动识别 UTF-8、小端 Unicode 和大端 Unicode 文本。否则,将使用用户提供的编码。有关更多信息,请参阅 Encoding.GetPreamble 方法。
XML/HTML 中的编码
对于 XML,关于使用的编码有一个简单且通用的标准:它应该在文档定义标签中指定,例如:
<?xml version="1.0" encoding="utf-8" ?>
<root>
<value1>hällö</value1>
</root>
.NET 的 `XmlReader`、`XmlDocument` 和 `XDocument` 可以妥善处理此类数据,因此如果指定了编码,您可以相信数据已被正确读取。
这很困难,但仍然有可能用一种编码保存 XML,并在定义标签中指定错误的编码,但这不是你的责任,而是 XML 作者的责任。
示例应用程序
我刚构建了一个小工具,您可以使用它来以不同的编码读取和写入文件。您可以检测编码、更改它们并重现不匹配。
历史
- 2010/7/15:首次发布
- 2010/8/15:添加了“XML/HTML 中的编码”部分,并为示例应用程序添加了相应代码