Уменьшите дублирование кода при поиске значений в XML с помощью XPath

Я пишу код для анализа XML-файла следующего формата (усеченного для простоты)

<?xml version="1.0" encoding="UTF-8"?>
<ship name="Foo">
 <base_type>Foo</base_type>
 <GFX>fooGFX</GFX>
 ....
</ship>

Я использую словарь, состоящий из пар ключей параметра и и xpath к значению, и запрашиваю его, чтобы загрузить все значения в различные переменные. Однако я замечаю, что будет огромное количество дублирования кода. Фактически, для каждого значения, которое я хочу получить, мне придется написать еще одну строку почти идентичного кода. Вот мой код:

class Ship
{
    public Ship()
    {
        paths = new Dictionary<string, string>();
        doc = new XmlDocument();
        //Define path to various elements
        paths.Add("name", "/ship/@name");
        paths.Add("base_type", "/ship/base_type");
        paths.Add("GFX", "/ship/GFX");
    }
    public void LoadFile(string filename)
    {// Loads the file and grabs the parameters
        doc.Load(filename);
        Name = doc.SelectSingleNode(paths["name"]).Value;
        Base_type = doc.SelectSingleNode(paths["base_type"]).Value;
        GFX = doc.SelectSingleNode(paths["GFX"]).Value;
    }

    public Dictionary<string, string> paths; //The XPaths to the various elements, define them in constructor
    public XmlDocument doc;
    public string Name;
    public string Base_type;
    public string GFX;
}

Обратите внимание на дублирование здесь:

variable = doc.SelectSingleNode(paths["variable_name"]).value. 

Будет еще много переменных, поэтому этот раздел будет объемным.

Есть ли способ упростить это? Если бы это был С++, я бы, наверное, попробовал указатели, но я знаю, что они не рекомендуются для использования в С#, так что есть ли аналогичный способ?

Я бы искал что-то, что я мог бы дать список имен переменных и xpaths, и чтобы код вытащил все значения и загрузил их в переменную в каком-то цикле или что-то в этом роде. Я хочу использовать XPath, потому что ожидаю, что формат этого файла может периодически меняться.

Любые идеи?

Заранее спасибо.

РЕДАКТИРОВАТЬ: я также хотел бы иметь возможность изменять эти данные и сохранять их обратно. Я не против сохранения всего нового дерева, если это необходимо, но было бы неплохо изменить данные на месте, если это возможно. Мне не нужно решение для модификации, но мне просто нужно, чтобы эта опция была открыта.


person Biosci3c    schedule 04.03.2012    source источник
comment
Последняя часть — сохранение изменений — удобно выполняется с помощью другого основного метода класса XmlSerializer — Serialize().   -  person Dimitre Novatchev    schedule 05.03.2012


Ответы (3)


Хорошим способом заполнения объекта является использование XmlSerializer.Deserialize. ().

Что-то вроде этого:

namespace TestSerialization
{
    using System;
    using System.IO;
    using System.Xml;
    using System.Xml.Serialization;

    public class TestSerialization
    {
        static void Main(string[] args)
        {
            string theXml =
@"<ship name='Foo'>
 <base_type>Foo</base_type>
 <GFX>fooGFX</GFX>
</ship>";
            Ship s = Ship.Create(theXml);

            // Write out the properties of the object.
            Console.Write(s.Name + "\t" + s.GFX);
        }
    }

    [XmlRoot("ship")]
    public class Ship
    {
        public Ship() { }

        public static Ship Create(string xmlText)
        {
            // Create an instance of the XmlSerializer specifying type.
            XmlSerializer serializer = new XmlSerializer(typeof(Ship));

            StringReader sr = new StringReader(xmlText);

            XmlReader xreader = new XmlTextReader(sr);

            // Use the Deserialize method to restore the object's state.
            return (Ship)serializer.Deserialize(xreader);
        }

        [XmlAttribute("name")]
        public string Name;

        [XmlElement("base_type")]
        public string Base_type;

        public string GFX;
    }
}

ОБНОВЛЕНИЕ: ОП добавил дополнительный вопрос:

Я также хотел бы иметь возможность изменять эти данные и сохранять их обратно. Я не против сохранения всего нового дерева, если это необходимо

Просто используйте метод XmlSerializer.Serialize().

Вот типичный пример его использования:

  // Create an XmlTextWriter using a FileStream.
  Stream fs = new FileStream(filename, FileMode.Create);
  XmlWriter writer = 
  new XmlTextWriter(fs, Encoding.Unicode);
  // Serialize using the XmlTextWriter.
  serializer.Serialize(writer, yourObject);
  writer.Close();
