目录
什么是字符顺序标记(BOM)
计算机内部数据存储都是二进制的,只有知道一段数据的二进制存储格式,这段数据才有意义。所谓的文本文件其实就是用一种特定的字符编码来将二进制源数据转换成文字。多数文本编辑器都可以编辑不同编码的文本文件,那么文本编辑器是怎样通过源二进制数据来得知这段数据的文本编码呢?答案就是靠字符顺序标记(Byte Order Mark),在文章里面我们就统一用英文简写BOM指这一名词。
下面是常用Unicode编码的BOM
UTF-8: EF BB BF
UTF-16 big endian: FE FF
UTF-16 little endian: FF FE
UTF-32 big endian: 00 00 FE FF
UTF-32 little endian: FF FE 00 00
.NET中的Encoding类和BOM
在.NET的世界里,我们经常用Encoding的静态属性来得到一个Encoding类,从这里得到的编码默认都是提供BOM的(如果支持BOM的话)。
如果你想让指定编码不提供BOM,那么需要手动构造这个编码类。
//不提供BOM的Encoding编码
Encoding utf8NoBom = new UTF8Encoding(false);
Encoding utf16NoBom = new UnicodeEncoding(false, false);
Encoding utf32NoBom = new UTF32Encoding(false, false);
另外这里都是构造little endian的,big endian构造函数里也有参数。
其次UnicodeEncoding类代表UTF16编码。
Encoding类中的GetPreamble方法可以返回当前编码提供的BOM
public static void Main()
{
Encoding utf32WithBom = Encoding.UTF32;
Encoding utf32NoBom = new UTF32Encoding(false, false);
Console.WriteLine("UTF32 With BOM");
PrintBytes(utf32WithBom.GetPreamble());
Console.WriteLine("UTF32 No Bom");
PrintBytes(utf32NoBom.GetPreamble());
}
static void PrintBytes(byte[] bytes)
{
if (bytes == null || bytes.Length == 0)
Console.WriteLine("<无值>");
foreach (var b in bytes)
Console.Write("{0:X2} ", b);
Console.WriteLine();
}
输出:
UTF32 With BOM
FF FE 00 00
UTF32 No Bom
<无值>
文件读写和BOM
文本写入时,StreamWriter类和File.WriteAllText方法的默认编码都是不带BOM的UTF8
当然我们可以通过构造函数来指定一个其他编码,构造方法就像上面讲的一样。比如:
public static void Main()
{
Encoding utf32bigbom = new UTF32Encoding(true, true);
Encoding utf32litbom = new UTF32Encoding(false, true);
Encoding utf32litnobom = new UTF32Encoding(false, false);
var content = "abcde";
WriteAndPrint(content, utf32bigbom);
WriteAndPrint(content, utf32litbom);
WriteAndPrint(content, utf32litnobom);
}
static void WriteAndPrint(string content, Encoding enc)
{
var path = Path.GetTempFileName();
File.WriteAllText(path, content, enc);
PrintBytes(File.ReadAllBytes(path));
}
static void PrintBytes(byte[] bytes)
{
if (bytes == null || bytes.Length == 0)
Console.WriteLine("<无值>");
foreach (var b in bytes)
Console.Write("{0:X2} ", b);
Console.WriteLine();
}
输出:
00 00 FE FF 00 00 00 61 00 00 00 62 00 00 00 63 00 00 00 64 00 00 00 65
FF FE 00 00 61 00 00 00 62 00 00 00 63 00 00 00 64 00 00 00 65 00 00 00
61 00 00 00 62 00 00 00 63 00 00 00 64 00 00 00 65 00 00 00
可以看出来:00 00 FE FF是UTF32 big endian的BOM,而FF FE 00 00是UTF32 little endian的BOM,第三行是没有加BOM的UTF32的源二进制数据。
读文本的时候,当构造StringReader类进指定字符串路径或者Stream对象的话,StringReader的表现是自动通过BOM来判定字符编码,当然我们也可以手动指定一个编码(尤其是没有BOM的文本数据,不手动指定编码是无法正确读取文本文件的)。
同样,File类的ReadAllText也具备同样功能,不过,细心地读者可能发现Reflector中File.ReadAllText的源码是用UTF8编码的StreamReader读取文件的,其实它调用了StreamReader中的这个构造函数:
public StreamReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize)
{ /* 内容省略*/ }
那么虽然传入的一个特定的编码,但这个detectEncodingFromByteOrderMarks参数是true的,StreamReader还是会自动觉察BOM来读文件的。
代码:
public static void Main()
{
var path1 = Path.GetTempFileName();
var path2 = Path.GetTempFileName();
string content = "abc";
//使用默认没有BOM的UTF8编码写文件
File.WriteAllText(path1, content);
//使用带BOM的UTF8编码
File.WriteAllText(path2, content, Encoding.UTF8);
PrintBytes(File.ReadAllBytes(path1));
PrintBytes(File.ReadAllBytes(path2));
Console.WriteLine(File.ReadAllText(path1));
Console.WriteLine(File.ReadAllText(path2));
}
static void PrintBytes(byte[] bytes)
{
foreach (var b in bytes)
Console.Write("{0:X2} ", b);
Console.WriteLine();
}
输出:
61 62 63
EF BB BF 61 62 63
abc
abc
可以看到上面虽然有文件没有BOM,但由于缺省UTF8,所以没有错误,但是其他编码就不是这样的情况了。
比如下面这段代码,我们再用UTF32编码:
public static void Main()
{
var path1 = Path.GetTempFileName();
var path2 = Path.GetTempFileName();
string content = "abc";
//使用带BOM的UTF32编码
File.WriteAllText(path1, content, Encoding.Unicode);
//使用没有BOM的UTF32编码写文件
File.WriteAllText(path2, content, new UnicodeEncoding(false, false));
PrintBytes(File.ReadAllBytes(path1));
PrintBytes(File.ReadAllBytes(path2));
//自动觉察BOM读文件
string c1 = File.ReadAllText(path1);
//path2没BOM,实际上用缺省UTF8读文件
string c2 = File.ReadAllText(path2);
//path2没BOM,用正确度UTF16读文件
string c3 = File.ReadAllText(path2, Encoding.Unicode);
ShowContent(c1);
ShowContent(c2);
ShowContent(c3);
}
static void ShowContent(string content)
{
Console.WriteLine("读入字符数:{0} 内容:{1}", content.Length, content);
}
static void PrintBytes(byte[] bytes)
{
foreach (var b in bytes)
Console.Write("{0:X2} ", b);
Console.WriteLine();
}
输出:
FF FE 61 00 62 00 63 00 //文件1 是有BOM的UTF16
61 00 62 00 63 00 //文件2 是没有BOM的UTF16
读入字符数:3 内容:abc //自动读取文件1
读入字符数:6 内容:a //自动读取文件2
读入字符数:3 内容:abc //指定UTF16编码读取文件2
看第四行,由于没有BOM的UTF16文件被当UTF8读,原来3个字符被读成6个字符。
关于怎样去掉BOM
有些时候我们需要对文本二进制数据进行处理,这时我们需要得到全部文本的二进制数组,可读取二进制数据时BOM是附在开头的,不同编码的BOM长度又不一样(有的编码没有BOM),此时需要某种方法来将BOM过滤掉。
当你知道Encoding.GetPreamble方法后(在前面讲到过),一切不都难。
这里给出两个函数,也是较常见的情景。
一个是直接得到去除掉BOM的字节数组。
另一个是将Stream的位置移动到BOM之后,这样后续Stream操作直接针对每一个字符的二进制数据。
public static void Main()
{
var path = Path.GetTempFileName();
File.WriteAllText(path, "a123一", Encoding.UTF8);
PrintBytes(File.ReadAllBytes(path));
//1
PrintBytes(GetBytesWithoutBOM(path, Encoding.UTF8));
//2
using (Stream stream = File.OpenRead(path))
{
SkipBOM(stream, Encoding.UTF8);
int data;
while ((data = stream.ReadByte()) != -1)
Console.Write("{0:X2} ", data);
Console.WriteLine();
}
}
static byte[] GetBytesWithoutBOM(string path, Encoding enc)
{
//LINQ
return File.ReadAllBytes(path).Skip(enc.GetPreamble().Length).ToArray();
}
static void SkipBOM(Stream stream, Encoding enc)
{
stream.Seek(enc.GetPreamble().Length, SeekOrigin.Begin);
}
static void PrintBytes(byte[] bytes)
{
foreach (var b in bytes)
Console.Write("{0:X2} ", b);
Console.WriteLine();
}
输出:
EF BB BF 61 31 32 33 E4 B8 80
61 31 32 33 E4 B8 80
61 31 32 33 E4 B8 80