使用XPath在XML中搜索值时减少代码重复

本文关键字:代码 XPath XML 搜索 使用 | 更新日期: 2023-09-27 18:26:55

我正在编写一些代码来解析以下格式的xml文件(为简化起见,已截断)

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

我使用了一个由参数和和xpath-to-value的键对组成的字典,并通过查询将所有值加载到各种变量中。然而,我注意到将会有大量的代码重复。事实上,对于我想要检索的每一个值,我都必须编写另一行看起来几乎相同的代码。这是我的代码:

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. 

将会有更多的变量,所以这一部分将是巨大的。

有什么可以简化的吗?如果这是C++,我可能会尝试指针,但我知道它们不建议在C#中使用,所以有类似的方法吗?

我会寻找一些东西,我可以给出一个变量名和xpath的列表,让代码提取所有值,并在某种循环或其他方式中加载到变量中。我想使用XPaths,因为我预计这个文件的格式可能会定期更改。

有什么想法吗?

提前谢谢。

编辑:我也希望能够修改这些数据并将其保存回来。如果必要的话,我并不反对保存一整棵新树,但如果可能的话,修改数据会很好。我不需要修改的解决方案,但我只需要打开这个选项。

使用XPath在XML中搜索值时减少代码重复

实现对象填充的一个好方法是使用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;
    }
}

更新:OP增加了一个额外的问题:

我还希望能够修改这些数据并将其保存回。我如果必要的话,我不反对保存一整棵新树

只需使用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();

这对你来说可能听起来很奇怪,但如果你想要大量的数据,那么就我个人而言,我会选择自动生成的代码解决方案。

含义:将所有的XML名称->映射到它们的XPath表达式->以显示名称,并具有自动为您生成数据类的功能。

例如:

假设输入文件为CSV格式(或制表符分隔等):

DisplayName、XMLField、XPathExpr
姓名/船名/@Name
Base_type、Base_type、/ship/Base_type
GFX、GFX、/船/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字典,因为我看不到它的任何特殊角色

一种方法是定义自己的自定义属性类型以将属性映射到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);
    }
}