【问题标题】:XPath to match Elements and AttributesXPath 匹配元素和属性
【发布时间】:2017-03-14 14:44:25
【问题描述】:

匹配属性和元素的正确 XPath 语法是什么?

更多信息

我创建了以下函数来查找包含给定值的元素和属性:

function Get-XPathToValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [xml]$Xml
        ,
        [Parameter(Mandatory)]
        [string]$Value
    )
    process {
        $Xml.SelectNodes("//*[.='{0}']" -f ($Value -replace "'","''")) | %{
            $xpath = ''
            $elem = $_
            while (($elem -ne $null) -and ($elem.NodeType -ne 'Document')) {
                $xpath = '/' + $elem.Name + $xpath 
                $elem = $elem.SelectSingleNode('..')
            }
            $xpath
        }
    }
}

这匹配元素,但不匹配属性。

通过将$Xml.SelectNodes("//*[.='{0}']" 替换为$Xml.SelectNodes("//@*[.='{0}']",我可以匹配属性,但不能匹配元素。

示例

[xml]$sampleXml = @"
<root>
    <child1>
        <child2 attribute1='hello'>
            <ignoreMe>what</ignoreMe>
            <child3>hello</child3>
            <ignoreMe2>world</ignoreMe2>
        </child2>
        <child2Part2 attribute2="ignored">hello</child2Part2>
    </child1>
    <notMe>
        <norMe>Not here</norMe>
    </notMe>
</root>
"@

Get-XPathToValue -Xml $sampleXml -Value 'hello'

返回:

/root/child1/child2/child3
/root/child1/child2Part2

应该返回:

/root/child1/child2/attribute1
/root/child1/child2/child3
/root/child1/child2Part2

你尝试过什么?

我尝试匹配:

  • //@*|*[.='{0}'] - 返回匹配元素,但返回所有属性。
  • //*|@*[.='{0}'] - 返回匹配的属性,但返回所有元素。
  • //*[.='{0}']|@*[.='{0}']" - 返回匹配的元素。
  • //@*[.='{0}']|*[.='{0}']" - 返回匹配的属性。
  • //(@*|*)[.='{0}']" - 抛出异常。

【问题讨论】:

  • 您生成 XPath 字符串的算法有缺陷。只要在同一级别中存在多个同名元素,它就会崩溃。无论如何,如果我可以问的话,它有什么意义?`
  • 好点;我会细化的。这只是一个帮助我进行分析的实用程序。我有时必须处理大型 XML 文件,我必须在文本编辑器中打开它们,找到被报告为错误的值,然后计算出相关元素的 xpath,以便我可以编写一个脚本来比较这个值在许多其他文件中使用相同的值,看看它们是否也有问题。如果我能帮上忙,我不喜欢手动做事;所以这只是节省了一点努力。它不是任何面向用户的解决方案的代码;只是我的实用腰带的东西。
  • 那里还有一个缺陷。 XPath 没有字符串转义。你不能用'' 替换',一切都很好,这不是它的工作原理。
  • 第三个缺陷是它完全忽略了 XML 命名空间。

标签: xml powershell xpath attributes element


【解决方案1】:

如您的问题的 cmets 所示,您派生 XPath 表达式的方法存在三个缺陷。

  1. 不处理同层有多个同名元素的情况。
  2. 它不能正确处理值中的引号。
  3. 它不处理 XML 命名空间。

这是我对解决这些问题的函数的看法(我还给它起了一个我认为在 cmdlet 命名方案中更合适的名称):

function Convert-ValueToXpath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [xml]$Xml
        ,
        [Parameter(Mandatory)]
        [string]$Value
    )
    process {
        $escapedValue = "concat('', '" + ($value -split "'" -join "', ""'"", '") + "')"
        $Xml.SelectNodes("(//*|//@*)[normalize-space() = {0}]" -f $escapedValue) | % {
            $xpath = ''
            $elem = $_
            while ($true) {
                if ($elem.NodeType -eq "Attribute") {
                    $xpath = '/@' + $elem.Name
                    $elem = $elem.OwnerElement
                } elseif ($elem.ParentNode) {
                    $precedingExpr = "./preceding-sibling::*[local-name() = '$($elem.LocalName)' and namespace-uri() = '$($elem.NamespaceURI)']"
                    $pos = $elem.SelectNodes($precedingExpr).Count + 1
                    $xpath = '/' + $elem.Name + "[" + $pos + "]" + $xpath
                    $elem = $elem.ParentNode
                } else {
                    break;
                }
            }
            $xpath
        }
    }
}

对于您的示例输入,我得到了这些 XPath:

/root[1]/child1[1]/child2[1]/@attribute1 /root[1]/child1[1]/child2[1]/child3[1] /root[1]/child1[1]/child2Part2[1]

【讨论】:

  • 很好,谢谢@Tomalak。注意:我认为上面仍然没有涵盖命名空间?不过可以使用/*[local-name()='$($elem.Name)'] 轻松解决这个问题。
  • 它涵盖了命名空间,但[local-name() = '$($elem.LocalName)' and namespace-uri() = '$($elem.NamespaceURI)']$precedingExpr 中会更好。事实上,让我们现在就包含它。
  • 问题是,它仍然没有生成 XPath 来解决如何在 XML 中声明命名空间的所有可能方式。可以这样做,但是生成的 XPath 变得非常臃肿。
【解决方案2】:

使用以下 XPath 解决了该问题://@*[.='{0}']|//*[.='{0}']

function Get-XPathToValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [xml]$Xml
        ,
        [Parameter(Mandatory)]
        [string]$Value
    )
    process {
        $Xml.SelectNodes("//@*[.='{0}']|//*[./text()='{0}']" -f ($Value -replace "'","''")) | %{
            $xpath = ''
            $elem = $_
            while (($elem -ne $null) -and ($elem.NodeType -ne 'Document')) {
                $prefix = ''
                if($elem.NodeType -eq 'Attribute'){$prefix = '@'}
                $xpath = '/' + $prefix + $elem.Name + $xpath 
                $elem = $elem.SelectSingleNode('..')
            }
            $xpath
        }
    }
}

【讨论】:

  • 小心。当为&lt;a&gt;&lt;b&gt;value-b&lt;/b&gt;&lt;/a&gt; 测试"value-b" 时,XPath 将选择a b。请看Testing text() nodes vs string values in XPath
  • 谢谢@kjhughes;更正。在最初考虑属性没有文本节点时(不考虑其他含义),我没有这样做;但是现在他们有不同的条件,那就更好了。
猜你喜欢
  • 2013-04-21
  • 1970-01-01
  • 1970-01-01
  • 2012-05-13
  • 1970-01-01
  • 2015-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多