【问题标题】:Reduce Code Duplication when Searching for values in XML with XPath使用 XPath 在 XML 中搜索值时减少代码重复
【发布时间】:2012-03-04 20:53:12
【问题描述】:

我正在编写一些代码来解析以下格式的 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. 

会有更多的变量,所以这个部分会很庞大。

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

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

有什么想法吗?

提前致谢。

编辑:我还希望能够修改这些数据并将其保存回来。如有必要,我不反对保存一棵全新的树,但如果可能的话,最好在适当的位置修改数据。我不需要修改的解决方案,但我只需要打开这个选项。

【问题讨论】:

  • 最后一部分 - 保存更改 - 使用 XmlSerializer 类的另一个主要方法 - Serialize() 可以方便地完成。

标签: c# xml xpath dictionary code-duplication


【解决方案1】:

实现对象填充的一个好方法是使用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 文件的上下文中是如何工作的吗?
  • @Biosci3c:是的,我用完整的解决方案更新了我的答案。
  • @ssg,是的,我相信是的。这只需要最少的编码,非常易于使用且非常强大。
  • 很抱歉回复晚了,但我真的认为我会走这条路,因为这似乎比疯狂地解析要容易得多。但有一件事,括号中的 [XmlRoot("ship")] 语法让我感到困惑,因为我以前从未见过它。你能解释一下吗(即它是一个模板、一个类等)吗?谢谢。
  • @Biosci3c:不客气。是的,请告诉我这在实践中是如何工作的。
【解决方案2】:

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

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

例如:

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

显示名称、XMLField、XPathExpr
姓名、姓名、/船/@姓名
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 字典,因为我看不到它有任何特殊作用。

【讨论】:

  • 有趣的想法。嗯,所以你建议我编写一个程序来自动编写代码,以便我可以在必要时更改它。所以你是说没有办法存储对变量和 xpaths 的引用列表并让它按我想要的方式检索它?
  • 是的,什么都感觉舒服。如果您想对 Ship 类本身使用反射,我添加了更多代码。但是,就我个人而言,我仍然会将Ship 的状态封装在一个数据类中,这样很容易序列化、通过通信层等传递。
  • 哇..你的评论完全改变了..我没有说这是不可能的。我还添加了一个关于如何使用反射来完成的示例。如果每个类都有一个封闭的 XML,那么您还可以为它生成一个 XSD 架构,并使用 Xsd2Code 从该架构自动生成一个类。
  • 我很想走模式路线,但我没有 VS 2010,只有快速版。关于如何轻松制作架构的任何建议?另外,我想补充一点,我需要能够修改树并稍后将其保存回来。我会更新问题。
  • 您可以在预构建步骤中从命令行使用 Xsd2Code。添加用于创建 Xml 序列化的标志,您将拥有自动生成的 Serialize() / Deserialize() 方法(从/到字符串和从/到文件)。
【解决方案3】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多