【问题标题】:Transform dynamic XML to CSV/Text将动态 XML 转换为 CSV/文本
【发布时间】:2016-05-03 15:15:36
【问题描述】:

我有一个包含各种元素和属性的 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
我的名字-123456-测试|我的名字|测试|123456|管理员|-|||
myname-98765-dev|myname|dev|98765|||biography|false|-

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

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

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

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

有没有比我考虑的更好的方法来做到这一点?

【问题讨论】:

  • 这取决于“更好”对你意味着什么
  • @MathiasR.Jessen - 不幸的是,“更好”并不总是意味着它所说的。我希望在这种情况下,“更好”意味着更容易。但在现实世界中,“更好”通常意味着最不臭的选项。 ;)

标签: xml powershell export-to-csv


【解决方案1】:

正如你自己和Ansgar has already shown 所指出的,这里最好的程序是:

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

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

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

【讨论】:

  • 这完美!谢谢你的功能。也为了笑.... ;)
【解决方案2】:

我可能会这样做:

[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

【讨论】:

  • 在最后一行使用-Delimiter "|" 开关?
猜你喜欢
  • 2020-10-07
  • 2011-03-05
  • 2015-10-28
  • 2014-11-22
  • 2017-02-17
  • 2017-04-18
  • 2011-06-18
  • 2013-06-20
相关资源
最近更新 更多