我永远无法预测 XMLReader 的行为。有啥理解技巧吗?

Posted

技术标签:

【中文标题】我永远无法预测 XMLReader 的行为。有啥理解技巧吗?【英文标题】:I can never predict XMLReader behavior. Any tips on understanding?我永远无法预测 XMLReader 的行为。有什么理解技巧吗? 【发布时间】:2011-01-08 16:07:42 【问题描述】:

似乎每次我使用 XMLReader 时,我都会经历一堆尝试和错误,试图弄清楚我将要阅读的内容与我正在阅读的内容以及我刚刚阅读的内容。我最后总能弄明白,但在我多次使用它之后,我似乎并没有牢牢掌握调用各种函数时 XMLReader 实际在做什么。例如,当我第一次调用 Read 时,如果它读取元素开始标记,它现在是在元素标记的末尾,还是准备开始读取元素的属性?如果我调用 GetAttribute,它是否知道属性的值?如果此时我调用 ReadStartElement 会发生什么?它会完成读取起始元素,还是寻找下一个元素,跳过所有属性?如果我想读取多个元素怎么办?尝试读取下一个元素并确定其名称的最佳方法是什么。 IsStartElement 之后的 Read 会起作用,还是 IsStartElement 会返回有关我刚刚读取的元素之后的节点的信息?

如您所见,我确实缺乏对 XMLReader 在其读取的各个阶段所处的位置以及其状态如何受到各种读取函数的影响的理解。是否有一些我根本没有注意到的简单模式?

这是另一个问题示例(取自回复):

string input = "<machine code=\"01\">The Terminator" +
   "<part code=\"01a\">Right Arm</part>" +
   "<part code=\"02\">Left Arm</part>" +
   "<part code=\"03\">Big Toe</part>" +
   "</machine>";

using (System.IO.StringReader sr = new System.IO.StringReader(input))

   using (XmlTextReader reader = new XmlTextReader(sr))
   
      reader.WhitespaceHandling = WhitespaceHandling.None;
      reader.MoveToContent();

      while(reader.Read())
      
         if (reader.Name.Equals("machine") && (reader.NodeType == XmlNodeType.Element))
         
            Console.Write("Machine code 0: ", reader.GetAttribute("code"));
            Console.WriteLine(reader.ReadElementString("machine"));
         
         if(reader.Name.Equals("part") && (reader.NodeType == XmlNodeType.Element))
         
            Console.Write("Part code 0: ", reader.GetAttribute("code"));
            Console.WriteLine(reader.ReadElementString("part"));
         
      
   

第一个问题,机器节点被完全跳过。 MoveToContent 似乎移动到机器元素的内容,导致它永远不会被解析。此外,如果您跳过 MoveToContent,则会收到错误消息:“'Element' is an invalid XmlNodeType.”试图读取元素字符串,我无法完全解释。

下一个问题是,在读取第一个部分元素时,ReadElementString 似乎在读取后将阅读器定位在下一个部分元素的开头。这会导致 reader.Read 在下一个循环开始时跳过下一个部分元素,直接跳到最后一个部分元素。所以这段代码的最终输出是:

零件代码 01a:右臂

零件代码 03:大脚趾

这是我试图理解的 XMLReader 混淆行为的一个典型例子。

【问题讨论】:

【参考方案1】:

事情是这样的……我已经编写了相当多的序列化代码(包括大量的 xml 处理),我发现自己与您完全处于同一条船上。我有一个非常简单的指导,因此:不要

我很乐意使用 XmlWriter 作为快速编写 xml 的一种方式,但在选择再次实现 IXmlSerializable 之前我会走火入魔 - 我只需编写一个单独的 DTO 并映射数据到那个;这也意味着架构(对于“mex”、“wsdl”等)是免费提供的。

【讨论】:

你能告诉我 DTO 是什么意思吗? 本质上是一个面向序列化/传输的对象模型——例如,如果您的 main 对象模型是不可变的(没有“setter”),则 DTO 可能具有读/写属性(因为这适用于某些序列化程序)或扁平层次结构。 那么你用什么来解析/读取 XML 呢? X文档? xml文档?那效率较低,对吗?或者也许更相关的问题是,更具体地说,您如何序列化和反序列化对象? (我有一个非常简单的对象,我正在写入一个非常小的文件。) @BlueMonkMN - 在 一般 的意义上,我可能会使用 protobuf-net ;-p 如果它必须是 xml,那么 XmlSerializer 与匹配的 DTO预期的布局(因此无需额外的工作),或XDocument. 在您的响应中包含如何使用 DTO 来提供 XML 序列化的代码示例是否合适,这样我们就不必在绊倒之前漫无目的地四处研究 DTO关于 XML 的部分?我很久以前使用过(DTO)XML序列化,但通常会尽量避免它,因为当我真的想将所有序列化代码放在一个中时,将属性应用到所有类成员以使其工作感觉很麻烦地方。 (如果你在不同的文件中有一个层次结构的类,那么遵循它可能会很痛苦。)【参考方案2】:

