尝试将一个文件中的XML子元素导入到另一个文件中

3

我已经研究了这篇文章,并发现它几乎正是我需要做的。然而,尽管按照这篇文章的建议,我仍无法生成预期的输出。基本上,我正在尝试从包含以下内容的XML($ManifestFile)文件中导入</parameter>元素:

<?xml version="1.0" encoding="utf-8"?>
<plasterManifest
  schemaVersion="1.1"
  templateType="Project" xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1">
  <metadata>
    <name>PlasterTestProject</name>
    <id>4c08dedb-7da7-4193-a2c0-eb665fe2b5e1</id>
    <version>0.0.1</version>
    <title>Testing creating custom Plaster Template for CI/CD</title>
    <description>Testing out creating a module project with Plaster for complete CI/CD files.</description>
    <author>Catherine Meyer</author>
    <tags></tags>
  </metadata>
  <parameters>
        <parameter name='AuthorName' type="user-fullname" prompt="Module author's name" />
        <parameter name='ModuleName' type="text" prompt="Name of your module" />
        <parameter name='ModuleDescription' type="text" prompt="Brief description on this module" />
        <parameter name='ModuleVersion' type="text" prompt="Initial module version" default='0.0.1' />
        <parameter name='GitLabUserName' type="text" prompt="Enter the GitLab Username to be used" default="${PLASTER_PARAM_FullName}"/>
        <parameter name="GitLubRepo" type="text" prompt="GitiLab repo name for this module" default="${PLASTER_PARAM_ModuleName}"/>
        <parameter name='ModuleFolders' type = 'multichoice' prompt='Please select folders to include' default='0,1'>
            <choice label='&amp;Public' value='Public' help='Folder containing public functions that can be used by the user.'/>
            <choice label='&amp;Private' value='Private' help='Folder containing internal functions that are not exposed to users'/>
        </parameter>
    </parameters>
</plasterManifest>

我正在尝试导入的文档($NewManifestFile)看起来像:

<?xml version="1.0" encoding="utf-8"?>
<plasterManifest schemaVersion="1.1" templateType="Project" xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1">
  <metadata>
     <name>test3</name>
     <id>8c028f40-cdc6-40dc-8442-f5256a8c0ed9</id>
     <version>0.0.1</version>
     <title>test3</title>
     <description>SDSKL</description>
     <author>NAME</author>
    <tags> </tags>
  </metadata>
  <parameters>
  </parameters>
  <content>
  </content>
</plasterManifest>

我是一个有用的助手,可以为您进行文本翻译。以下是您需要翻译的内容:

我编写的代码大致如下:

$ManifestFile = [xml](Get-Content ".\PlasterManifest.xml")
$NewManifestFile = [xml](Get-Content $PlasterMetadata.Path)
$NewManifestFile.plasterManifest.metadata.name

$Parameters = $ManifestFile.SelectSingleNode("//plasterManifest/parameters/parameter")
$Parameters
$NewParameters = $NewManifestFile.SelectSingleNode("//plasterManifest/parameters")
#Importing the parameters and content
foreach ($parameter in $Parameters) {
   $NewParamElem = $ManifestFile.ImportNode($parameter, $true)
   $NewParameters.AppendChild($NewParamElem)
}
[void]$NewManifestFile.save($PlasterMetadata.Path)

现在,它不会报错,但是它也完全没有被导入。似乎某个元素在某处没有被正确地分配。我尝试了很多替代方案,这似乎是我想要的唯一一个。有什么建议吗?
2个回答

4

你当前的方法存在几个问题:

  • 您没有将源文档的元素导入到目标文档中,尽管这是将其插入到目标文档的 DOM 中的先决条件。

  • 您正在使用 .SelectSingleNode() 选择源文档节点,尽管我认为您应该使用 .SelectNodes() 来选择所有的 <parameter> 元素。

  • 您缺少文档的命名空间管理,这是通过 .SelectSingleNode() / .SelectNodes() 成功执行 XPath 查询的一个先决条件

    • 鉴于命名空间管理很复杂,下面的解决方案依赖于 PowerShell 的 XML DOM 点符号,这部分与您的问题有关,它避免了需要处理命名空间,并提供了一个解决方法。如果您确实想要处理命名空间 - 这是正确的方式 - 请参见 Ansgar Wiechers 的有用答案

这是一个带有注释的解决方案:

$ManifestFile = [xml](Get-Content -Raw ./PlasterManifest.xml)
$NewManifestFile = [xml](Get-Content -Raw $PlasterMetadata.Path)

# Get the <parameters> element in the *source* doc.
# Note that PowerShell's dot notation-based access to the DOM does
# NOT require namespace management.
$ParametersRoot = $ManifestFile.plasterManifest.parameters