person Dimitre Novatchev    schedule 04.03.2012
comment
Не могли бы вы показать мне, как это будет работать в контексте моего XML-файла? - person Biosci3c; 05.03.2012
comment
@ Biosci3c: Да, я обновил свой ответ полным решением. - person Dimitre Novatchev; 05.03.2012
comment
@ssg, да, я так думаю. Это требует минимального кодирования, очень проста в использовании и очень мощна. - person Dimitre Novatchev; 05.03.2012
comment
Извините за поздний ответ, но я серьезно думаю, что собираюсь пойти по этому пути, так как это кажется намного проще, чем анализировать как сумасшедший. Однако меня смущает заключенный в квадратные скобки синтаксис [XmlRoot(ship)], поскольку я никогда раньше его не видел. Не могли бы вы немного объяснить это (например, это шаблон, класс и т. д.)? Спасибо. - person Biosci3c; 29.04.2012
comment
@Biosci3c: Добро пожаловать. И да, пожалуйста, дайте мне знать, как это работает на практике. - person Dimitre Novatchev; 30.04.2012
comment
Извините, отредактированный комментарий. Не могли бы вы объяснить мне терминологию [xmlroot(ship)] в квадратных скобках, поскольку я никогда раньше ее не видел. - person Biosci3c; 30.04.2012
comment
Ты жжешь!!! Это такой удивительный метод. Я люблю, когда все так просто. Большой принять для вас. Кажется, я могу получить доступ к элементам без необходимости писать много кода для каждого элемента. Еще не пробовал писать в xml, но уверен, что это будет красиво и просто. Это решает для меня большую проблему, а именно наличие представления класса корабля и представления xml. Теперь я могу редактировать оба сразу. Очень хорошо! Спасибо еще раз. - person Biosci3c; 30.04.2012
comment
@Biosci3c: Добро пожаловать. Атрибут C# [XmlRoot("ship")] указывает, что верхний элемент сериализованного XML-представления объекта должен иметь имя "ship". Подробнее о различных атрибутах сериализации можно прочитать здесь: msdn.microsoft.com/en-us/library/83y7df3e(v=vs.100).aspx - person Dimitre Novatchev; 30.04.2012
comment
Хорошо, я столкнулся с одной проблемой. Некоторые элементы являются необязательными (например, элемент License), и мне нужен способ, чтобы он игнорировал это и устанавливал соответствующую переменную как Null. Кроме того, мне нужно, чтобы он не записывал элементы для любых нулевых переменных. - person Biosci3c; 30.04.2012
comment
@ Biosci3c: если у элемента нет дочернего текстового узла, значение соответствующего свойства будет пустой строкой или нулевым значением (необходимо проверить). Вам все равно, если сериализация создает пустой элемент - из-за этого. - person Dimitre Novatchev; 30.04.2012
comment
давайте продолжим обсуждение в чате - person Biosci3c; 30.04.2012
comment
Решено, была моя ошибка. Была ли ошибка файла не найдена, а не ошибка отсутствующего элемента. Работает отлично. - person Biosci3c; 30.04.2012

Это может показаться вам странным, но если вы собираетесь использовать огромное количество данных, то лично я бы выбрал решение с автоматически сгенерированным кодом.

Значение: сопоставьте все ваши XML-имена -> с их выражениями XPath -> для отображения имен и получите что-то, что автоматически сгенерирует для вас класс данных.

Например:

Допустим, входной файл находится в формате CSV (или с разделителями табуляции и т. д.):

DisplayName,XMLField,XPathExpr
Name,name,/ship/@name
Base_type,base_type,/ship/base_type
GFX,GFX,/ship/GFX

Теперь вам понадобится некоторый процесс, который будет автоматически генерировать из него код. Для этого вы можете разработать служебную программу C#, использовать какой-либо язык сценариев, например Perl, или даже создать перевод XSL.

Конечный результат будет выглядеть примерно так:

class AutoGeneratedShipData
{
    public AutoGeneratedShipData(XmlDocument xmlDoc)
    {
        // Code initialization like in your sample
    }

    public string Name ...
    public string Base_type ...
    public string GFX ...
}

Вы можете продолжить и добавить сериализацию, поддержку INotifyPropertyChanged и другие украшения, которые сочтете нужными.

Другой подход

