XmlReader在UTF-8 BOM上出现问题

7

我在我的应用程序中有以下XML解析代码:

    public static XElement Parse(string xml, string xsdFilename)
    {
        var readerSettings = new XmlReaderSettings
        {
            ValidationType = ValidationType.Schema,
            Schemas = new XmlSchemaSet()
        };
        readerSettings.Schemas.Add(null, xsdFilename);
        readerSettings.ValidationFlags |= XmlSchemaValidationFlags.ProcessInlineSchema;
        readerSettings.ValidationFlags |= XmlSchemaValidationFlags.ProcessSchemaLocation;
        readerSettings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings;
        readerSettings.ValidationEventHandler +=
            (o, e) => { throw new Exception("The provided XML does not validate against the request's schema."); };

        var readerContext = new XmlParserContext(null, null, null, XmlSpace.Default, Encoding.UTF8);

        return XElement.Load(XmlReader.Create(new StringReader(xml), readerSettings, readerContext));
    }

我正在使用它将发送到我的WCF服务的字符串解析为XML文档,以进行自定义反序列化。
当我读取文件并将它们通过网络发送(请求)时,它可以正常工作;我已经验证BOM没有被发送。 在我的请求处理程序中,我正在将响应对象序列化并将其作为字符串发送回来。 序列化过程在字符串前面添加了UTF-8 BOM,这会导致相同的代码在解析响应时出现错误。
System.Xml.XmlException : Data at the root level is invalid. Line 1, position 1.

根据我在过去一个小时左右所做的研究,XmlReader应该会遵循BOM。如果我手动从字符串前面删除BOM,则响应xml解析正常。

我是否错过了一些明显的东西,或者至少是一些隐匿的东西?

编辑:这是我用来返回响应的序列化代码:

private static string SerializeResponse(Response response)
{
    var output = new MemoryStream();
    var writer = XmlWriter.Create(output);
    new XmlSerializer(typeof(Response)).Serialize(writer, response);
    var bytes = output.ToArray();
    var responseXml = Encoding.UTF8.GetString(bytes);
    return responseXml;
}

如果只是xml文件不正确包含BOM的问题,那么我将切换到

var responseXml = new UTF8Encoding(false).GetString(bytes);

但是从我的研究中并不清楚BOM在实际XML字符串中是否是非法的;例如,请参见c# Detect xml encoding from Byte Array?


我在这里遇到了这个问题:https://dev59.com/cHVC5IYBdhLWcg3wcwsm - George Stocker
5个回答

9
在我的请求处理程序中,我正在将响应对象序列化并将其作为字符串发送回去。序列化过程会在字符串前面添加一个UTF-8 BOM,这会导致解析响应时出现相同的代码错误。
所以你想防止BOM成为序列化过程的一部分。不幸的是,你没有提供你的序列化逻辑。
你应该做的是提供一个UTF8Encoding实例,通过UTF8Encoding(bool)构造函数创建来禁用BOM的生成,并将这个Encoding实例传递给你使用的任何生成中间字符串的方法。

谢谢!在我的研究中,我遇到了这个智慧的片段,但是我找不到任何明确的指示来包含或排除BOM。 - Matt Mills
今天帮了我很多,解决方案非常好! - András Ottó

6
XML字符串必须不包含BOM,BOM只允许在使用UTF-8编码的字节数据(例如流)中出现。这是因为字符串表示形式没有被编码,而已经是一系列unicode字符的序列。
因此,似乎您错误地加载了字符串,但您未提供代码。
编辑:
感谢您发布序列化代码。
您不应该将数据写入MemoryStream,而是应该将其写入StringWriter,然后可以使用ToString将其转换为字符串。由于这避免了通过字节表示传递,因此不仅更快,而且还避免了这些问题。
像这样:
private static string SerializeResponse(Response response)
{
    var output = new StringWriter();
    var writer = XmlWriter.Create(output);
    new XmlSerializer(typeof(Response)).Serialize(writer, response);
    return output.ToString();
}