我最新的解决方案(适用于我当前的情况)是在实现状态机时坚持使用 Read()、IsStartElement(name) 和 GetAttribute(name)。

using (System.Xml.XmlReader xr = System.Xml.XmlTextReader.Create(stm))

   employeeSchedules = new Dictionary<string, EmployeeSchedule>();
   EmployeeSchedule emp = null;
   WeekSchedule sch = null;
   TimeRanges ranges = null;
   TimeRange range = null;
   while (xr.Read())
   
      if (xr.IsStartElement("Employee"))
      
         emp = new EmployeeSchedule();
         employeeSchedules.Add(xr.GetAttribute("Name"), emp);
      
      else if (xr.IsStartElement("Unavailable"))
      
         sch = new WeekSchedule();
         emp.unavailable = sch;
      
      else if (xr.IsStartElement("Scheduled"))
      
         sch = new WeekSchedule();
         emp.scheduled = sch;
      
      else if (xr.IsStartElement("DaySchedule"))
      
         ranges = new TimeRanges();
         sch.daySchedule[int.Parse(xr.GetAttribute("DayNumber"))] = ranges;
         ranges.Color = ParseColor(xr.GetAttribute("Color"));
         ranges.FillStyle = (System.Drawing.Drawing2D.HatchStyle)
            System.Enum.Parse(typeof(System.Drawing.Drawing2D.HatchStyle),
            xr.GetAttribute("Pattern"));
      
      else if (xr.IsStartElement("TimeRange"))
      
         range = new TimeRange(
            System.Xml.XmlConvert.ToDateTime(xr.GetAttribute("Start"),
            System.Xml.XmlDateTimeSerializationMode.Unspecified),
            new TimeSpan((long)(System.Xml.XmlConvert.ToDouble(xr.GetAttribute("Length")) * TimeSpan.TicksPerHour)));
         ranges.Add(range);
      
   
   xr.Close();

Read 后,如果您只是读取了一个起始元素(可选地检查读取的元素的名称),IsStartElement 将返回 true,并且您可以立即访问该元素的所有属性。如果您只需要阅读元素和属性,那么这非常简单。

编辑 问题中发布的新示例带来了一些其他挑战。读取那个 XML 的正确方法似乎是这样的:

using (System.IO.StringReader sr = new System.IO.StringReader(input))

   using (XmlTextReader reader = new XmlTextReader(sr))
   
      reader.WhitespaceHandling = WhitespaceHandling.None;

      while(reader.Read())
      
         if (reader.Name.Equals("machine") && (reader.NodeType == XmlNodeType.Element))
         
            Console.Write("Machine code 0: ", reader.GetAttribute("code"));
            Console.WriteLine(reader.ReadString());
         
         if(reader.Name.Equals("part") && (reader.NodeType == XmlNodeType.Element))
         
            Console.Write("Part code 0: ", reader.GetAttribute("code"));
            Console.WriteLine(reader.ReadString());
         
      
   

您必须使用 ReadString 而不是 ReadElementString 以避免读取结束元素并跳到下一个元素的开头(让下面的 Read() 跳过结束元素,这样它就不会跳过下一个开始元素)。这似乎仍然有些令人困惑并且可能不可靠,但它适用于这种情况。

经过一些额外的思考,我的观点是 XMLReader 太令人困惑了如果您使用任何方法来读取内容,而不是 Read 方法。如果您将自己限制在 Read 方法以从 XML 流中读取,我认为会简单得多。下面是它如何与新示例一起使用(再一次,似乎 IsStartElement、GetAttribute 和 Read 是关键方法,您最终会得到一个状态机):

while(reader.Read())

   if (reader.IsStartElement("machine"))
   
      Console.Write("Machine code 0: ", reader.GetAttribute("code"));
   
   if(reader.IsStartElement("part"))
   
      Console.Write("Part code 0: ", reader.GetAttribute("code"));
   
   if (reader.NodeType == XmlNodeType.Text)
   
      Console.WriteLine(reader.Value);
   

【讨论】:

以上是关于我永远无法预测 XMLReader 的行为。有啥理解技巧吗?的主要内容,如果未能解决你的问题,请参考以下文章

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

如果没有空格分隔符,为啥 XmlReader 会跳过所有其他元素?

如何在多类预测中得到未知类?

使用 PHP 和 XMLReader 解析 XML

UITableViewRowAction 行为不可预测

在 XmlReader .NET 4.0 中加载失败目录文件