XmlReader 的行为与换行符不同

XmlReader behaves different with line breaks

如果数据在一行 index=int.Parse(logDataReader.ReadElementContentAsString());value=double.Parse(logDataReader.ReadElementContentAsString(), 使光标向前移动。如果我取消这些调用,我会在调试中看到它循环 6 次。

在下面的第一个 (<logData id="Bravo">) 中只读取了 3 <data>(并且它们是错误的,因为该值用于下一个索引)。在第二个 (<logData id="Bravo">) 上,所有 <data> 都被读取。

编辑 xml 和插入换行符不是一个选项,因为该文件是动态创建的(由 XML 作者)。 NewLineChars 设置是一个换行符。来自 XMLwriter 它实际上只是一行 - 我将它分解以找出它在哪里中断。在浏览器中正常显示。

如何解决这个问题?

这是我的 XML:

<?xml version="1.0" encoding="utf-8"?>
<log>
   <logData id="Alpha">
      <data><index>100</index><value>150</value></data>
      <data><index>110</index><value>750</value></data>
      <data><index>120</index><value>750</value></data>
      <data><index>130</index><value>150</value></data>
      <data><index>140</index><value>0</value></data>
      <data><index>150</index><value>222</value></data>
   </logData>
   <logData id="Bravo">
      <data>
         <index>100</index>
         <value>25</value>
      </data>
      <data>
         <index>110</index>
         <value>11</value>
      </data>
      <data>
         <index>120</index>
         <value>1</value>
      </data>
      <data>
         <index>130</index>
         <value>25</value></data>
      <data>
         <index>140</index>
         <value>0</value>
      </data>
      <data>
         <index>150</index>
         <value>1</value>
      </data>
   </logData>
</log>

还有我的代码:

static void Main(string[] args)
{
    List<LogData> logDatas = GetLogDatasFromFile("singleVersusMultLine.xml");
    Debug.WriteLine("Main");
    Debug.WriteLine("logData");
    foreach (LogData logData in logDatas)
    {
        Debug.WriteLine($"    logData.ID {logData.ID}");
        foreach(LogPoint logPoint in logData.LogPoints)
        {
            Debug.WriteLine($"        logData.Index {logPoint.Index}  logData.Value {logPoint.Value}");
        }
    }
    Debug.WriteLine("end");
}       
public static List<LogData> GetLogDatasFromFile(string xmlFile)
{
    List<LogData> logDatas = new List<LogData>();

    using (XmlReader reader = XmlReader.Create(xmlFile))
    {
        // move to next "logData"
        while (reader.ReadToFollowing("logData"))
        {
            var logData = new LogData(reader.GetAttribute("id"));
            using (var logDataReader = reader.ReadSubtree())
            {
                // inside "logData" subtree, move to next "data"
                while (logDataReader.ReadToFollowing("data"))
                {
                    // move to index
                    logDataReader.ReadToFollowing("index");
                    // read index
                    var index = int.Parse(logDataReader.ReadElementContentAsString());
                    // move to value
                    logDataReader.ReadToFollowing("value");
                    // read value
                    var value = double.Parse(logDataReader.ReadElementContentAsString(), CultureInfo.InvariantCulture);
                    logData.LogPoints.Add(new LogPoint(index, value));
                }
            }
            logDatas.Add(logData);
        }
    }
    return logDatas;
}

public class LogData
{
    public string ID { get; }
    public List<LogPoint> LogPoints { get; } = new List<LogPoint>();
    public LogData (string id)
    {
        ID = id;
    }
}
public class LogPoint
{
    public int Index { get; }
    public double Value { get; }
    public LogPoint ( int index, double value)
    {
        Index = index;
        Value = value;
    }
}

我找到了解决方法,但对我来说不是一个可以接受的答案。 XMLreader 不应因换行而表现不同。

XmlWriter 这将在文本中放置换行符:

XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
xmlWriterSettings.NewLineOnAttributes = true;
xmlWriterSettings.Indent = true;
using (XmlWriter xmlWriter = XmlWriter.Create(fileNameXML, xmlWriterSettings))
{

我找到了这个 here

您的问题如下。根据 documentation for XmlReader.ReadElementContentAsString():

This method reads the start tag, the contents of the element, and moves the reader past the end element tag.

并且来自 documentationXmlReader.ReadToFollowing(String)

It advances the reader to the next following element that matches the specified name and returns true if a matching element is found.

因此,在调用ReadElementContentAsString()之后,由于reader已经前进到下一个节点,它可能已经定位到下一个<value><data> 节点。然后,当您调用 ReadToFollowing() 时,此元素节点被 跳过 ,因为该方法无条件地移动到具有正确名称的 next 节点。但是,如果 XML 被缩进,那么在调用 ReadElementContentAsString() 之后的下一个节点将是一个 XmlNodeType.Whitespace 节点,以防止出现此错误。

解决方法是在调用ReadElementContentAsString()后检查reader是否已经正确定位。首先介绍一下扩展方法:

public static class XmlReaderExtensions
{
    public static bool ReadToFollowingOrCurrent(this XmlReader reader, string localName, string namespaceURI)
    {
        if (reader == null)
            throw new ArgumentNullException(nameof(reader));
        if (reader.NodeType == XmlNodeType.Element && reader.LocalName == localName && reader.NamespaceURI == namespaceURI)
            return true;
        return reader.ReadToFollowing(localName, namespaceURI);
    }
}

然后修改你的代码如下:

public static List<LogData> GetLogDatasFromFile(string xmlFile)
{
    List<LogData> logDatas = new List<LogData>();

    using (XmlReader reader = XmlReader.Create(xmlFile))
    {
        // move to next "logData"
        while (reader.ReadToFollowing("logData", ""))
        {
            var logData = new LogData(reader.GetAttribute("id"));
            using (var logDataReader = reader.ReadSubtree())
            {
                // inside "logData" subtree, move to next "data"
                while (logDataReader.ReadToFollowing("data", ""))
                {
                    // move to index
                    logDataReader.ReadToFollowing("index", "");
                    // read index
                    var index = XmlConvert.ToInt32(logDataReader.ReadElementContentAsString());
                    // move to value
                    logDataReader.ReadToFollowingOrCurrent("value", "");
                    // read value
                    var value = XmlConvert.ToDouble(logDataReader.ReadElementContentAsString());
                    logData.LogPoints.Add(new LogPoint(index, value));
                }
            }
            logDatas.Add(logData);
        }
    }
    return logDatas;
}       

备注:

  • 总是喜欢使用 XmlReader 方法,其中本地名称和名称空间是单独指定的,例如 XmlReader.ReadToFollowing (String, String). When you use a method such as XmlReader.ReadToFollowing(String) 接受单个限定名称,您正在隐式地硬编码选择 XML prefix,这通常不是一个好主意。 XML 解析应该独立于前缀选择。

  • 虽然您使用 CultureInfo.InvariantCulture 语言环境正确解析了双精度,但使用 XmlConvert class 中的方法来正确处理解析和格式设置会更容易.

  • XmlReader.ReadSubtree()XmlReader 保留在 on 正在读取的元素的 EndElement 节点上,因此您应该之后需要调用 ReadToFollowingOrCurrent()。 (顺便说一句,很好地使用 ReadSubtree() 避免读得太少或太多;通过使用这种方法可以避免 XmlReader 的几个常见错误。)

  • 如您所见,使用 XmlReader 手动读取 XML 的代码应始终使用格式化和未格式化 XML 进行单元测试,因为某些错误只会与一个或另一个一起出现。 (有关此类的其他示例,请参见 this answer, this one and 。)

工作示例 .Net fiddle here.

确实那个代码(我在你的另一个问题中提供给你的)是错误的。 ReadToFollowing 将读取到具有该名称的下一个元素,即使它的光标已经定位在具有该名称的元素上。当有空格时 - 在您阅读 index 后,光标会移动到该空格,并且 ReadToFollowing("value") 会如您所愿地工作。但是,如果没有空格,则光标已经在 value 节点上,因此 ReadToFollowing("value") 读取到后续 "data" 节点中的下一个 "value"。

我认为以下方法更安全:

public static List<LogData> GetLogDatasFromFile(string xmlFile) {
    List<LogData> logDatas = new List<LogData>();

    using (XmlReader reader = XmlReader.Create(xmlFile)) {
        LogData currentData = null;
        while (reader.Read()) {
            if (reader.IsStartElement("logData")) {
                // we are positioned on start of logData
                if (currentData != null)
                    logDatas.Add(currentData);
                currentData = new LogData(reader.GetAttribute("id"));
            }
            else if (reader.IsStartElement("data")) {
                // we are on start of "data"
                // we always have "currentData" at this point                        
                Debug.Assert(currentData != null);
                reader.ReadToFollowing("index");
                var index = int.Parse(reader.ReadElementContentAsString());
                // check if we are not already on "value"
                if (!reader.IsStartElement("value"))
                    reader.ReadToFollowing("value");
                var value = double.Parse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture);
                currentData.LogPoints.Add(new LogPoint(index, value));
            }
        }

        if (currentData != null)
            logDatas.Add(currentData);
    }

    return logDatas;
}