oliswiss
Goto Top

PowerShell XML - gleichen Child bei allen Nodes hinzufügen

Hallo PowerShell-Guru's,
nach stundenlangem try-and-error bin ich am Ende und die Lösung sollte an sich ganz einfach sein. Bin ganz neu mit PS unterwegs und drum fehlt mir da leider zuviel an Hintergrundwissen und zur Syntax! Ich hoffe sehr, dass mir da jemand kurz unter die Arme greifen kann.
Folgendes:
Ich erhalte ein XML-file wie folgt:
<?xml version="1.0" encoding="UTF-8"?>  
<PRODUCTS>
     <SPAREPARTS>
          <FATHER>02X_HBZ310</FATHER>
          <PART>02X_HBZ103</PART>
     </SPAREPARTS>
     <SPAREPARTS>
          <FATHER>02X_HBZ310</FATHER>
          <PART>13X_EFLB1303S2</PART>
     </SPAREPARTS>
  ...<SPAREPARTS> ...das wiederholt sich einige tausend male!
</PRODUCTS>
Mit Powershell muss ich bei jedem <SPAREPARTS> ein neues Child "<GROUP>XGROUP</GROUP>" hinzufügen - und zwar immer dasselbe!
Erklärung: Anschliessend gebe ich das ganze in ein CSV-file aus, wobei das neue Child dann die dritte Spalte bildet (mit immer demselben Wert).
So sollte das Ergebnis in der XML-Datei aussehen:
<?xml version="1.0" encoding="UTF-8"?>  
<PRODUCTS>
     <SPAREPARTS>
          <FATHER>02X_HBZ310</FATHER>
          <PART>02X_HBZ103</PART>
          <GROUP>XGROUP</GROUP>
     </SPAREPARTS>
     <SPAREPARTS>
          <FATHER>02X_HBZ310</FATHER>
          <PART>13X_EFLB1303S2</PART>
          <GROUP>XGROUP</GROUP>
     </SPAREPARTS>
  ...<SPAREPARTS> ...das wiederholt sich einige tausend male!
</PRODUCTS>
Das Problem ist, dass ich schon alles mögliche versucht hatte, das SOLL-Ergebnis aber nicht hinbekomme!!!
Der Teil des Scripts, welches das bewerkstelligen soll sieht derzeit so aus:
Curl $DownloadPath -o $XMLTempPath
[xml]$temp = Get-Content $XMLTempPath
$NewChild = $temp.CreateElement("XGROUP")  
$NewChild.InnerText = $XGroup
ForEach-Object ...???
Set-Content $XMLImpPath
Entweder ist mein ganzer Ansatz komplett falsch, aber zumindest habe ich bei Zeile 5 "ForEach-Object ...???" ein massives Problem von wegen keiner Ahnung wie das Syntaxmässig aussehen sollte damit das Child dann auch bei allen Elementen eingefügt wird!
Vielen Dank für Eure Hilfe!

Content-Key: 438260

Url: https://administrator.de/contentid/438260

Printed on: April 26, 2024 at 11:04 o'clock

Mitglied: 139374
Solution 139374 Apr 08, 2019 updated at 06:50:52 (UTC)
Goto Top
#......
#...
foreach($node in $temp.SelectNodes('/PRODUCTS/SPAREPARTS')){  
    $node.appendChild($newChild.Clone()) | out-null
}

Btw. Sowas würde ich viel effizienter mit XSLT abfackeln, das wäre dann auch ums x fache schneller, das wurde nämlich genau für sowas geschaffen .
Member: oliswiss
oliswiss Apr 08, 2019 updated at 08:25:48 (UTC)
Goto Top
Guten Morgen timeout,
vielen Dank! Damit werde ich aber folgendes gefragt:
"Cmdlet Set-Content an der Befehlspipelineposition 1
Geben Sie Werte für die folgenden Parameter an:
Value:
"
Gehe ich hier recht in der Annahme, dass die Var "$node" keinen Wert hat und PS diesen wissen möchte? Nein, vermutlich nicht!
Das Problem dürfte wahrscheinlich daran liegen, dass das obige Gesamtkonstrukt keine "Pipe" ist ... er fragt mich vermutlich danach, was er bei der letzten Zeile "Set-Content $XMLImpPath" speichern soll!?

