将动态XML转换为CSV / Text

时间:2016-05-03 15:15:36

标签: xml powershell export-to-csv

我有一个包含各种元素的XML文件。属性。有些将是共同的,但不是每个节点都有所有(或相同)的节点。示例XML如下:

<?xml version='1.0' encoding='UTF-8'?>
<index>
    <doc id='0'>
        <field name='IDTREE' norm='124' flags='Idfp--S--Ni08--------'>
            <val>-</val>
        </field>
        <field name='role' norm='114' flags='Idfp--S--Ni08--------'>
            <val>administrators</val>
        </field>
        <field name='internalid' norm='117' flags='Idfp--S--Ni08--------'>
            <val>123456</val>
        </field>
        <field name='version' norm='124' flags='Idfp--S--Ni08--------'>
            <val>test</val>
        </field>
        <field name='id' norm='124' flags='Idfp--S--Ni08--------'>
            <val>myname-123456-test</val>
        </field>
        <field name='siteId' norm='124' flags='Idfp--S--Ni08--------'>
            <val>myname</val>
        </field>
    </doc>
    <doc id='1'>
        <field name='internalid' norm='117' flags='Idfp--S--Ni08--------'>
            <val>98765</val>
        </field>
        <field name='version' norm='124' flags='Idfp--S--Ni08--------'>
            <val>dev</val>
        </field>
        <field name='category' norm='113' flags='Idfp--S--Ni08--------'>
            <val>biography</val>
        </field>
        <field name='display' norm='120' flags='Idfp--S--Ni08--------'>
            <val>false</val>
        </field>
        <field name='publisher' norm='124' flags='Idfp--S--Ni08--------'>
            <val>-</val>
        </field>
        <field name='id' norm='124' flags='Idfp--S--Ni08--------'>
            <val>myname-98765-dev</val>
        </field>
        <field name='siteId' norm='124' flags='Idfp--S--Ni08--------'>
            <val>myname</val>
        </field>
    </doc>
</index>

我想要做的是将这个(非常大的)XML文件转换为可以导入Excel(或SQL)的文本文件(管道分隔)。我希望输出像:

id|siteId|version|internalid|role|IDTREE|category|display|publisher
myname-123456-test|myname|test|123456|administrators|-|||
myname-98765-dev|myname|dev|98765|||biography|false|-

我想我需要对XML数据进行2次传递,第一次获取列名称,第二次将数据添加到相应的字段以输出到文本文件。

我知道每个文档至少有4个相同的字段节点:id,siteId,version和internalid。其他一切都会有所不同。

我最初的想法是通过XML进行1次传递,将字段的名称属性添加到哈希表中。在第2遍,我将使用哈希表循环通过&amp;将每个字段分配到输出上的适当位置。

我现在正在使用它来读取XML文件。

$f = [System.Xml.XmlReader]::Create("C:\Test\MyXMLFile.xml")

while ($f.read()) {
    switch ($f.NodeType) {
        ([System.Xml.XmlNodeType]::Element) {
            if ($f.Name -eq "doc") {
                $e = [System.Xml.Linq.XElement]::ReadFrom($f)               
                $nbr = [String] $e.Attribute("id").Value
                $fields = $e.Descendants("field")
                foreach ($fld in $fields) {
                    $z = $fld.FirstAttribute.Value
                    $z1 = $fld.Element("val").Value
                }
                # write output 
            }
        }
    }
}

有没有比我考虑的方式更好的方法呢?

2 个答案:

答案 0 :(得分:3)

正如您自己指出的那样,Ansgar has already shown,最好的程序是:

  1. 遍历文件一次以查找所有可能的列名称
  2. 再次遍历文件并基于1
  3. 创建结构化对象

    话虽这么说,如果您正在使用HUGE xml文件,那么使用XmlReader的方法可能比解析整个文件更快,内存更少。

    我会简化您当前的代码,并将其拆分为两个相似但不同的操作。

    让我们从第1步开始,收集字段名称:

    # Import the XElement-to-XML linq assembly
    Add-Type -AssemblyName System.Xml.Linq |Out-Null
    
    function Get-FieldNames
    {
        param(
            [string]$Path = "C:\Test\MyXMLFile.xml",
            [switch]$AsHashTable
        )
    
        # Create reader
        $xmlReader = [System.Xml.xmlReader]::Create($Path)
    
        # Set up a dictionary
        $hashTable = [ordered]@{}
    
        # Read through the file
        while ($xmlReader.Read()) 
        {
            # Only interested in the <doc> elements
            if($xmlReader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $xmlReader.Name -eq "doc") 
            {
                $docElement = [System.Xml.Linq.XElement]::ReadFrom($xmlReader)
                foreach ($field in $docElement.Descendants("field")) 
                {
                    # Grab name of each field entry and set dictionary entry
                    $fieldName = $field.Attribute("name").Value
                    $hashTable[$fieldName] = $null
                }
            }
        }
    
        if($AsHashTable)
        {
            return $hashTable
        }
        else
        {
            return $hashTable.Keys
        }
    }
    

    现在我们可以使用第一个函数为属性表创建一个模板,以后我们可以使用New-Object -Property

    $objectTemplate = Get-FieldNames -AsHashTable
    

    大!全部设置为解析实际值。与以前完全相同的策略:

    function Get-XMLFieldValues 
    {
        param(
            [string]$Path = "C:\dev\test\huge.xml",
            [hashtable]$Template
        )
    
        # Create reader
        $xmlReader = [System.Xml.xmlReader]::Create($Path)
    
        # Read through the file
        while ($xmlReader.Read()) 
        {
            # Only interested in the <doc> elements
            if($xmlReader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $xmlReader.Name -eq "doc") 
            {
                $docElement = [System.Xml.Linq.XElement]::ReadFrom($xmlReader)
    
                # This is important - clone the template HashTable, don't reuse it
                $objectProperties = $Template.Clone()
    
                foreach ($field in $docElement.Descendants("field")) 
                {
                    # Grab name of the current field entry
                    $fieldName = $field.Attribute("name").Value
                    # Assign the value from the <val> child node
                    $objectProperties[$fieldName] = $($field.Descendants("val")|Select-Object -First 1).Value
                }
                # Create and emit a psobject
                New-Object psobject -Property $objectProperties
            }
        }
    }
    

    将它与步骤1中的哈希表相结合,等等:

    Get-XMLFieldValues -Template $objectTemplate |ft -AutoSize
    

    Ready to Export!

    现在您需要做的就是将输出传输到Export-Csv -Delimite '|'而不是Format-Table

答案 1 :(得分:1)

我可能会这样做:

[xml]$xml = Get-Content 'C:\Test\MyXMLFile.xml'

# transform XML to list of custom objects
$docs = $xml.SelectNodes('//doc') | ForEach-Object {
    $props = @{}
    $_.Field | ForEach-Object { $props[$_.name] = $_.val }
    New-Object -Type PSObject -Property $props
}

# get list of unique property names
$props = $docs | ForEach-Object {
    Get-Member -InputObject $_ -Type NoteProperty
} | Select-Object -Expand Name -Unique

# add missing properties to objects
$docs | ForEach-Object {
    $doc = $_
    $props | Where-Object {
        $_.PSObject.Properties.Name -notcontains $_
    } | ForEach-Object {
        $doc | Add-Member -Type NoteProperty -Name $_ -Value ''
    }
}

# export object list to CSV
$docs | Export-Csv 'C:\Test\MyXMLFile.csv' -Delimiter '|' -NoType