Было бы использовать отражение для загрузки данных в свойства, но для этого вам нужно будет вручную создать свойство для каждого члена данных, а также сопоставление данных.

Вот пример:

LoadData(xmlDoc, "Name", "/ship/@name");
LoadData(xmlDoc, "Base_type", "/ship/base_type");
LoadData(xmlDoc, "GFX", "/ship/GFX");

Где LoadData() будет что-то вроде:

private void LoadData(XmlDocument xmlDoc, Dictionary<string, string> propertyNameToXPathMap)
{
    foreach ( PropertyInfo pi in this.GetType().GetProperties() )
    {
        // Is the property mapped to an xpath?
        if ( propertyNameToXPathMap.ContainsKey(pi.Name) )
        {
            string sPathExpression = propertyNameToXPathMap[pi.Name];

            // Extract the Property's value from XML based on the xpath expr.
            string value = xmlDoc.SelectSingleNode(sPathExpression).Value;

            // Set this object's property's value
            pi.SetValue(this, value, null);
        }
    }
}
  • Обратите внимание, что я игнорирую ваш словарь paths, потому что не вижу для него особой роли.
person AVIDeveloper    schedule 04.03.2012
comment
Интересная идея. Хм, так вы предлагаете мне написать программу для автоматического написания кода, чтобы я мог изменить его, когда это необходимо. Итак, вы говорите, что нет способа сохранить список ссылок на переменные и xpaths и получить его так, как я хочу? - person Biosci3c; 05.03.2012
comment
Да, то, что когда-либо чувствует себя комфортно. Я добавил больше кода на случай, если вы захотите использовать отражение в самом классе Ship. Однако лично я бы по-прежнему инкапсулировал состояние Ship в классе данных, который легко сериализовать, передать через коммуникационный уровень и т. д. - person AVIDeveloper; 05.03.2012
comment
Вау.. ваш комментарий полностью изменился.. Я не говорил, что это невозможно. Я также добавил пример того, как это сделать с отражением. Если у вас есть закрытый XML для каждого класса, вы также можете создать для него схему XSD и автоматически сгенерировать класс из этой схемы, например, с помощью Xsd2Code. - person AVIDeveloper; 05.03.2012
comment
Я бы хотел пойти по пути схемы, но у меня нет VS 2010, только экспресс-версия. Любые предложения о том, как легко сделать схему? Кроме того, я хочу добавить, что мне нужно иметь возможность изменять дерево и сохранять его позже. Я обновлю вопрос. - person Biosci3c; 05.03.2012
comment
Вы можете использовать Xsd2Code из командной строки на этапе предварительной сборки. Добавьте флаги для создания сериализации Xml, и вы получите автоматически сгенерированные методы Serialize()/Deserialize() (как из/в строку, так и из/в файл). - person AVIDeveloper; 05.03.2012
comment
И есть инструменты, которые реконструируют ваши XML-данные и создают из них XSD-схему. Например. Среда IntelliJ Idea (как только вы ее получите :) - person AVIDeveloper; 05.03.2012

Один из способов — определить собственный тип пользовательского атрибута. для сопоставления свойства с селектором XPath определите автоматические свойства для ваших переменных, которые необходимо сопоставить с селектором XPath, и украсьте их с помощью вашего пользовательского атрибута. Например:

[MapTo("/ship/@name")]
public string Name { get; set; }

[MapTo("/ship/base_type")]
public string BaseType { get; set; }

Затем, после загрузки XML-документа, напишите цикл, который использует отражение для повторения каждого из этих свойств и устанавливает их значение на основе связанного с ними селектора XPath. Например, предположим, что пользовательский атрибут объявлен следующим образом:

[AttributeUsage(AttributeTargets.Property)]
public class MapToAttribute : Attribute
{
    public string XPathSelector { get; private set; }

    public MapToAttribute(string xPathSelector)
    {
        XPathSelector = xPathSelector;
    }
}

Тогда ваш цикл, который выполняет сопоставление, предполагая, что он находится где-то внутри метода экземпляра в классе, содержащем ваши сопоставленные свойства (если нет, замените this на целевой объект), будет выглядеть так:

foreach (var property in this.GetType().GetProperties())
{
    var mapToAttribute = property.GetCustomAttributes(typeof(MapToAttribute), false).SingleOrDefault() as MapToAttribute;
    if (mapToAttribute != null)
    {
        property.SetValue(this, doc.SelectSingleNode(mapToAttribute.XPathSelector).Value);
    }
}
person Clafou    schedule 04.03.2012