Wie löse ich dieses Problem?

Zu Deinem Input bez. XSLT:
Mein Script erledigt insgesamt ganz verschiedene Aufgaben an einem Stück und zeitgesteuert. Mir steht ein Windows-Server zur Verfügung, der solche Jobs wenn immer möglich mit Bordmitteln erledigen können sollte. Powershell zu benutzen war aus dieser Sicht die Ideale Lösung, auch wenn ich vor zwei Wochen noch keine Ahnung davon hatte.
Einige solche PS-Scripts laufen schon aktiv und zeitgesteuert (ein Script pro XML-file was zu verarbeiten ist). Folgendes wird dabei immer erledigt:
1. XML-file online abholen (http oder ftp)
2. Fehler korrigieren (bspw. Leerzeichen entfernen, von UTF-16 nach UTF-8 konvertieren usw.)
3. Auf XML-Schema prüfen (gegen ein XSD-Schema-file)
4. wenn 3. OK > CSV generieren, ansonsten "STOP"
5. mittels anstossen einer executable mit params die CSV in SQL-Server importieren
6. alles wird pro Durchlauf in Log-files geschrieben ...

XSLT kenne ich nicht. Kann ich das denn innerhalb eines PS-Scripts verwenden? Würde ja Sinn machen wenns viel performanter ist.

Ganz einfach gesagt, ist die Grundsätzliche Aufgabe immer die folgende und allenfalls kennst Du ja bessere Variante als das mit PS zu machen?:
1. XML-File online holen
2. XML-File bearbeiten und evtl. ergänzen (wie das Child oben)
3. XML-File auf Schema prüfen
4. CSV generieren
5. EXE* mit Params ausführen um CSV in SQL zu importieren
  • Diese Applikation verwendet dann vorgefertigte Import-Templates ...

Bin gespannt was da jetzt von Dir kommt! Auf jeden Fall schon mal vielen Dank!
Member: oliswiss
oliswiss Apr 08, 2019 at 08:51:40 (UTC)
Goto Top
Ha, ich habs face-smile

Die Zeile für Speichern muss dann so aussehen:
Set-Content -Path $XMLImpPath -Value $temp.InnerXml
Mitglied: 139374
Solution 139374 Apr 08, 2019 updated at 10:36:24 (UTC)
Goto Top
Zitat von @oliswiss:

Ha, ich habs face-smile

Die Zeile für Speichern muss dann so aussehen:
Set-Content -Path $XMLImpPath -Value $temp.InnerXml
Es gibt ja die schöne Methode zum Speichern die extra dafür geschaffen wurde!
$temp.Save('d:\pfad\datei.xml')  
Set-Content ist hier unschön und fehleranfällig wenn im XML ein anderes Encoding enthalten ist und Set-Content es dann in einem anderen wegeschreibt. Die Save-Methode dagegen berücksichtigt das schon.

XSLT kenne ich nicht. Kann ich das denn innerhalb eines PS-Scripts verwenden?
Ja, wie das geht, findest du hier im Forum.
Ein passendes XSL Stylesheet für dein obiges Vorhaben ist dieses hier
<?xml version="1.0" encoding="UTF-8"?> 
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 
<xsl:template match="@*|node()"> 
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/> 
        </xsl:copy>
    </xsl:template>
<xsl:template match="/PRODUCTS/SPAREPARTS"> 
      <xsl:copy>
         <xsl:apply-templates select="@*|node()"/> 
         <GROUP>XGROUP</GROUP>
       </xsl:copy>
</xsl:template>
</xsl:stylesheet>
Kannst du hier selbst testen
https://www.freeformatter.com/xsl-transformer.html#ad-output

Uf Wiederluege
Mitglied: 139374
Solution 139374 Apr 08, 2019 at 14:47:29 (UTC)
Goto Top
Member: oliswiss
oliswiss Apr 08, 2019 at 15:30:16 (UTC)
Goto Top
you're right, thanks face-smile
Member: colinardo
Solution colinardo Apr 08, 2019 updated at 17:47:49 (UTC)
Goto Top
Servus @oliswiss,
Zitat von @139374:
Ja, wie das geht, findest du hier im Forum.
damit du dir das nicht zusammensuchen musst hier das ganze noch XSLT-Transformation direkt in der Powershell:

Für das Beispiel sind deine Daten (xml und xlst) hier direkt im Code hinterlegt (deswegen ist die Funktion mit MemoryStreams und Readers etwas aufgebläht), die können selbstverständlich auch aus Dateien kommen. Diese Funktion gibt das Resultat direkt als String aus.
function Transform-XML {
    param(
        [string]$stylesheet,[string]$xml
    )
    try{
        $xslt = New-Object system.xml.xsl.xslcompiledtransform
        $xsltstream = New-Object System.IO.MemoryStream
        $xmlstream = New-Object System.IO.MemoryStream
        $outputstream = New-Object System.IO.MemoryStream
        $xsltbuffer = [System.Text.Encoding]::UTF8.GetBytes($stylesheet)
        $xmlbuffer = [System.Text.Encoding]::UTF8.GetBytes($xml)

        $xsltstream.Write($xsltbuffer,0,$xsltbuffer.length)
        $xmlstream.Write($xmlbuffer,0,$xmlbuffer.length)
        $xsltstream.Position = 0
        $xmlstream.Position = 0

        $xsltreader = [System.Xml.XmlReader]::Create($xsltstream)
        $xmlreader = [System.Xml.XmlReader]::Create($xmlstream)
        $xslt.Load($xsltreader)
        $args = [System.Xml.Xsl.XsltArgumentList]::new()
        $xslt.Transform($xmlreader,$args,$outputstream)
        $outputstream.Position = 0
        $result = [System.IO.StreamReader]::new($outputstream).ReadToEnd()
        $xsltstream.Dispose()
        $xmlstream.Dispose()
        $xsltreader.Dispose()
        $xmlreader.Dispose()
        $outputstream.Dispose()
        return $result
    }catch{
        write-error $_.Exception
    }
}

$myxml = @'  
<?xml version="1.0" encoding="UTF-8"?>  
<PRODUCTS>
     <SPAREPARTS>
          <FATHER>02X_HBZ310</FATHER>
          <PART>02X_HBZ103</PART>
     </SPAREPARTS>
     <SPAREPARTS>
          <FATHER>02X_HBZ310</FATHER>
          <PART>13X_EFLB1303S2</PART>
     </SPAREPARTS>
</PRODUCTS>
'@  

$mystylesheet = @'  
<?xml version="1.0" encoding="UTF-8"?>  
<xsl:stylesheet version="1.0"  
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">  
<xsl:output method="xml" indent="yes"/>  
<xsl:strip-space elements="*"/>  
<xsl:template match="@*|node()">  
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>  
    </xsl:copy>
</xsl:template>
<xsl:template match="/PRODUCTS/SPAREPARTS">  
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>  
        <GROUP>XGROUP</GROUP>
    </xsl:copy>
</xsl:template>
</xsl:stylesheet>
'@  

Transform-XML -stylesheet $mystylesheet -xml $myxml

back-to-topErgebnis:
<?xml version="1.0" encoding="utf-8"?>
<PRODUCTS>
  <SPAREPARTS>
    <FATHER>02X_HBZ310</FATHER>
    <PART>02X_HBZ103</PART>
    <GROUP>XGROUP</GROUP>
  </SPAREPARTS>
  <SPAREPARTS>
    <FATHER>02X_HBZ310</FATHER>
    <PART>13X_EFLB1303S2</PART>
    <GROUP>XGROUP</GROUP>
  </SPAREPARTS>
</PRODUCTS>

Alternativ akzeptiert die folgende vereinfachte Funktion direkt die entsprechenden Dateien per Pfadangabe und gibt das Ergebnis ebenfalls direkt als Datei aus:
function Transform-XML{
    param(
        [string]$xsltfile,[string]$xmlfile,[string]$xmlfileout
    )
    try{
        $xslt = New-Object system.xml.xsl.xslcompiledtransform
        $xslt.Load($xsltfile)
        $xslt.Transform($xmlfile,$xmlfileout)
    }catch{
        throw $_.Exception.Message
    }
}

Transform-XML -xsltfile 'D:\transform.xslt' -xmlfile 'D:\test.xml' -xmlfileout 'D:\test_new.xml'  
Grüße Uwe