.NET中带BOM字符编码的读写

 

.NET中带BOM字符编码的读写

 

问题描述:

最近遇到下面这样的问题,把一个UTF-8编码的XML文件上传到服务器,然后使用XmlDocument解析该XML文件的时候,提示文件格式错误,结果发现从上传的文件流中读取出的XML字符串前多了一个“?”,导致解析失败。从上传的XML文件流中分析,该流的前三位是EF、BB和BF,这是UTF-8的BOM标识符。BOM到底是什么?该如何正确使用?遇到这样的问题该如何避免?请看下文。

 

1、什么是字符顺序标记(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

 

2、.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);

 

Encoding类中的GetPreamble方法可以返回当前编码提供的BOM

 

3、文件读写和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个字符。

 

4、关于怎样去掉BOM

有些时候我们需要对文本二进制数据进行处理,这时我们需要得到全部文本的二进制数组,可读取二进制数据时BOM是附在开头的,不同编码的BOM长度又不一样(有的编码没有BOM),此时需要某种方法来将BOM过滤掉。

当你知道Encoding.GetPreamble方法后(在前面讲到过),一切不都难。

 

这里给出三个函数,也是较常见的情景。

一个是直接得到去除掉BOM的字节数组。

二是将Stream的位置移动到BOM之后,这样后续Stream操作直接针对每一个字符的二进制数据。

三是使用StreamReader(detectEncodingFromByteOrderMarks为true)自动检测BOM,并跳过。

 

        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();

            }

            //3
            using (Stream stream = File.OpenRead(path))
            {

                StreamReader reader = new StreamReader(stream, Encoding.UTF8, true);
                char[] cs = new char[64];
                StringBuilder sb = new StringBuilder();
                int len = 0;
                while ((len = reader.Read(cs, 0, cs.Length)) > 0)
                {
                    sb.Append(cs, 0, len);
                }

                string str = sb.ToString();
                byte[] bs = Encoding.UTF8.GetBytes(str);
                foreach (byte b in bs)
                {
                    Console.Write("{0:X2} ", b);
                }
            }
        }

        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

61 31 32 33 E4 B8 80

 

(结果均正确)

 

 对于文章开头提到的问题,最好的解决办法是使用StreamReader从文件上传流中读取字符串,然后再使用XmlDocument来解析。

 

 

 

 

 

你可能感兴趣的:(.net,String,Stream,Path,byte,encoding)