web-dev-qa-db-ja.com

XElementへのXPathを取得しますか?

XElementをドキュメントの奥深くに持っています。 XElement(およびXDocument?)が与えられた場合、その完全な(つまり、/root/item/element/childなどの)XPathを取得する拡張メソッドはありますか?

例えば。 myXElement.GetXPath()?

編集:わかりました、私は非常に重要な何かを見落としているようです。おっと!要素のインデックスを考慮する必要があります。提案された修正済みのソリューションについては、私の最後の回答を参照してください。

42
core

拡張メソッド:

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement
    /// (e.g. "/people/person[6]/name[1]/last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();
            string name = e.Name.LocalName;

            // If the element is the root, no index is required

            return (index == -1) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name, 
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) + 
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            return -1;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}

そしてテスト:

class Program
{
    static void Main(string[] args)
    {
        Program.Process(XDocument.Load(@"C:\test.xml").Root);
        Console.Read();
    }

    static void Process(XElement element)
    {
        if (!element.HasElements)
        {
            Console.WriteLine(element.GetAbsoluteXPath());
        }
        else
        {
            foreach (XElement child in element.Elements())
            {
                Process(child);
            }
        }
    }
}

そしてサンプル出力:

/tests/test[1]/date[1]
/tests/test[1]/time[1]/start[1]
/tests/test[1]/time[1]/end[1]
/tests/test[1]/facility[1]/name[1]
/tests/test[1]/facility[1]/website[1]
/tests/test[1]/facility[1]/street[1]
/tests/test[1]/facility[1]/state[1]
/tests/test[1]/facility[1]/city[1]
/tests/test[1]/facility[1]/Zip[1]
/tests/test[1]/facility[1]/phone[1]
/tests/test[1]/info[1]
/tests/test[2]/date[1]
/tests/test[2]/time[1]/start[1]
/tests/test[2]/time[1]/end[1]
/tests/test[2]/facility[1]/name[1]
/tests/test[2]/facility[1]/website[1]
/tests/test[2]/facility[1]/street[1]
/tests/test[2]/facility[1]/state[1]
/tests/test[2]/facility[1]/city[1]
/tests/test[2]/facility[1]/Zip[1]
/tests/test[2]/facility[1]/phone[1]
/tests/test[2]/info[1]

これで解決するはずです。番号?

42
core

名前空間の接頭辞を考慮するように、Chrisのコードを更新しました。 GetAbsoluteXPathメソッドのみが変更されます。

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement, including the namespace.
    /// (e.g. "/a:people/b:person[6]/c:name[1]/d:last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();

            var currentNamespace = e.Name.Namespace;

            string name;
            if (currentNamespace == null)
            {
                name = e.Name.LocalName;
            }
            else
            {
                string namespacePrefix = e.GetPrefixOfNamespace(currentNamespace);
                name = namespacePrefix + ":" + e.Name.LocalName;
            }

            // If the element is the root, no index is required
            return (index == -1) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name,
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) +
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            return -1;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}
11

このクラスの最新の変更を共有しましょう。基本的に、要素に兄弟がなく、local-name()演算子で名前空間が含まれている場合、インデックスは除外されます。名前空間の接頭辞に問題がありました。

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement, including the namespace.
    /// (e.g. "/a:people/b:person[6]/c:name[1]/d:last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }


        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();

            var currentNamespace = e.Name.Namespace;

            string name;
            if (String.IsNullOrEmpty(currentNamespace.ToString()))
            {
                name = e.Name.LocalName;
            }
            else
            {
                name = "*[local-name()='" + e.Name.LocalName + "']";
                //string namespacePrefix = e.GetPrefixOfNamespace(currentNamespace);
                //name = namespacePrefix + ":" + e.Name.LocalName;
            }

            // If the element is the root or has no sibling elements, no index is required
            return ((index == -1) || (index == -2)) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name,
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) +
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned or -2 if element has no sibling elements.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            // Element is root
            return -1;
        }

        if (element.Parent.Elements(element.Name).Count() == 1)
        {
            // Element has no sibling elements
            return -2;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}
6
Chaveiro

これは実際には this 質問の複製です。回答としてマークされていませんが、その質問に対する my answer のメソッドは、すべての状況で常に機能するXMLドキュメント内のノードへのXPathを明確に定式化する唯一の方法です。 (要素だけでなく、すべてのノードタイプでも機能します。)

ご覧のとおり、生成されるXPathは醜く、抽象的です。しかし、それは多くの回答者がここで提起した懸念に対処します。ここで行った提案のほとんどは、元のドキュメントの検索に使用すると、ターゲットノードを含む1つ以上のノードのセットを生成するXPathを生成します。問題はその「以上」です。たとえば、DataSetのXML表現がある場合、特定のDataRowの要素への単純なXPathである_/DataSet1/DataTable1_も、DataTable内の他のすべてのDataRowの要素を返します。 XMLがどのようにフォーラム化されているかについて何かを知らなければ、それを明確にすることはできません(たとえば、主キー要素はありますか?)。

しかし、/node()[1]/node()[4]/node()[11]では、何があっても、ノードが返すノードは1つだけです。

4
Robert Rossney

別のプロジェクト の一部として、要素への単純なXPathを生成する拡張メソッドを開発しました。選択した回答に似ていますが、XElementに加えてXAttribute、XText、XCData、XCommentをサポートしています。 code nuget として利用できます。ここのプロジェクトページ: xmlspecificationcompare.codeplex.com

2
Eli Algranti

.NETによってネイティブに提供されるものを探している場合、答えは「いいえ」です。これを行うには、独自の拡張メソッドを作成する必要があります。

0
Scott Dorman

同じ要素につながる複数のxpathが存在する可能性があるため、ノードにつながる最も単純なxpathを見つけることは簡単ではありません。

つまり、ノードへのxpathを見つけるのは非常に簡単です。ルートノードを読み取ってノード名を組み合わせ、有効なxpathが得られるまで、ノードツリーをステップアップするだけです。

0
Rune Grimstad

「完全なxpath」とは、あらゆる要素に一致する可能性のあるxpathの数がveryになる可能性があるため、タグの単純なチェーンを意味すると想定します。

ここでの問題は、特定のxpathを構築するのが特に不可能ではないにしても、同じ要素まで可逆的にトレースバックすることが非常に難しいということです-それは条件ですか?

「いいえ」の場合、おそらく、現在の要素parentNodeを参照して再帰的にループすることにより、クエリを構築できます。 「はい」の場合、兄弟セット内のインデックスの位置を相互参照し、IDのような属性が存在する場合はそれを参照することで拡張することを検討します。これは、一般的なソリューションの場合、XSDに大きく依存します。可能です。

0
annakata