将动态 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
创建结构化对象
话虽这么说,如果您正在处理巨大的 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
我有一个 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 创建结构化对象
话虽这么说,如果您正在处理巨大的 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