将动态 XML 转换为 CSV/Text

Transform dynamic XML to CSV/Text

我有一个 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 数据进行两次传递,第一次获取列名,第二次将数据添加到适当的字段中以输出到文本文件.

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

我最初的想法是使 1 通过 XML,将字段的名称属性添加到散列 table。在第 2 遍中,我将使用散列 table 循环遍历并将每个字段分配到输出的适当位置。

我现在正在使用它来阅读 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 
            }
        }
    }
}

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

我可能会这样做:

[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

正如您自己指出的那样,,这里最好的程序是:

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

话虽这么说,如果您正在处理巨大的 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
    }
}

现在我们可以使用第一个函数为 属性 table 创建一个模板,稍后我们可以将其与 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 步中的散列table结合起来,等等:

Get-XMLFieldValues -Template $objectTemplate |ft -AutoSize

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