4

我有一个 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。我需要每个项目文件夹中的个人信息。

4

2 回答 2

3

tl;博士

正如您所怀疑的,名称冲突阻止了.Item对感兴趣的 XML 元素上的属性的访问;解决显式枚举元素的问题:

$xml.PatchScan.Machine.Product | % { $_.Item | select BulletinId, PatchName, Status }

%ForEach-Object是cmdlet的内置别名;见底部的解释。


作为替代方案Ansgar Wiecher 的有用答案提供了一个简洁的基于 XPath 的解决方案,它既高效又允许复杂的查询

顺便说一句:PowerShell v3+ 附带Select-Xmlcmdlet,它将文件路径作为参数,允许使用单管道解决方案:

(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'
    

正如示例(以及问题中您自己的代码)所示,这允许通过方便的点符号进行访问

注意:如果您使用点符号来定位具有至少一个属性和/或元素的元素,则返回元素本身(一个 XmlElement实例);否则,它是元素的文本内容;有关通过点表示法更新XML 文档的信息,请参阅此答案

点表示法的缺点是可能存在名称冲突,如果偶然的输入 XML 元素名称恰好与固有 [System.Xml.XmlElement]属性名称(对于单元素属性)或固有[Array]属性名称(对于数组值属性;[System.Object[]]源自[Array])。

如果发生名称冲突:如果正在访问的属性包含:

  • 单个元素( [System.Xml.XmlElement]),附带属性 win

    • 这也可能是有问题的,因为它使访问内在类型属性变得不可预测- 请参阅底部。
  • 元素数组类型的属性获胜。[Array]

    • 因此,以下元素名称使用数组值属性(通过反射命令获得
      Get-Member -InputObject 1, 2 -Type Properties, ParameterizedProperty)打破点符号:

          Item Count IsFixedSize IsReadOnly IsSynchronized Length LongLenth Rank SyncRoot
      

[System.Xml.XmlElement]有关此差异的讨论以及如何在发生碰撞时访问内在属性,请参见最后一节。

解决方法是使用cmdlet 使用数组值属性的显式枚举ForEach-Object顶部所示。
这是一个完整的例子:

[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     

进一步深入兔子洞:在以下情况下会隐藏哪些属性:

注意:我所说的遮蔽是指在名称冲突的情况下,“获胜”属性(其值被报告的属性)有效地隐藏了另一个属性,从而“将其置于阴影中”。


在对数组使用点表示法的情况下,一个称为成员枚举的功能开始发挥作用,它适用于 PowerShell v3+中的任何集合;换句话说:行为不是特定于[xml]类型的。

简而言之:访问集合上的属性会隐式访问集合中每个成员(集合中的项)的属性,并将结果值作为数组[System.Object[]])返回;。例如:

# Using member 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 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.XmlDocumentSystem.Xml.XmlElement实例)一起使用的情况下,PowerShell 添加的附带属性会影响类型固有属性[2]

虽然这种行为很容易掌握,但结果取决于特定输入的事实也可能很危险

例如,在以下示例中,附带的name 子元素会在元素本身上隐藏同名的固有属性:

> ([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 将两者都报告为数组 [object[]]的元素。

[2]看起来System.Xml.XmlElement,当 PowerShell在幕后调整底层类型时,它并没有公开它的属性而是通过get_*访问器方法,它仍然允许访问,就好像它们属性一样,但是添加了 PowerShell 的附带-但是-善意的财产优先。如果您对此有更多了解,请告诉我们。

于 2018-03-10T20:13:30.000 回答
3

XML 是一种结构化的文本格式。它对“文件夹”一无所知。您在屏幕截图中看到的只是您用于显示数据的程序如何呈现数据。

无论如何,获得所需内容的最佳方法是使用SelectNodes()XPath表达式。照常。

[xml]$xml = Get-Content 'X:\folder\my.xml'
$xml.SelectNodes('//Product/Item[@Class="Patch"]') |
    Select-Object BulletinID, PatchName, Status
于 2018-02-02T23:23:10.600 回答