我已经做了那个更改,而且它完美地工作。谢谢! - Matt Mills
根据此链接,XML 中不存在 BOM 的限制:http://www.w3.org/TR/REC-xml/#charencoding。 - knocte
这段代码是可以正常工作的... 但是,当你切换到 StringWriter 后,<?xml ...?> 声明中的 encoding 属性似乎总是显示为 UTF-16。例如,对于 UTF-8,你需要使用 return output.ToString().Replace("utf-16", "utf-8"); - David
此外,我意识到这只是一个快速的例子,但是在示例代码中,您不应该仅仅创建一个XmlSerializer。XmlSerializer会泄漏内存--例如,请参见此处。一种常见的解决方法是将序列化程序设为类被序列化的static对象。 - David
1
@David,字符串的内存表示形式是UTF-16,这就是为什么将“UTF-16”写入输出实际上对于内存中的表示是正确的原因。话虽如此,在结果字符串上执行replace操作不仅很慢,而且您可能会替换其他字符串,这些字符串恰好包含“utf-16”,与所使用的XML编码无关。至于泄漏问题,这段代码并不会泄漏,因为它使用了使用缓存程序集的构造函数重载(而不是具有特定XmlRootAttribute的构造函数)。 - Lucero

0
这是将MemoryStream转换为可用于XmlDocument的字符串的方法(Skip函数是Linq):
    public static string Decode(MemoryStream ms)
    {
      var fileBytes = ms.ToArray();
      var isUnicode = fileBytes[0] == 0xff && fileBytes[1] == 0xfe;  //UTF-16 little endian
      var isUtf8Bom = fileBytes[0] == 0xef && fileBytes[1] == 0xbb && fileBytes[2] == 0xbf;

      string xml = isUnicode ? Encoding.Unicode.GetString(fileBytes) : 
                   (isUtf8Bom ? new UTF8Encoding(false, true).GetString(fileBytes.Skip(3).ToArray()) : new UTF8Encoding(false, true).GetString(fileBytes));

       return xml;
    } 

0

字符串中本来就不应该有BOM。
BOM用于检测原始字节数组的编码,它们不应该出现在实际字符串中。

这个字符串是从哪里来的?
你可能使用了错误的编码方式读取它。


我确保至少使用了正确的编码 :) 我已经将序列化代码添加到我的问题中。 - Matt Mills
这是一个有趣的答案...我有一个情况,我正在从远程API(我无法控制)中提取数据,我只是通过req.GetResponse().GetResponseStream()加载数据,并将该流直接放入XmlReader中。是否有更好的方法来避免这个问题? - Tom Lianza
@TomLianza:那要看它发送了哪些字节。 - SLaks

0

C#中的字符串编码为UTF-16,因此BOM是错误的。通常的规则是,将XML编码为字节数组,然后从字节数组解码。


@Stephen,我认为关于替代内存字符串表示的事情在以下Channel 9视频中:http://channel9.msdn.com/shows/Going+Deep/Vance-Morrison-CLR-Through-the-Years/ - Lucero
@Stephen,文档中说:“字符串是用于表示文本的Unicode字符的顺序集合。”后面又说:“字符串中的每个Unicode字符由Unicode标量值定义,也称为Unicode代码点或Unicode字符的序数(数字)值。每个代码点都使用UTF-16编码,并且编码的每个元素的数字值由Char对象表示。”重点是字符串不是序列化表示,而是由UTF16代码点组成的Unicode字符序列抽象。 - Lucero
@Lucero:关于字节序的问题,你说得很好!但是我仍然认为文档声明了UTF-16编码(只是没有指定字节序)。 - Stephen Cleary
@Stephen,字节序仅在将整数实体加载到处理器寄存器中时才有意义,尤其是对于比字节大的任何东西,基本上它定义了最有意义的字节或最不重要的字节先出现。因此,由于我们已经处理了16位实体,因此字节顺序在此处无意义,并且BOM在此处也没有功能。另请参阅http://unicode.org/faq/utf_bom.html#BOM -“文件中间的U + FEFF该怎么办?”(请注意,FAQ中的讨论涉及数据流,而不是我们在内存中拥有的代码点序列)。 - Lucero
@Stephen,抱歉,但你在这里完全错了。LE和BE在写入字节流时已经预定义了它们的字节序,因此不使用BOM。一旦你处理已从字节表示中加载的16位代码,字节顺序就没有意义了。请参见前面提到的FAQ,“Unicode是16位编码吗?”、“什么是UTF?”以及“UTF之间的一些区别是什么?”。 - Lucero
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接