C# 基于 XPath 创建 XML 节点?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/508390/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 06:10:47  来源:igfitidea点击:

Create XML Nodes based on XPath?

c#xmlxpath

提问by Fred Strauss

Does anyone know of an existing means of creating an XML hierarchy programatically from an XPath expression?

有谁知道从 XPath 表达式以编程方式创建 XML 层次结构的现有方法?

For example if I have an XML fragment such as:

例如,如果我有一个 XML 片段,例如:

<feed>
    <entry>
        <data></data>
        <content></content>
    </entry>
</feed>

Given the XPath expression /feed/entry/content/@source I would have:

鉴于 XPath 表达式 /feed/entry/content/@source 我会有:

<feed>
    <entry>
        <data></data>
        <content @source=""></content>
    </entry>
</feed>

I realize this is possible using XSLT but due to the dynamic nature of what I'm trying to accomplish a fixed transformation won't work.

我意识到使用 XSLT 可以做到这一点,但由于我试图完成固定转换的动态特性,这将不起作用。

I am working in C# but if someone has a solution using some other language please chime in.

我在 C# 中工作,但如果有人有使用其他语言的解决方案,请加入。

Thanks for the help!

谢谢您的帮助!

回答by xcud

In the example you present the only thing being created is the attribute ...

在您展示的示例中,唯一创建的是属性...

XmlElement element = (XmlElement)doc.SelectSingleNode("/feed/entry/content");
if (element != null)
    element.SetAttribute("source", "");

If what you really want is to be able to create the hierarchy where it doesn't exist then you could your own simple xpath parser. I don't know about keeping the attribute in the xpath though. I'd rather cast the node as an element and tack on a .SetAttribute as I've done here:

如果您真正想要的是能够在它不存在的地方创建层次结构,那么您可以使用自己的简单 xpath 解析器。我不知道如何将属性保留在 xpath 中。我宁愿将节点转换为元素并添加 .SetAttribute ,就像我在这里所做的那样:


static private XmlNode makeXPath(XmlDocument doc, string xpath)
{
    return makeXPath(doc, doc as XmlNode, xpath);
}

static private XmlNode makeXPath(XmlDocument doc, XmlNode parent, string xpath)
{
    // grab the next node name in the xpath; or return parent if empty
    string[] partsOfXPath = xpath.Trim('/').Split('/');
    string nextNodeInXPath = partsOfXPath.First();
    if (string.IsNullOrEmpty(nextNodeInXPath))
        return parent;

    // get or create the node from the name
    XmlNode node = parent.SelectSingleNode(nextNodeInXPath);
    if (node == null)
        node = parent.AppendChild(doc.CreateElement(nextNodeInXPath));

    // rejoin the remainder of the array as an xpath expression and recurse
    string rest = String.Join("/", partsOfXPath.Skip(1).ToArray());
    return makeXPath(doc, node, rest);
}

static void Main(string[] args)
{
    XmlDocument doc = new XmlDocument();
    doc.LoadXml("<feed />");

    makeXPath(doc, "/feed/entry/data");
    XmlElement contentElement = (XmlElement)makeXPath(doc, "/feed/entry/content");
    contentElement.SetAttribute("source", "");

    Console.WriteLine(doc.OuterXml);
}

回答by ilen

If the XPath string is processed from back to front, its easier to process non rooted XPaths eg. //a/b/c... It should support Gordon's XPath syntax too although I have not tried...

如果 XPath 字符串是从后到前处理的,则更容易处理非根 XPath,例如。//a/b/c... 它也应该支持 Gordon 的 XPath 语法,虽然我还没有尝试过...

static private XmlNode makeXPath(XmlDocument doc, string xpath)
{
    string[] partsOfXPath = xpath.Split('/');
    XmlNode node = null;
    for (int xpathPos = partsOfXPath.Length; xpathPos > 0; xpathPos--)
    {
        string subXpath = string.Join("/", partsOfXPath, 0, xpathPos);
        node = doc.SelectSingleNode(subXpath);
        if (node != null)
        {
            // append new descendants
            for (int newXpathPos = xpathPos; newXpathPos < partsOfXPath.Length; newXpathPos++)
            {
                node = node.AppendChild(doc.CreateElement(partsOfXPath[newXpathPos]));
            }
            break;
        }
    }

    return node;
}

