XmlReader ведет себя по-разному с переносами строк

Если данные находятся в одной строке, index=int.Parse(logDataReader.ReadElementContentAsString()); а такжеvalue=double.Parse(logDataReader.ReadElementContentAsString(),заставить курсор двигаться вперед. Если я возьму эти вызовы, я увижу это цикл 6 раз в отладке.

В следующем только 3 <data> читаются (и они неверны, так как значение для следующего индекса) на первом (<logData id="Bravo">). На втором (<logData id="Bravo">) все <data> читаются

Это не возможность редактировать xml и вставлять разрывы строк, так как этот файл создается динамически (XMLwriter). 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;
    }
}

3 ответа

Решение

Ваша проблема заключается в следующем. Согласно документации для XmlReader.ReadElementContentAsString():

Этот метод читает начальный тег, содержимое элемента и перемещает читателя за тег конечного элемента.

И из документации для XmlReader.ReadToFollowing(String):

Он продвигает читателя к следующему следующему элементу, который соответствует указанному имени, и возвращает true, если соответствующий элемент найден.

Таким образом, после звонка ReadElementContentAsString(), поскольку читатель был продвинут на следующий узел, он может быть уже расположен на следующем узле <value> или же <data> узел. Затем, когда вы звоните ReadToFollowing() этот элементный узел пропускается, потому что метод безусловно переходит к следующему узлу с правильным именем. Но если XML имеет отступ, то следующий узел сразу после вызова ReadElementContentAsString() будет XmlNodeType.Whitespace узел, защищающий от этой ошибки.

Решение состоит в том, чтобы проверить, правильно ли установлен считыватель после вызова ReadElementContentAsString(), Сначала введите следующий метод расширения:

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), Когда вы используете метод, такой как XmlReader.ReadToFollowing(String) который принимает одно квалифицированное имя, вы неявно жестко программируете выбор префикса XML, что, как правило, не очень хорошая идея. Синтаксический анализ XML не должен зависеть от выбора префикса.

  • В то время как вы правильно проанализировали свой дубль, используя CultureInfo.InvariantCulture локаль, еще проще использовать методы из XmlConvert класс для правильной обработки и форматирования.

  • XmlReader.ReadSubtree() оставляет XmlReader расположен на EndElement узел читаемого элемента, поэтому вам не нужно вызывать ReadToFollowingOrCurrent() после этого. (Хорошее использование ReadSubtree() кстати, избегать читать слишком мало или слишком много; с помощью этого метода можно избежать нескольких частых ошибок с XmlReader.)

  • Как вы обнаружили, код, который вручную читает XML, используя XmlReader должен всегда подвергаться модульному тестированию как с отформатированным, так и с неформатированным XML, потому что определенные ошибки будут возникать только с одним или другим. (См., Например, этот ответ, этот и этот, а также другие примеры такого.)

Рабочий образец.Net скрипка здесь.

Действительно, этот код (который я вам предоставил в вашем другом вопросе) неверен. ReadToFollowing будет читать следующий элемент с этим именем, даже если его курсор уже расположен на элементе с этим именем. Когда есть пробел - после прочтения indexкурсор перемещается в этот пробел и ReadToFollowing("value") работает как вы ожидаете. Однако, если пробелов нет, курсор уже включен value узел и так ReadToFollowing("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;
}

Я нашел решение, но для меня не приемлемый ответ. XMLreader не должен вести себя по-разному с переносами строк.

В XmlWriter это вставит разрывы строк в тексте:

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

Я нашел это здесь.

Другие вопросы по тегам