【问题标题】:Trying to import XML children from one file to another尝试将 XML 子项从一个文件导入到另一个文件
【发布时间】:2020-03-08 21:14:46
【问题描述】:

我查看了this 的帖子,发现这几乎正是我需要做的。但是,鉴于这篇文章中的建议,我无法产生预期的输出。基本上,我正在尝试从包含以下内容的 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)

现在,它不会出错,但它也根本不会导入。似乎某些元素未在某处正确分配。我尝试了很多替代方案,这似乎是唯一接近我想要的替代方案。有什么建议么?

【问题讨论】:

    标签: xml powershell xpath xml-namespaces


    【解决方案1】:

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

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

      李>
    • 您正在使用 .SelectSingleNode() 选择源文档节点,即使 - 我想 - 您打算使用 .SelectNodes() 选择 所有 &lt;parameter&gt; 元素。

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


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

    $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() 方法,因为它们接受XPath queries,是最灵活、最强大的方法来定位感兴趣的元素(节点)一个 XML 文档,但如果输入文档声明了命名空间(例如在您的情况下为 xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1"),它们确实需要显式命名空间处理

        1234563 parameters) 和 fails 带有命名空间限定(命名空间前缀)的那些(例如,plaster:parameters)。
    • 命名空间处理涉及这些步骤(请注意,给定文档可能有多个命名空间声明,但为简单起见,说明假设只有一个):

      • 实例化命名空间管理器并将其与输入文档[的名称表]相关联。

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

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

      • Ansgar Wiechers' helpful answer 证明了这一切。

    • 1234563 this answer
  • 相比之下,PowerShell 的点表示法始​​终是 namespace-agnostic,因此它需要 no 显式命名空间处理。

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

    • PowerShell 的点表示法:

      • PowerShell 方便地将 XML 文档的 DOM(输入文档中节点的层次结构)映射到具有属性的嵌套对象上,允许您深入使用常规点符号进入文档;例如,XPath 查询 '/root/elem' 的等价物将是 $xmlDoc.root.elem
        但是,这意味着您只能使用此表示法来访问您已经知道层次结构中路径的元素 - 不支持查询(尽管支持 XPath 的 Select-Xml cmdlet 退出)。

      • 这个映射忽略命名空间限定符(前缀),所以必须使用纯元素名,不带任何命名空间前缀;例如,如果输入文档有一个 plaster:parameters 元素,您必须将其引用为 parameters

      • 和点符号一样方便,它也有陷阱,其中最值得注意的是准叶元素 - 那些根本没有子节点或只有 非元素 子节点(例如文本节点)的节点 - 返回为 字符串,而不是元素,这使得修改它们变得困难。
        此外,类型原生属性与 PowerShell 添加的反映特定文档的元素和属性名称的属性之间可能存在名称冲突 - 请参阅this answer
        简而言之:XML DOM 和 PowerShell 的对象模型之间的映射不是——也不能是——精确和完整的

  • 【讨论】:

      【解决方案2】:

      正如mklement0 所指出的,您的XML 文档具有名称空间,因此在选择具有XPath 表达式的节点时,您需要一个名称空间管理器。使用 dot-access 选择节点可以帮助您进行命名空间管理,但由于 dot-access 并不总是按预期的方式工作,我仍然建议坚持使用 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')
      

      【讨论】:

      • 是的,我确实读到点访问会造成一些不良影响。我已经处理了一些 XML 操作,并且上次选择了 XPath 表达式,但我认为使用点访问会更容易。学过的知识。感谢您的反馈!
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-22
      • 1970-01-01
      • 2020-01-24
      • 2018-05-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多