回答by Eranga Dissanayaka

Here is my version. Hope this also would help someone.

这是我的版本。希望这也会对某人有所帮助。

    public static void Main(string[] args)
    {

        XmlDocument doc = new XmlDocument();
        XmlNode rootNode = GenerateXPathXmlElements(doc, "/RootNode/FirstChild/SecondChild/ThirdChild");

        Console.Write(rootNode.OuterXml);

    }

    private static XmlDocument GenerateXPathXmlElements(XmlDocument xmlDocument, string xpath)
    {
        XmlNode parentNode = xmlDocument;

        if (xmlDocument != null && !string.IsNullOrEmpty(xpath))
        {
            string[] partsOfXPath = xpath.Split('/');


            string xPathSoFar = string.Empty;

            foreach (string xPathElement in partsOfXPath)
            {
                if(string.IsNullOrEmpty(xPathElement))
                    continue;

                xPathSoFar += "/" + xPathElement.Trim();

                XmlNode childNode = xmlDocument.SelectSingleNode(xPathSoFar);
                if(childNode == null)
                {
                    childNode = xmlDocument.CreateElement(xPathElement);
                }

                parentNode.AppendChild(childNode);

                parentNode = childNode;
            }
        }

        return xmlDocument;
    }

回答by laktak

Here's my quick hack that can also create attributes as long as you use a format like /configuration/appSettings/add[@key='name']/@value.

这是我的快速技巧,只要您使用/configuration/appSettings/add[@key='name']/@value.

static XmlNode createXPath(XmlDocument doc, string xpath)
{
  XmlNode node=doc;
  foreach (string part in xpath.Substring(1).Split('/'))
  {
    XmlNodeList nodes=node.SelectNodes(part);
    if (nodes.Count>1) throw new ComponentException("Xpath '"+xpath+"' was not found multiple times!");
    else if (nodes.Count==1) { node=nodes[0]; continue; }

    if (part.StartsWith("@"))
    {
      var anode=doc.CreateAttribute(part.Substring(1));
      node.Attributes.Append(anode);
      node=anode;
    }
    else
    {
      string elName, attrib=null;
      if (part.Contains("["))
      {
        part.SplitOnce("[", out elName, out attrib);
        if (!attrib.EndsWith("]")) throw new ComponentException("Unsupported XPath (missing ]): "+part);
        attrib=attrib.Substring(0, attrib.Length-1);
      }
      else elName=part;

      XmlNode next=doc.CreateElement(elName);
      node.AppendChild(next);
      node=next;

      if (attrib!=null)
      {
        if (!attrib.StartsWith("@")) throw new ComponentException("Unsupported XPath attrib (missing @): "+part);
        string name, value;
        attrib.Substring(1).SplitOnce("='", out name, out value);
        if (string.IsNullOrEmpty(value) || !value.EndsWith("'")) throw new ComponentException("Unsupported XPath attrib: "+part);
        value=value.Substring(0, value.Length-1);
        var anode=doc.CreateAttribute(name);
        anode.Value=value;
        node.Attributes.Append(anode);
      }
    }
  }
  return node;
}

SplitOnce is an extension method:

SplitOnce 是一个扩展方法:

public static void SplitOnce(this string value, string separator, out string part1, out string part2)
{
  if (value!=null)
  {
    int idx=value.IndexOf(separator);
    if (idx>=0)
    {
      part1=value.Substring(0, idx);
      part2=value.Substring(idx+separator.Length);
    }
    else
    {
      part1=value;
      part2=null;
    }
  }
  else
  {
    part1="";
    part2=null;
  }
}

Sample:

样本:

