C# XmlReader 根据我调用阅读器方法的方式读取 XML 错误且不同

Posted

技术标签:

【中文标题】C# XmlReader 根据我调用阅读器方法的方式读取 XML 错误且不同【英文标题】:C# XmlReader reads XML wrong and different based on how I invoke the reader's methods 【发布时间】:2020-02-23 23:37:40 【问题描述】:

所以我目前对 C# XmlReader 工作原理的理解是,当我将它包装在以下构造中时,它需要一个给定的 XML 文件并逐个节点地读取它:

using System.Xml;
using System;
using System.Diagnostics;
...
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreComments = true;
settings.IgnoreWhitespace = true;
settings.IgnoreProcessingInstructions = true;
using (XmlReader reader = XmlReader.Create(path, settings))

    while (reader.Read())
    
        // All reader methods I call here will reference the current node
        // until I move the pointer to some further node by calling methods like
        // reader.Read(), reader.MoveToContent(), reader.MoveToElement() etc
    

为什么以下两个 sn-ps(在上述构造中)会产生两个截然不同的结果,即使它们都调用相同的方法?

I used this example file for testing.

Debug.WriteLine(new string(' ', reader.Depth * 2) + "<" + reader.NodeType.ToString() + "|" + reader.Name + ">" + reader.ReadString() + "</>");

(片段 1) 对比 (片段 2)

string xmlcontent = reader.ReadString();
string xmlname = reader.Name.ToString();
string xmltype = reader.NodeType.ToString();
int xmldepth = reader.Depth;
Debug.WriteLine(new string(' ', xmldepth * 2) + "<" + xmltype + "|" + xmlname + ">" + xmlcontent + "</>");

片段 1 的输出:

<XmlDeclaration|xml></>
<Element|rss></>
    <Element|head></>
        <Text|>Test Xml File</>
      <Element|description>This will test my xml reader</>
    <EndElement|head></>
    <Element|body></>
        <Element|g:id>1QBX23</>
        <Element|g:title>Example Title</>
        <Element|g:description>Example Description</>
      <EndElement|item></>
      <Element|item></>
          <Text|>2QXB32</>
        <Element|g:title>Example Title</>
        <Element|g:description>Example Description</>
      <EndElement|item></>
    <EndElement|body></>
  <EndElement|xml></>
<EndElement|rss></>

是的,这是在我的输出窗口中格式化的。可以看出,它跳过了某些元素并为其他一些元素输出了错误的深度。因此,NodeTypes 是正确的,不像 Snippet Number 2,它输出:

<XmlDeclaration|xml></>
  <Element|xml></>
      <Element|title></>
      <EndElement|title>Test Xml File</>
      <EndElement|description>This will test my xml reader</>
    <EndElement|head></>
      <Element|item></>
        <EndElement|g:id>1QBX23</>
        <EndElement|g:title>Example Title</>
        <EndElement|g:description>Example Description</>
      <EndElement|item></>
        <Element|g:id></>
        <EndElement|g:id>2QXB32</>
        <EndElement|g:title>Example Title</>
        <EndElement|g:description>Example Description</>
      <EndElement|item></>
    <EndElement|body></>
  <EndElement|xml></>
<EndElement|rss></>

再一次,深度搞砸了,但它不像片段编号 1 那样重要。它还跳过了一些元素并分配了错误的节点类型。

为什么不能输出预期的结果?为什么这两个 sn-ps 会产生两个完全不同的输出,具有不同的深度、NodeTypes 和跳过的节点? 我很感激这方面的任何帮助。我为此搜索了很多答案,但似乎我是唯一遇到这些问题的人。我在 Visual Studio 2017 中使用 .NET Framework 4.6.2 和 Asp.net Web 窗体。

【问题讨论】:

顺序很重要,因为 ReadString 将推进当前位置。但是,文档建议无论如何不要使用该方法。 @DaveM 我知道文档建议使用ReadContentAsString()ReadElementContentAsString(),但目前我想遍历所有节点,这两种方法在遇到没有任何内容的节点。为什么 ReadString() 会推进位置?有什么我可以使用的替代品吗? XmlReader 是在 .net 中解析 xml 的“硬方法”。但它可能更有效。它本质上是基于流的,因此您可以读取/解析非常大的 xml 文档,而无需将它们完全放在内存中。为了实现这一点,许多读取操作会推进位置(因为必须推进底层流,并且“返回”并不容易或不可能,具体取决于流的类型)。 如果你对性能要求不高,也不想解析巨大的 xml 文件,我建议改用 XmlDocument。 如果你事先知道xml格式,使用XmlSerializer(或等效的)就更容易了。 【参考方案1】:

首先,您使用的是XmlReader.ReadString() 已弃用的方法

XmlReader.ReadString 方法

... 将元素或文本节点的内容作为字符串读取。但是,我们建议您改用ReadElementContentAsString 方法,因为它提供了一种更直接的方式来处理此操作。

但是,除了警告我们不要使用该方法之外,文档并没有准确说明它的实际作用。要确定这一点,我们需要转到reference source:

public virtual  string  ReadString() 
    if (this.ReadState != ReadState.Interactive) 
        return string.Empty;
    
    this.MoveToElement();
    if (this.NodeType == XmlNodeType.Element) 
        if (this.IsEmptyElement) 
            return string.Empty;
        
        else if (!this.Read()) 
            throw new InvalidOperationException(Res.GetString(Res.Xml_InvalidOperation));
        
        if (this.NodeType == XmlNodeType.EndElement) 
            return string.Empty;
        
    
    string result = string.Empty;
    while (IsTextualNode(this.NodeType)) 
        result += this.Value;
        if (!this.Read()) 
            break;
        
    
    return result;

此方法执行以下操作:

    如果当前节点为空元素节点,则返回空字符串。

    如果当前节点是一个非空元素,提前阅读器

    如果当前节点是元素的末尾,则返回一个空字符串。

    虽然当前节点是文本节点,但将文本添加到字符串中并提前阅读。只要当前节点不是文本节点,就返回累积的字符串。

由此我们可以看出,这种方法是为了提高读者的阅读能力而设计的。我们还可以看到,给定混合内容 XML,如 &lt;head&gt;text &lt;b&gt;BOLD&lt;/b&gt; more text&lt;/head&gt;ReadString() 只会部分读取 &lt;head&gt; 元素,而将阅读器定位在 &lt;b&gt;。这种奇怪之处可能是微软弃用该方法的原因。

我们还可以看到为什么您的两个 sn-ps 功能不同。首先,在调用ReadString() 并推进读者之前,您会收到reader.Depthreader.NodeType。在推进阅读器之后,您将获得这些属性。

由于您的意图是遍历节点并获取每个节点的值,而不是 ReadString()ReadElementContentAsString(),您应该只使用 XmlReader.Value

获取当前节点的文本值。

因此,您更正后的代码应如下所示:

 string xmlcontent = reader.Value;
 string xmlname = reader.Name.ToString();
 string xmltype = reader.NodeType.ToString();
 int xmldepth = reader.Depth;
 Console.WriteLine(new string(' ', xmldepth * 2) + "<" + xmltype + "|" + xmlname + ">" + xmlcontent + "</>");

XmlReader 很难使用。您总是需要检查文档以确定给定方法将读者定位的确切位置。例如,XmlReader.ReadElementContentAsString() 将阅读器移动到元素的末尾过去,而XmlReader.ReadSubtree() 将阅读器移动到元素的末尾。但作为一般规则,任何名为 Read 的方法都会使读者进步,因此您需要小心在外部 while (reader.Read()) 循环内使用 Read 方法。

演示小提琴here.

【讨论】:

非常感谢!明天我会在工作中测试你的答案。这似乎是一个合乎逻辑的结论。 +1 附带说明,在循环中处理元素本身时,如何修改固定的 sn-p 以读取元素文本内容(如果它只有一个文本节点)?跨度> @SearchingSolutions - 你真的不能,因为XmlReader 是只进的。问题如前所述,该元素可能具有mixed content,例如&lt;body&gt;text &lt;b&gt;BOLD&lt;/b&gt; more text&lt;/body&gt;。您可以做的是推迟发出元素节点并开始读取其子节点。如果只遇到文字,就累加;如果遇到其他问题,则发出延迟节点 + 累积文本。 如果您事先知道 XML 架构,您有更多选择。 我找到了一个很好的解决方案,遗憾的是我不能确定输入文件会有什么方案。无论如何,感谢您抽出宝贵的时间!

以上是关于C# XmlReader 根据我调用阅读器方法的方式读取 XML 错误且不同的主要内容,如果未能解决你的问题,请参考以下文章

XmlReader:无法解析不带引号的属性

C# 使用 XMLReader(?) 读取子元素

有没有办法让 XmlReader 将字符引用保留为文本而不是转换它?

XmlReader 创建空字符串 C#

c# 操作xml之xmlReader

为啥 XmlReader 跳过标签?