无法在 PowerShell 中完全解析 XML
Posted
技术标签:
【中文标题】无法在 PowerShell 中完全解析 XML【英文标题】:Unable to completely parse XML in PowerShell 【发布时间】:2018-02-02 22:12:06 【问题描述】:我有一个 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
当我运行上面的代码时,它没有返回任何结果。但是,仅出于测试目的,我删除了 Item 部分。现在,我可以通过修改上面的代码来让它工作。
我将 XML 加载到 XML 对象中。现在我尝试将其遍历到产品,并且效果很好:
PS> $xmlobj.PatchScan.Machine.Product |选择对象-属性名称,SP 名称 SP ---- -- Windows 10 专业版 (x64) 1607 Internet Explorer 11 (x64) 黄金 Windows 媒体播放器 12.0 黄金版 MDAC 6.3 (x64) 黄金 .NET Framework 4.7 (x64) 金牌 MSXML 3.0 SP11 MSXML 6.0 (x64) SP3 DirectX 9.0c 金牌 Adobe Flash 23 金 VMware 工具 x64 金牌 Microsoft Visual C++ 2008 SP1 可再发行黄金版 Microsoft Visual C++ 2008 SP1 Redistributable (x64) Gold现在添加 Item 并且 Intellisense 放置一个括号,好像 Item 是一个方法 $xmlobj.PatchScan.Machine.Product.Item(
← 看到了吗?所以这就是为什么我认为出于某种原因Item
节点正在做一些奇怪的事情,这就是我的障碍。
这个截图更好地展示了它是如何从许多产品文件夹开始的,然后在每个产品文件夹中都有许多项目文件夹。
我不关心的产品文件夹中的 XML。我需要每个项目文件夹中的个人信息。
【问题讨论】:
【参考方案1】:XML 是一种结构化的文本格式。它对“文件夹”一无所知。您在屏幕截图中看到的只是您用于显示数据的程序如何呈现数据。
无论如何,获得所需内容的最佳方法是使用 SelectNodes()
和 XPath 表达式。像往常一样。
[xml]$xml = Get-Content 'X:\folder\my.xml'
$xml.SelectNodes('//Product/Item[@Class="Patch"]') |
Select-Object BulletinID, PatchName, Status
【讨论】:
+1 用于 XPath 解决方案,但请注意 OP 的唯一问题是 名称冲突。简而言之,OP 的命令中断了,因为Item
与 [Array]
类型的属性名称发生冲突。【参考方案2】:
tl;dr
正如您所怀疑的,名称冲突阻止了对感兴趣的 XML 元素上的 .Item
属性的访问; 修复父元素显式枚举的问题:
$xml.PatchScan.Machine.Product | % $_.Item | select BulletinId, PatchName, Status
%
是ForEach-Object
cmdlet 的内置别名;解释见底部。
作为替代方案,Ansgar Wiecher's helpful answer 提供了一个简洁的基于 XPath 的解决方案,它既高效又允许复杂的查询。 p>
顺便说一句:PowerShell v3+ 附带 Select-Xml
cmdlet,它以文件路径作为参数,允许使用单管道解决方案:
(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]
实例中包含的对象层次结构(例如,使用转换[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 和/或 child 元素的元素,则元素 itself返回(XmlElement
实例);否则为元素的文本内容;有关通过点符号更新 XML 文档的信息,请参阅this answer。
点表示法的缺点是,如果 偶然 input-XML 元素名称恰好相同,则可能存在 名称冲突作为 intrinsic [System.Xml.XmlElement]
属性名称(对于 单元素 属性)或固有 [Array]
属性名称(对于 array-有价值的属性;[System.Object[]]
派生自 [Array]
)。
如果发生名称冲突:如果正在访问的属性包含:
单个子元素 ([System.Xml.XmlElement]
),附带属性获胜。
子元素的数组,[Array]
类型的属性获胜。
因此,以下元素名称用数组值属性打破点符号(通过反射命令获得Get-Member -InputObject 1, 2 -Type Properties, ParameterizedProperty
):
Item Count IsFixedSize IsReadOnly IsSynchronized Length LongLenth Rank SyncRoot
有关此差异的讨论以及如何在发生冲突时访问固有的[System.Xml.XmlElement]
属性,请参阅最后一节。
解决方法是使用显式数组值属性枚举,使用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
进一步深入兔子洞:哪些属性在以下情况下被遮蔽:
注意:遮蔽我的意思是,在名称冲突的情况下,“获胜”属性——其值被报告的那个——有效地隐藏了另一个,从而“把它放在阴影”。
在对数组使用点表示法的情况下,一个名为member-access enumeration 的功能开始发挥作用,适用于任何 PowerShell v3+ 中的集合;换句话说:该行为并非特定于 [xml]
类型。
简而言之:访问集合上的属性会隐式访问集合(集合中的项目)的每个成员上的属性,并将结果值作为数组返回([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 装饰的System.Xml.XmlDocument
和System.Xml.XmlElement
实例)结合使用的情况下,PowerShell 添加的附带属性会影响类型固有个:[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] 如果给定元素同时具有属性 and 和同名元素,PowerShell 会将 both 报告为 数组 [object[]]
.
[2] 貌似,当 PowerShell 在后台适配底层的System.Xml.XmlElement
类型时,它不会像这样公开其属性,而是通过get_*
访问器 methods,它仍然允许像 属性一样进行访问,但优先使用 PowerShell 添加的偶然但真正的属性。如果您对此有更多了解,请告诉我们。
【讨论】:
以上是关于无法在 PowerShell 中完全解析 XML的主要内容,如果未能解决你的问题,请参考以下文章
无法将正则表达式模式表单文件解析为 powershell 中的变量
powershell -enc参数无法解码base64编码payload的解决方案
Win10 cmd/PowerShell SSH无法创建目录 和 SCP 找不到文件