public static void Set(XmlDocument doc, string xpath, string value)
{
  if (doc==null) throw new ArgumentNullException("doc");
  if (string.IsNullOrEmpty(xpath)) throw new ArgumentNullException("xpath");

  XmlNodeList nodes=doc.SelectNodes(xpath);
  if (nodes.Count>1) throw new ComponentException("Xpath '"+xpath+"' was not found multiple times!");
  else if (nodes.Count==0) createXPath(doc, xpath).InnerText=value;
  else nodes[0].InnerText=value;
}

e.g.

例如

Set(doc, "/configuration/appSettings/add[@key='Server']/@value", "foobar");

回答by kenyee

I liked Chris' version because it handled attributes in xpaths and the other solutions didn't (though it doesn't handle "text()" in the path that well which I fixed). I unfortunately had to use this in a VB app, so here's the conversion for that:

我喜欢 Chris 的版​​本,因为它处理了 xpaths 中的属性,而其他解决方案则没有(尽管它在我修复的路径中没有处理“text()”)。不幸的是,我不得不在 VB 应用程序中使用它,所以这是它的转换:

        Private Sub SplitOnce(ByVal value As String, ByVal separator As String, ByRef part1 As String, ByRef part2 As String)
        If (value IsNot Nothing) Then
            Dim idx As Integer = value.IndexOf(separator)
            If (idx >= 0) Then
                part1 = value.Substring(0, idx)
                part2 = value.Substring(idx + separator.Length)
            Else
                part1 = value
                part2 = Nothing
            End If
        Else
            part1 = ""
            part2 = Nothing
        End If
    End Sub
    Private Function createXPath(ByVal doc As XmlDocument, ByVal xpath As String) As XmlNode
        Dim node As XmlNode = doc
        Dim part As String
        For Each part In xpath.Substring(1).Split("/")
            Dim nodes As XmlNodeList = node.SelectNodes(part)
            If (nodes.Count > 1) Then
                Throw New Exception("Xpath '" + xpath + "' was not found multiple times!")
            ElseIf (nodes.Count = 1) Then
                node = nodes(0)
                Continue For
            End If

            If (part.EndsWith("text()")) Then
                ' treat this the same as previous node since this is really innertext
                Exit For
            ElseIf (part.StartsWith("@")) Then
                Dim anode As XmlAttribute = doc.CreateAttribute(part.Substring(1))
                node.Attributes.Append(anode)
                node = anode
            Else
                Dim elName As String = Nothing
                Dim attrib As String = Nothing
                If (part.Contains("[")) Then
                    SplitOnce(part, "[", elName, attrib)
                    If (Not attrib.EndsWith("]")) Then
                        Throw New Exception("Unsupported XPath (missing ]): " + part)
                    End If
                    attrib = attrib.Substring(0, attrib.Length - 1)
                Else
                    elName = part
                End If
                Dim nextnode As XmlNode = doc.CreateElement(elName)
                node.AppendChild(nextnode)
                node = nextnode
                If (attrib IsNot Nothing) Then
                    If (Not attrib.StartsWith("@")) Then
                        Throw New Exception("Unsupported XPath attrib (missing @): " + part)
                    End If
                    Dim name As String = ""
                    Dim value As String = ""
                    SplitOnce(attrib.Substring(1), "='", name, value)
                    If (String.IsNullOrEmpty(value) Or Not value.EndsWith("'")) Then
                        Throw New Exception("Unsupported XPath attrib: " + part)
                    End If
                    value = value.Substring(0, value.Length - 1)
                    Dim anode As XmlAttribute = doc.CreateAttribute(name)
                    anode.Value = value
                    node.Attributes.Append(anode)
                End If
            End If
        Next
        Return node
    End Function

回答by Mark Miller

I know this is a really old thread ... but I have just been trying the same thing and came up with the following regex which is not perfect but I find more generic

我知道这是一个非常古老的线程......但我一直在尝试同样的事情并想出了以下不完美的正则表达式,但我发现更通用

/+([\w]+)(\[@([\w]+)='([^']*)'\])?|/@([\w]+)

The string /configuration/appSettings/add[@key='name']/@value

字符串 /configuration/appSettings/add[@key='name']/@value

should be parsed to

应该被解析为

Found 14 match(es):

找到 14 个匹配项:

start=0, end=14 Group(0) = /configuration Group(1) = configuration Group(2) = null Group(3) = null Group(4) = null Group(5) = null

start=0, end=14 Group(0) = /configuration Group(1) = configuration Group(2) = null Group(3) = null Group(4) = null Group(5) = null

start=14, end=26 Group(0) = /appSettings Group(1) = appSettings Group(2) = null Group(3) = null Group(4) = null Group(5) = null

start=14, end=26 Group(0) = /appSettings Group(1) = appSettings Group(2) = null Group(3) = null Group(4) = null Group(5) = null

start=26, end=43 Group(0) = /add[@key='name'] Group(1) = add Group(2) = [@key='name'] Group(3) = key Group(4) = name Group(5) = null

start=26, end=43 Group(0) = /add[@key='name'] Group(1) = add Group(2) = [@key='name'] Group(3) = key Group(4) ) = 名称组(5) = 空

