使用带有来自变量的节点的 PowerShell 解析 XML 文件

Parsing an XML file with PowerShell with node from variable

尊敬的 Powershell 用户,您好,

我正在尝试解析 xml 文件,这些文件的结构可能不同。因此,我想根据从变量接收到的节点结构访问节点值。

例子

#XML file
$xml = [xml] @'
<node1>
    <node2>
        <node3>
            <node4>test1</node4>
        </node3>
    </node2>
</node1>
'@

直接访问值是可行的。

#access XML node directly -works-
$xml.node1.node2.node3.node4        # working <OK>

通过变量的节点信息访问值不起作用。

#access XML node via path from variable -does not work-
$testnodepath = 'node1.node2.node3.node4'

$xml.$testnodepath                  # NOT working
$xml.$($testnodepath)               # NOT working

有没有办法通过从变量接收节点信息来直接访问 XML 节点值?

PS:我知道,有一种通过 Selectnode 的方法,但我认为这是低效的,因为它基本上是在搜索关键字。

#Working - but inefficient
$testnodepath = 'node1/node2/node3/node4'
$xml.SelectNodes($testnodepath)

我需要一种非常有效的方法来解析 XML 文件,因为我需要解析巨大的 XML 文件。有没有办法通过从变量接收节点结构来直接访问 $xml.node1.node2.node3.node4 形式的节点值?

您可以将包含 属性 路径的字符串拆分为单独的名称,然后将它们一一解引用:

# define path
$testnodepath = 'node1.node2.node3.node4'

# create a new variable, this will be our intermediary for keeping track of each node/level we've resolved so far
$target = $xml

# now we just loop through each node name in the path
foreach($nodeName in $testnodepath.Split('.')){
  # keep advancing down through the path, 1 node name at a time
  $target = $target.$nodeName
}

# this now resolves to the same value as `$xml.node1.node2.node3.node4`
$target

您可以为此使用 ExecutionContext ExpandString

$ExecutionContext.InvokeCommand.ExpandString("`$(`$xml.$testnodepath)")
test1

如果节点路径 ($testnodepath) 来自外部(例如参数),您可能希望 prevent any malicious code injections 通过分割任何不是单词字符或点 (.):

$securenodepath = $testnodepath -Replace '[^\w\.]'
$ExecutionContext.InvokeCommand.ExpandString("`$(`$xml.$securenodepath)")

I will need to parse huge XML files

下面介绍了一种memory-friendly streaming方法,不需要将整个XML文档(DOM)加载到记忆。所以你可以解析非常大的 XML 文件,即使它们不适合内存。它还应该提高解析速度,因为我们可以简单地跳过我们不感兴趣的元素。为此,我们使用System.Xml.XmlReader来处理XML个元素on-the-fly,同时从文件中读取。

我已将代码包装在 可重用函数中:

Function Import-XmlElementText( [String] $FilePath, [String[]] $ElementPath ) {

    $stream = $reader = $null

    try {
        $stream = [IO.File]::OpenRead(( Convert-Path -LiteralPath $FilePath )) 
        $reader = [System.Xml.XmlReader]::Create( $stream )

        $curElemPath = ''  # The current location in the XML document

        # While XML nodes are read from the file
        while( $reader.Read() ) {
            switch( $reader.NodeType ) {
                ([System.Xml.XmlNodeType]::Element) {
                    if( -not $reader.IsEmptyElement ) {
                        # Start of a non-empty element -> add to current path
                        $curElemPath += '/' + $reader.Name
                    }
                }
                ([System.Xml.XmlNodeType]::Text) {
                    # Element text -> collect if path matches
                    if( $curElemPath -in $ElementPath ) {
                        [PSCustomObject]@{
                            Path  = $curElemPath
                            Value = $reader.Value
                        }
                    }
                }
                ([System.Xml.XmlNodeType]::EndElement) {
                    # End of element - remove current element from the path
                    $curElemPath = $curElemPath.Substring( 0, $curElemPath.LastIndexOf('/') ) 
                }
            }
        }
    }
    finally {
        if( $reader ) { $reader.Close() }
        if( $stream ) { $stream.Close() }
    }
}

这样称呼它:

Import-XmlElementText -FilePath test.xml -ElementPath '/node1/node2a/node3a', '/node1/node2b'

鉴于此 输入 XML:

<node1>
    <node2a>
        <node3a>test1</node3a>
        <node3b/>
        <node3c a='b'/>
        <node3d></node3d>
    </node2a>
    <node2b>test2</node2b>
</node1>

这个输出产生:

Path                 Value
----                 -----
/node1/node2a/node3a test1
/node1/node2b        test2

实际上该函数输出的对象可以像往常一样由管道命令处理或存储在数组中:

$foundElems = Import-XmlElementText -FilePath test.xml -ElementPath '/node1/node2a/node3a', '/node1/node2b'

$foundElems[1].Value  # Prints 'test2'

备注:

  • Convert-Path 用于将可能是相对的 PowerShell 路径(又名 PSPath)转换为可由 .NET 函数使用的绝对路径。这是必需的,因为 .NET 使用与 PowerShell 不同的当前目录,并且 PowerShell 路径可以采用 .NET 甚至无法理解的形式(例如 Microsoft.PowerShell.Core\FileSystem::C:\something.txt)。
  • 当遇到一个元素的开始时,我们必须跳过空元素,例如<node/>,因为对于这样的元素,我们不会进入EndElement case分支,这将呈现当前路径($curElemPath) 无效(该元素不会再次从当前路径中删除)。