# Get the parent of the <parameter> elements, <parameters>, in the *destination* doc.
# Note: Ideally we'd also use dot notation in order for this, 
#       but since the target <parameters> element is *empty*, 
#       PowerShell represents it as a *string* rather than as an XML element.
#       Instead, we use the type-native index indexer ([...]) to get the
#       (first and only) <parameters> child element of the 
#       <plasterManifest> element by name.
$NewParametersRoot = $NewManifestFile.plasterManifest['parameters']

# Import the source element's subtree into the destination document, so it can
# be inserted into the DOM later.
$ImportedParametersRoot = $NewManifestFile.ImportNode($ParametersRoot, $True)

# For simplicity, replace the entire <parameters> element, which
# obviates the need for a loop.
# Note the need to call .ReplaceChild() on the .documentElement property,
# not on the document object itself.
$null = $NewManifestFile.documentelement.ReplaceChild($ImportedParametersRoot, $NewParametersRoot)

# Save the modified destination document.
$NewManifestFile.Save($PlasterMetadata.Path)

可选背景信息:

  • .SelectSingleNode() / .SelectNodes() 方法是定位 XML 文档中感兴趣的元素(节点)最灵活、最强大的方法,因为它们接受 XPath 查询,但如果输入文档声明了命名空间(例如在你的情况下声明了 xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1"),则需要进行显式的命名空间处理

    • 注意:如果给定的输入文档声明了命名空间,并且您忽略以下描述的处理方式,则 .SelectSingleNode() / .SelectNodes() 对于所有查询都会返回 $null,如果使用未限定的元素名称(例如 parameters),则会失败并显示带有命名空间前缀的(命名空间限定的)元素名称(例如 plaster:parameters)。

    • 命名空间处理涉及以下步骤(请注意,给定的文档可能有多个命名空间声明,但为简单起见,说明假设只有一个):

      • 实例化命名空间管理器并将其与输入文档['s name table]相关联。

      • 将命名空间的 URI 与符号标识符相关联。如果输入文档中的命名空间声明是默认命名空间 - xmlns - 您不能将其用作符号标识符(名称 xmlns 被保留),必须简单地选择一个。

      • 然后,当您调用 .SelectSingleNode() / .SelectNodes() 时,您必须在查询字符串中使用此符号标识符作为元素名称前缀;例如,如果您(自己选择的)符号标识符是 plaster,并且您正在查找文档中的任何位置的元素 parameters,则会使用查询字符串 '//plaster:pararameters'

      • Ansgar Wiechers' helpful answer演示了所有这些内容。

    • 考虑使用PowerShell 的 Select-Xml cmdlet 作为替代方法:作为围绕 .SelectNodes() 的高级包装器,它也支持 XPath 查询,但使命名空间管理更加容易 - 请参见 this answer

  • 相比之下,PowerShell 的点表示法始终是命名空间不可知的,因此它不需要显式的命名空间处理。

    • 注意:虽然这降低了复杂性,但只有在您知道适当的命名空间处理不是处理输入文档的必要条件时,才应使用它。

    • PowerShell 的点表示法:

      • PowerShell 将 XML 文档的 DOM - 输入文档中节点的层次结构 - 方便地映射到具有属性的嵌套对象上,允许您使用常规点表示法深入到文档中;例如,与 XPath 查询 '/root/elem' 等效的是 $xmlDoc.root.elem


3
正如mklement0所指出的,您的XML文档有命名空间,因此在使用XPath表达式选择节点时需要一个命名空间管理器。使用点访问来选择节点可以避开命名空间管理,但由于点访问不总是按照人们的预期方式工作,我仍然建议坚持使用SelectNodes()并使用适当的命名空间管理器。
$uri = 'http://www.microsoft.com/schemas/PowerShell/Plaster/v1'

[xml]$ManifestFile = Get-Content 'C:\path\to\old.xml'
$nm1 = New-Object Xml.XmlNamespaceManager $ManifestFile.NameTable
$nm1.AddNamespace('ns1', $uri)

[xml]$NewManifestFile = Get-Content 'C:\path\to\new.xml'
$nm2 = New-Object Xml.XmlNamespaceManager $NewManifestFile.NameTable
$nm2.AddNamespace('ns2', $uri)

$ManifestFile.SelectNodes('//ns1:parameter', $nm1) | ForEach-Object {
    $newnode = $NewManifestFile.ImportNode($_, $true)
    $parent  = $NewManifestFile.SelectSingleNode('//ns2:parameters', $nm2)
    $parent.AppendChild($newnode) | Out-Null
}

$NewManifestFile.Save('C:\path\to\new.xml')

1
是的,我确实读到了点访问可能会带来一些不必要的影响。我稍微处理过 XML 操作,并选择了上次使用 XPath 表达式,但我认为使用点访问会更容易。吸取教训。感谢您的反馈! - deathcat05

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接