start=43, end=50 Group(0) = /@value Group(1) = null Group(2) = null Group(3) = null Group(4) = null Group(5) = value

start=43, end=50 Group(0) = /@value Group(1) = null Group(2) = null Group(3) = null Group(4) = null Group(5) = value



Which means we have

这意味着我们有

Group(0) = Ignored Group(1) = The element name Group(2) = Ignored Group(3) = Filter attribute name Group(4) = Filter attribute value

Group(0) = 忽略 Group(1) = 元素名称 Group(2) = 忽略 Group(3) = 过滤器属性名称 Group(4) = 过滤器属性值

Here is a java method which can use the pattern

这是一个可以使用模式的java方法

public static Node createNodeFromXPath(Document doc, String expression) throws XPathExpressionException {
StringBuilder currentPath = new StringBuilder();
Matcher matcher = xpathParserPattern.matcher(expression);

Node currentNode = doc.getFirstChild();

while (matcher.find()) {
    String currentXPath = matcher.group(0);
    String elementName = matcher.group(1);
    String filterName = matcher.group(3);
    String filterValue = matcher.group(4);
    String attributeName = matcher.group(5);

    StringBuilder builder = currentPath.append(currentXPath);
    String relativePath = builder.toString();
    Node newNode = selectSingleNode(doc, relativePath);

    if (newNode == null) {
        if (attributeName != null) {
            ((Element) currentNode).setAttribute(attributeName, "");
            newNode = selectSingleNode(doc, relativePath);

        } else if (elementName != null) {
            Element element = doc.createElement(elementName);
            if (filterName != null) {
                element.setAttribute(filterName, filterValue);
            }
            currentNode.appendChild(element);
            newNode = element;

        } else {
            throw new UnsupportedOperationException("The given xPath is not supported " + relativePath);
        }
    }

    currentNode = newNode;
}

if (selectSingleNode(doc, expression) == null) {
    throw new IllegalArgumentException("The given xPath cannot be created " + expression);
}

return currentNode;

}

}

回答by Roderick Llewellyn

One problem with this idea is that xpath "destroys" information.

这个想法的一个问题是 xpath “破坏”了信息。

There are an infinite number of xml trees that can match many xpaths. Now in some cases, like the example you give, there is an obvious minimal xml tree which matches your xpath, where you have a predicate that uses "=".

有无数个 xml 树可以匹配许多 xpath。现在在某些情况下,就像您给出的示例一样,有一个明显的最小 xml 树与您的 xpath 匹配,其中您有一个使用“=”的谓词。

But for example if the predicate uses not equal, or any other arithmetic operator other than equal, an infinite number of possibilities exist. You could try to choose a "canonical" xml tree which requires, say, the fewest bits to represent.

但是,例如,如果谓词使用不等于或除等于以外的任何其他算术运算符,则存在无限多种可能性。您可以尝试选择一个“规范的”xml 树,它需要最少的位来表示。

Suppose for example you had xpath /feed/entry/content[@source > 0]. Now any xml tree of the appropriate structure in which node content had an attribute source whose value was > 0 would match, but there are an infinite number of numbers greater than zero. By choosing the "minimal" value, presumably 1, you could attempt to canonicalize your xml.

