无法在 PowerShell 中完全解析 XML
Unable to completely parse XML in PowerShell
我有一个 XML 文件,我想对其进行解析并检索特定信息。
为了便于理解,下面是 XML 文件的屏幕截图:
我想解析 XML 并为每个 Item
节点检索屏幕截图中指示的字段。每个检索到的值都需要按项目节点进行格式化。
最后,我希望能够指定要查找的条件,并且只检索找到的条件。
我一直在努力,没有运气。这是我能够想出的:
[xml]$MyXMLFile = gc 'X:\folder\my.xml'
$XMLItem = $MyXMLFile.PatchScan.Machine.Product.Item
$Patch = $XMLItem | Where-Object {$_.Class -eq 'Patch'}
$Patch.BulletinID
$Patch.PatchName
$Patch.Status
当我运行上面的代码时,它returns没有结果。但是,仅出于测试目的,我删除了 Item 部分。现在,我可以通过修改上面的代码让它工作。
我将 XML 加载到 XML 对象中。现在我尝试将它向下遍历到产品并且它完美地工作:
PS> $xmlobj.PatchScan.Machine.Product | Select-Object -Property Name, SP
Name SP
---- --
Windows 10 Pro (x64) 1607
Internet Explorer 11 (x64) Gold
Windows Media Player 12.0 Gold
MDAC 6.3 (x64) Gold
.NET Framework 4.7 (x64) Gold
MSXML 3.0 SP11
MSXML 6.0 (x64) SP3
DirectX 9.0c Gold
Adobe Flash 23 Gold
VMware Tools x64 Gold
Microsoft Visual C++ 2008 SP1 Redistributable Gold
Microsoft Visual C++ 2008 SP1 Redistributable (x64) Gold
现在添加 Item 并且 Intellisense 会放一个括号,就好像 Item 是一个方法 $xmlobj.PatchScan.Machine.Product.Item(
← 看到了吗?所以这就是为什么我出于某种原因认为 Item
节点正在做一些奇怪的事情,这是我的障碍。
这张截图更好地展示了它是如何从许多产品文件夹开始的,然后在每个产品文件夹中又是许多项目文件夹。
我不关心产品文件夹中的XML。我需要每个项目文件夹中的个人信息。
XML 是一种结构化文本格式。它对 "folders" 一无所知。您在屏幕截图中看到的只是您用于显示数据的程序如何呈现数据。
无论如何,获得所需内容的最佳方法是使用 SelectNodes()
和 XPath 表达式。和往常一样。
[xml]$xml = Get-Content 'X:\folder\my.xml'
$xml.SelectNodes('//Product/Item[@Class="Patch"]') |
Select-Object BulletinID, PatchName, Status
tl;dr
如您所料,a 名称冲突 阻止了对 .Item
属性 的访问 [= =311=]感兴趣的元素; 修复显式枚举parent元素的问题:
$xml.PatchScan.Machine.Product | % { $_.Item | select BulletinId, PatchName, Status }
%
是 built-in cmdlet 的别名;请参阅底部的解释。
作为替代方案,提供了一个简洁的XPath-based解决方案,它既是高效并允许复杂的查询.
顺便说一句:PowerShell v3+ 附带 Select-Xml
cmdlet,它以文件路径作为参数,允许 single-pipeline 解决方案:
(Select-Xml -LiteralPath X:\folder\my.xml '//Product/Item[@Class="Patch"]').Node |
Select-Object BulletinId, PatchName, Status
Select-Xml
将匹配的 XML 节点包装在外部对象中,因此需要访问 .Node
属性.
PowerShell 对 XML DOM(点符号)的改编:
PowerShell 装饰 [System.Xml.XmlDocument]
实例中包含的对象层次结构(例如,使用 cast [xml]
创建) ):
具有为输入文档的特定元素和属性命名的属性[1]每一层;例如:
([xml] '<foo><bar>baz</bar></foo>').foo.bar # -> 'baz'
([xml] '<foo><bar id="1" /></foo>').foo.bar.id # -> '1'
将给定层次结构级别的多个同名元素隐式转换为数组 (具体来说,[object[]]
类型);例如:
([xml] '<foo><C>one</C><C>two</C></foo>').foo.C[1] # -> 'two'
如示例(以及问题中您自己的代码)所示,这允许 通过方便的点符号进行访问 。
注意:如果您使用点符号来定位至少具有一个 attribute and/or child 元素的元素,元素 本身 被返回(一个 XmlElement
instance); otherwise, it is the element's text content; for information about updating XML documents via dot notation, see .
点符号的 缺点 是可能存在 名称冲突 ,如果 偶然 input-XML 元素名称恰好与 intrinsic [System.Xml.XmlElement]
属性 名称相同(对于 single-element 属性),或固有 [Array]
属性 名称(对于 数组 值属性;[System.Object[]]
派生自 [Array]
).
如果发生名称冲突: 如果正在访问的 属性 包含:
a 单个子元素([System.Xml.XmlElement]
), 附带 属性获胜.
- 这也是可能有问题,因为它使得访问内在 类型属性不可预测 - 见底部。
子元素数组,[Array]
类型的属性获胜。
因此,以下元素名称使用 array-valued 属性 打破点符号(通过反射获得命令
Get-Member -InputObject 1, 2 -Type Properties, ParameterizedProperty
):
Item Count IsFixedSize IsReadOnly IsSynchronized Length LongLenth Rank SyncRoot
请参阅最后一节,讨论这种差异以及如何在发生碰撞时访问内在 [System.Xml.XmlElement]
属性。
解决方法是使用显式枚举array-valued属性,使用 ForEach-Object
cmdlet,如顶部所示。
这是一个完整的例子:
[xml] $xml = @'
<PatchScan>
<Machine>
<Product>
<Name>Windows 10 Pro (x64)</Name>
<Item Class="Patch">
<BulletinId>MSAF-054</BulletinId>
<PatchName>windows10.0-kb3189031-x64.msu</PatchName>
<Status>Installed</Status>
</Item>
<Item Class="Patch">
<BulletinId>MSAF-055</BulletinId>
<PatchName>windows10.0-kb3189032-x64.msu</PatchName>
<Status>Not Installed</Status>
</Item>
</Product>
<Product>
<Name>Windows 7 Pro (x86)</Name>
<Item Class="Patch">
<BulletinId>MSAF-154</BulletinId>
<PatchName>windows7-kb3189031-x86.msu</PatchName>
<Status>Partly Installed</Status>
</Item>
<Item Class="Patch">
<BulletinId>MSAF-155</BulletinId>
<PatchName>windows7-kb3189032-x86.msu</PatchName>
<Status>Uninstalled</Status>
</Item>
</Product>
</Machine>
</PatchScan>
'@
# Enumerate the array-valued .Product property explicitly, so that
# the .Item property can successfully be accessed on each XmlElement instance.
$xml.PatchScan.Machine.Product |
ForEach-Object { $_.Item | Select-Object BulletinID, PatchName, Status }
以上结果:
Class BulletinId PatchName Status
----- ---------- --------- ------
Patch MSAF-054 windows10.0-kb3189031-x64.msu Installed
Patch MSAF-055 windows10.0-kb3189032-x64.msu Not Installed
Patch MSAF-154 windows7-kb3189031-x86.msu Partly Installed
Patch MSAF-155 windows7-kb3189032-x86.msu Uninstalled
更深入的兔子洞:什么属性被隐藏在什么时候:
注意:shadowing 我的意思是在名称冲突的情况下,“获胜者”属性 - 其价值被报告的人 - 有效地隐藏了另一个,从而“把它放在阴影中”。
在对数组使用点符号的情况下,一个名为member-access enumeration的特性开始发挥作用,适用于 PowerShell v3+ 中的 any 集合;换句话说:该行为并非特定于 [xml]
类型。
简而言之:访问集合上的 属性 会隐式访问集合的 每个成员 上的 属性(集合中的项目)和 returns 结果值作为 数组 ([System.Object[]]
); .例如:
# Using member-access enumeration, collect the value of the .prop property from
# the array's individual *members*.
> ([pscustomobject] @{ prop = 10 }, [pscustomobject] @{ prop = 20 }).prop
10
20
但是,如果集合类型 本身 有一个 属性 同名,则集合自己的 属性 优先;例如:
# !! Since arrays themselves have a property named .Count,
# !! member-access enumeration does NOT occur here.
> ([pscustomobject] @{ count = 10 }, [pscustomobject] @{ count = 20 }).Count
2 # !! The *array's* count property was accessed, returning the count of elements
在 对 [xml]
使用点符号的情况下(PowerShell-decorated System.Xml.XmlDocument
和 System.Xml.XmlElement
实例),PowerShell-added,附带属性影响 type-intrinsic 那些:[2]
虽然这种行为很容易理解,但 结果取决于特定输入 的事实也可能是 诡异的 :
例如,在下面的例子中附带的name
child元素阴影 元素本身 :
上同名的内在 属性
> ([xml] '<xml><child>foo</child></xml>').xml.Name
xml # OK: The element's *own* name
> ([xml] '<xml><name>foo</name></xml>').xml.Name
foo # !! .name was interpreted as the incidental *child* element
如果您确实需要访问内部类型的属性,请使用.get_<property-name>()
:
> ([xml] '<xml><name>foo</name></xml>').xml.get_Name()
xml # OK - intrinsic property value to use of .get_*()
[1] 如果给定元素同时具有属性 和 以及同名元素,PowerShell 报告 both,作为数组[object[]]
.
的元素
[2] 表面上,当 PowerShell 在幕后适配底层 System.Xml.XmlElement
类型时,它不会公开其属性 本身,但通过get_*
访问器方法,仍然允许访问,就好像它们是属性一样,但 PowerShell-added incidental-but-bona-fide 属性优先。如果您对此有更多了解,请告诉我们。
我有一个 XML 文件,我想对其进行解析并检索特定信息。
为了便于理解,下面是 XML 文件的屏幕截图:
我想解析 XML 并为每个 Item
节点检索屏幕截图中指示的字段。每个检索到的值都需要按项目节点进行格式化。
最后,我希望能够指定要查找的条件,并且只检索找到的条件。
我一直在努力,没有运气。这是我能够想出的:
[xml]$MyXMLFile = gc 'X:\folder\my.xml'
$XMLItem = $MyXMLFile.PatchScan.Machine.Product.Item
$Patch = $XMLItem | Where-Object {$_.Class -eq 'Patch'}
$Patch.BulletinID
$Patch.PatchName
$Patch.Status
当我运行上面的代码时,它returns没有结果。但是,仅出于测试目的,我删除了 Item 部分。现在,我可以通过修改上面的代码让它工作。
我将 XML 加载到 XML 对象中。现在我尝试将它向下遍历到产品并且它完美地工作:
PS> $xmlobj.PatchScan.Machine.Product | Select-Object -Property Name, SP Name SP ---- -- Windows 10 Pro (x64) 1607 Internet Explorer 11 (x64) Gold Windows Media Player 12.0 Gold MDAC 6.3 (x64) Gold .NET Framework 4.7 (x64) Gold MSXML 3.0 SP11 MSXML 6.0 (x64) SP3 DirectX 9.0c Gold Adobe Flash 23 Gold VMware Tools x64 Gold Microsoft Visual C++ 2008 SP1 Redistributable Gold Microsoft Visual C++ 2008 SP1 Redistributable (x64) Gold
现在添加 Item 并且 Intellisense 会放一个括号,就好像 Item 是一个方法 $xmlobj.PatchScan.Machine.Product.Item(
← 看到了吗?所以这就是为什么我出于某种原因认为 Item
节点正在做一些奇怪的事情,这是我的障碍。
这张截图更好地展示了它是如何从许多产品文件夹开始的,然后在每个产品文件夹中又是许多项目文件夹。
我不关心产品文件夹中的XML。我需要每个项目文件夹中的个人信息。
XML 是一种结构化文本格式。它对 "folders" 一无所知。您在屏幕截图中看到的只是您用于显示数据的程序如何呈现数据。
无论如何,获得所需内容的最佳方法是使用 SelectNodes()
和 XPath 表达式。和往常一样。
[xml]$xml = Get-Content 'X:\folder\my.xml'
$xml.SelectNodes('//Product/Item[@Class="Patch"]') |
Select-Object BulletinID, PatchName, Status
tl;dr
如您所料,a 名称冲突 阻止了对 .Item
属性 的访问 [= =311=]感兴趣的元素; 修复显式枚举parent元素的问题:
$xml.PatchScan.Machine.Product | % { $_.Item | select BulletinId, PatchName, Status }
%
是 built-in cmdlet 的别名;请参阅底部的解释。
作为替代方案,
顺便说一句:PowerShell v3+ 附带 Select-Xml
cmdlet,它以文件路径作为参数,允许 single-pipeline 解决方案:
(Select-Xml -LiteralPath X:\folder\my.xml '//Product/Item[@Class="Patch"]').Node |
Select-Object BulletinId, PatchName, Status
Select-Xml
将匹配的 XML 节点包装在外部对象中,因此需要访问 .Node
属性.
PowerShell 对 XML DOM(点符号)的改编:
PowerShell 装饰 [System.Xml.XmlDocument]
实例中包含的对象层次结构(例如,使用 cast [xml]
创建) ):
具有为输入文档的特定元素和属性命名的属性[1]每一层;例如:
([xml] '<foo><bar>baz</bar></foo>').foo.bar # -> 'baz' ([xml] '<foo><bar id="1" /></foo>').foo.bar.id # -> '1'
将给定层次结构级别的多个同名元素隐式转换为数组 (具体来说,
[object[]]
类型);例如:([xml] '<foo><C>one</C><C>two</C></foo>').foo.C[1] # -> 'two'
如示例(以及问题中您自己的代码)所示,这允许 通过方便的点符号进行访问 。
注意:如果您使用点符号来定位至少具有一个 attribute and/or child 元素的元素,元素 本身 被返回(一个 XmlElement
instance); otherwise, it is the element's text content; for information about updating XML documents via dot notation, see
点符号的 缺点 是可能存在 名称冲突 ,如果 偶然 input-XML 元素名称恰好与 intrinsic [System.Xml.XmlElement]
属性 名称相同(对于 single-element 属性),或固有 [Array]
属性 名称(对于 数组 值属性;[System.Object[]]
派生自 [Array]
).
如果发生名称冲突: 如果正在访问的 属性 包含:
a 单个子元素(
[System.Xml.XmlElement]
), 附带 属性获胜.- 这也是可能有问题,因为它使得访问内在 类型属性不可预测 - 见底部。
子元素数组,
[Array]
类型的属性获胜。因此,以下元素名称使用 array-valued 属性 打破点符号(通过反射获得命令
Get-Member -InputObject 1, 2 -Type Properties, ParameterizedProperty
):Item Count IsFixedSize IsReadOnly IsSynchronized Length LongLenth Rank SyncRoot
请参阅最后一节,讨论这种差异以及如何在发生碰撞时访问内在 [System.Xml.XmlElement]
属性。
解决方法是使用显式枚举array-valued属性,使用 ForEach-Object
cmdlet,如顶部所示。
这是一个完整的例子:
[xml] $xml = @'
<PatchScan>
<Machine>
<Product>
<Name>Windows 10 Pro (x64)</Name>
<Item Class="Patch">
<BulletinId>MSAF-054</BulletinId>
<PatchName>windows10.0-kb3189031-x64.msu</PatchName>
<Status>Installed</Status>
</Item>
<Item Class="Patch">
<BulletinId>MSAF-055</BulletinId>
<PatchName>windows10.0-kb3189032-x64.msu</PatchName>
<Status>Not Installed</Status>
</Item>
</Product>
<Product>
<Name>Windows 7 Pro (x86)</Name>
<Item Class="Patch">
<BulletinId>MSAF-154</BulletinId>
<PatchName>windows7-kb3189031-x86.msu</PatchName>
<Status>Partly Installed</Status>
</Item>
<Item Class="Patch">
<BulletinId>MSAF-155</BulletinId>
<PatchName>windows7-kb3189032-x86.msu</PatchName>
<Status>Uninstalled</Status>
</Item>
</Product>
</Machine>
</PatchScan>
'@
# Enumerate the array-valued .Product property explicitly, so that
# the .Item property can successfully be accessed on each XmlElement instance.
$xml.PatchScan.Machine.Product |
ForEach-Object { $_.Item | Select-Object BulletinID, PatchName, Status }
以上结果:
Class BulletinId PatchName Status
----- ---------- --------- ------
Patch MSAF-054 windows10.0-kb3189031-x64.msu Installed
Patch MSAF-055 windows10.0-kb3189032-x64.msu Not Installed
Patch MSAF-154 windows7-kb3189031-x86.msu Partly Installed
Patch MSAF-155 windows7-kb3189032-x86.msu Uninstalled
更深入的兔子洞:什么属性被隐藏在什么时候:
注意:shadowing 我的意思是在名称冲突的情况下,“获胜者”属性 - 其价值被报告的人 - 有效地隐藏了另一个,从而“把它放在阴影中”。
在对数组使用点符号的情况下,一个名为member-access enumeration的特性开始发挥作用,适用于 PowerShell v3+ 中的 any 集合;换句话说:该行为并非特定于 [xml]
类型。
简而言之:访问集合上的 属性 会隐式访问集合的 每个成员 上的 属性(集合中的项目)和 returns 结果值作为 数组 ([System.Object[]]
); .例如:
# Using member-access enumeration, collect the value of the .prop property from
# the array's individual *members*.
> ([pscustomobject] @{ prop = 10 }, [pscustomobject] @{ prop = 20 }).prop
10
20
但是,如果集合类型 本身 有一个 属性 同名,则集合自己的 属性 优先;例如:
# !! Since arrays themselves have a property named .Count,
# !! member-access enumeration does NOT occur here.
> ([pscustomobject] @{ count = 10 }, [pscustomobject] @{ count = 20 }).Count
2 # !! The *array's* count property was accessed, returning the count of elements
在 对 [xml]
使用点符号的情况下(PowerShell-decorated System.Xml.XmlDocument
和 System.Xml.XmlElement
实例),PowerShell-added,附带属性影响 type-intrinsic 那些:[2]
虽然这种行为很容易理解,但 结果取决于特定输入 的事实也可能是 诡异的 :
例如,在下面的例子中附带的name
child元素阴影 元素本身 :
> ([xml] '<xml><child>foo</child></xml>').xml.Name
xml # OK: The element's *own* name
> ([xml] '<xml><name>foo</name></xml>').xml.Name
foo # !! .name was interpreted as the incidental *child* element
如果您确实需要访问内部类型的属性,请使用.get_<property-name>()
:
> ([xml] '<xml><name>foo</name></xml>').xml.get_Name()
xml # OK - intrinsic property value to use of .get_*()
[1] 如果给定元素同时具有属性 和 以及同名元素,PowerShell 报告 both,作为数组[object[]]
.
[2] 表面上,当 PowerShell 在幕后适配底层 System.Xml.XmlElement
类型时,它不会公开其属性 本身,但通过get_*
访问器方法,仍然允许访问,就好像它们是属性一样,但 PowerShell-added incidental-but-bona-fide 属性优先。如果您对此有更多了解,请告诉我们。