例如,假设您有 xpath /feed/entry/content[@source > 0]。现在,任何具有适当结构的 xml 树(其中节点内容具有值 > 0 的属性源)都将匹配,但是有无数个大于零的数字。通过选择“最小”值,大概是 1,您可以尝试规范化您的 xml。

Xpath predicates can contain pretty arbitrary arithmetic expressions, so the general solution to this is quite difficult, if not impossible. You could imagine a huge equation in there, and it would have to be solved in reverse to come up with values that would match the equation; but since there can be an infinite number of matching values (as long as it's really an inequality not an equation), a canonical solution would need to be found.

Xpath 谓词可以包含非常任意的算术表达式,因此对此的一般解决方案非常困难,如果不是不可能的话。你可以想象那里有一个巨大的方程,它必须反向求解才能得出与方程匹配的值;但是由于可以有无限多个匹配值(只要它确实是不等式而不是方程),就需要找到规范的解决方案。

Many expressions of other forms also destroy information. For example, an operator like "or" always destroys information. If you know that (X or Y) == 1, you don't know if X is 1, Y is 1, or both of them is 1; all you know for sure is that one of them is 1! Therefore if you have an expression using OR, you cannot tell which of the nodes or values that are inputs to the OR should be 1 (you can make an arbitrary choice and set both 1, as that will satisfy the expression for sure, as will the two choices in which only one of them is 1).

许多其他形式的表达也会破坏信息。例如,像“或”这样的操作符总是会破坏信息。如果你知道(X or Y) == 1,你就不知道 X 是 1,Y 是 1,还是两者都是 1;您所知道的只是其中之一是 1!因此,如果您有一个使用 OR 的表达式,您将无法判断作为 OR 输入的哪些节点或值应该是 1(您可以任意选择并将两者都设置为 1,因为这肯定会满足表达式,正如其中只有一个是1)的两种选择。

Now suppose there are several expressions in the xpath which refer to the same set of values. You then end up with a system of simultaneous equations or inequalities that can be virtually impossible to solve. Again, if you restrict the allowable xpath to a small subset of its full power, you can solve this problem. I suspect the fully general case is similar to the Turing halting problem, however; in this case, given an arbitrary program (the xpath), figure out a set of consistent data that matches the program, and is in some sense minimal.

现在假设在 xpath 中有几个引用相同值集的表达式。然后,您最终会得到一个几乎无法解决的联立方程或不等式系统。同样,如果将允许的 xpath 限制为其全部功率的一小部分,则可以解决此问题。然而,我怀疑完全一般的情况类似于图灵停机问题;在这种情况下,给定一个任意程序(xpath),找出一组与该程序匹配的一致数据,并且在某种意义上是最小的。

回答by Ben

The C# version of Mark Miller's Java solution

Mark Miller 的 Java 解决方案的 C# 版本

    /// <summary>
    /// Makes the X path. Use a format like //configuration/appSettings/add[@key='name']/@value
    /// </summary>
    /// <param name="doc">The doc.</param>
    /// <param name="xpath">The xpath.</param>
    /// <returns></returns>
    public static XmlNode createNodeFromXPath(XmlDocument doc, string xpath)
    {
        // Create a new Regex object
        Regex r = new Regex(@"/+([\w]+)(\[@([\w]+)='([^']*)'\])?|/@([\w]+)");

        // Find matches
        Match m = r.Match(xpath);

        XmlNode currentNode = doc.FirstChild;
        StringBuilder currentPath = new StringBuilder();

        while (m.Success)
        {
            String currentXPath = m.Groups[0].Value;    // "/configuration" or "/appSettings" or "/add"
            String elementName = m.Groups[1].Value;     // "configuration" or "appSettings" or "add"
            String filterName = m.Groups[3].Value;      // "" or "key"
            String filterValue = m.Groups[4].Value;     // "" or "name"
            String attributeName = m.Groups[5].Value;   // "" or "value"

            StringBuilder builder = currentPath.Append(currentXPath);
            String relativePath = builder.ToString();
            XmlNode newNode = doc.SelectSingleNode(relativePath);

            if (newNode == null)
            {
                if (!string.IsNullOrEmpty(attributeName))
                {
                    ((XmlElement)currentNode).SetAttribute(attributeName, "");
                    newNode = doc.SelectSingleNode(relativePath);
                }
                else if (!string.IsNullOrEmpty(elementName))
                {
                    XmlElement element = doc.CreateElement(elementName);
                    if (!string.IsNullOrEmpty(filterName))
                    {
                        element.SetAttribute(filterName, filterValue);
                    }

                    currentNode.AppendChild(element);
                    newNode = element;
                }
                else
                {
                    throw new FormatException("The given xPath is not supported " + relativePath);
                }
            }

            currentNode = newNode;

            m = m.NextMatch();
        }

        // Assure that the node is found or created
        if (doc.SelectSingleNode(xpath) == null)
        {
            throw new FormatException("The given xPath cannot be created " + xpath);
        }

        return currentNode;
    }

回答by user1984091

Here is a enhanced RegEx based on Mark Miller's code

这是基于Mark Miller 代码的增强型 RegEx

/([\w]+)(?:(?:[\[])(@|)([\w]+)(?:([!=<>]+)(?:(?:(?:')([^']+)(?:'))|([^']+))|)(?:[]])|)|([.]+))

Group 1: Node name
Group 2: @ (or Empty, for non attributes)
Group 3: Attribute Key
Group 4: Attribute Value (if string)
Group 5: Attribute Value (if number)
Group 6: .. (dots, one or more)

回答by Christian Peeters

I needed a XNode instead of a XmlNode implementation, and the RegEx was not working for me (because element names with . or - are not functioning)

我需要一个 XNode 而不是 XmlNode 实现,而 RegEx 对我不起作用(因为带有 . 或 - 的元素名称不起作用)

So this what's what worked for me:

所以这对我有用:

public static XNode createNodeFromXPath(XElement elem, string xpath)
{
    // Create a new Regex object
    Regex r = new Regex(@"/*([a-zA-Z0-9_\.\-]+)(\[@([a-zA-Z0-9_\.\-]+)='([^']*)'\])?|/@([a-zA-Z0-9_\.\-]+)");

    xpath = xpath.Replace("\"", "'");
    // Find matches
    Match m = r.Match(xpath);

    XNode currentNode = elem;
    StringBuilder currentPath = new StringBuilder();

    while (m.Success)
    {
        String currentXPath = m.Groups[0].Value;    // "/configuration" or "/appSettings" or "/add"
        String elementName = m.Groups[1].Value;     // "configuration" or "appSettings" or "add"
        String filterName = m.Groups[3].Value;      // "" or "key"
        String filterValue = m.Groups[4].Value;     // "" or "name"
        String attributeName = m.Groups[5].Value;   // "" or "value"

        StringBuilder builder = currentPath.Append(currentXPath);
        String relativePath = builder.ToString();
        XNode newNode = (XNode)elem.XPathSelectElement(relativePath);

        if (newNode == null)
        {
            if (!string.IsNullOrEmpty(attributeName))
            {
                ((XElement)currentNode).Attribute(attributeName).Value = "";
                newNode = (XNode)elem.XPathEvaluate(relativePath);
            }
            else if (!string.IsNullOrEmpty(elementName))
            {
                XElement newElem = new XElement(elementName);
                if (!string.IsNullOrEmpty(filterName))
                {
                    newElem.Add(new XAttribute(filterName, filterValue));
                }

                ((XElement)currentNode).Add(newElem);
                newNode = newElem;
            }
            else
            {
                throw new FormatException("The given xPath is not supported " + relativePath);
            }
        }

        currentNode = newNode;
        m = m.NextMatch();
    }

    // Assure that the node is found or created
    if (elem.XPathEvaluate(xpath) == null)
    {
        throw new FormatException("The given xPath cannot be created " + xpath);
    }

    return currentNode;
}