将嵌套的JSON数组转换为CSV文件中的单独列

9

我有一个长这样的JSON文件:

{
    "id": 10011,
    "title": "Test procedure",
    "slug": "slug",
    "url": "http://test.test",
    "email": "test@test.com",
    "link": "http://test.er",
    "subject": "testing",
    "level": 1,
    "disciplines": [
      "discipline_a",
      "discipline_b",
      "discipline_c"
    ],
    "areas": [
      "area_a",
      "area_b"
    ]
  },

我试图使用以下命令将其转换为CSV文件:
(Get-Content "PATH_TO\test.json" -Raw | ConvertFrom-Json)| Convertto-CSV -NoTypeInformation | Set-Content "PATH_TO\test.csv"

然而,对于我正在处理的某些领域和区域,结果CSV文件中出现了System.Object[]。

是否有一种方法可以将所有这些嵌套值作为单独的列放入CSV文件中,例如area_1、area_2等。同样适用于学科。


也许这个链接会有帮助:https://dev59.com/lIvda4cB1Zd3GeqPX1EN - Anthony McGrath
在你的例子中,这个记录的“discipline_a”或“area_a”列会显示什么值? - andyb
@andyb 这些只是指定不同学科和领域的字符串。例如,areas数组可能包含“数学”,“化学”等。因此,我希望在生成的CSV中,列'area_1'包含“数学”,列'area_2'包含“化学”。'area_.'列的数量应由特定对象可能拥有的最大区域数确定。 - user2758935
2个回答

11
2017-11-20,完全重写了函数以提高性能并添加了-ArrayBase和对PSStandardMembers和分组对象的支持。

Flatten-Object

递归地展平包含数组、哈希表和(自定义)对象的对象。所提供的objects的所有添加属性将与其他对象对齐

需要PowerShell版本2或更高版本。

Cmdlet

Function Flatten-Object {                                       # Version 00.02.12, by iRon
    [CmdletBinding()]Param (
        [Parameter(ValueFromPipeLine = $True)][Object[]]$Objects,
        [String]$Separator = ".", [ValidateSet("", 0, 1)]$Base = 1, [Int]$Depth = 5, [Int]$Uncut = 1,
        [String[]]$ToString = ([String], [DateTime], [TimeSpan]), [String[]]$Path = @()
    )
    $PipeLine = $Input | ForEach {$_}; If ($PipeLine) {$Objects = $PipeLine}
    If (@(Get-PSCallStack)[1].Command -eq $MyInvocation.MyCommand.Name -or @(Get-PSCallStack)[1].Command -eq "<position>") {
        $Object = @($Objects)[0]; $Iterate = New-Object System.Collections.Specialized.OrderedDictionary
        If ($ToString | Where {$Object -is $_}) {$Object = $Object.ToString()}
        ElseIf ($Depth) {$Depth--
            If ($Object.GetEnumerator.OverloadDefinitions -match "[\W]IDictionaryEnumerator[\W]") {
                $Iterate = $Object
            } ElseIf ($Object.GetEnumerator.OverloadDefinitions -match "[\W]IEnumerator[\W]") {
                $Object.GetEnumerator() | ForEach -Begin {$i = $Base} {$Iterate.($i) = $_; $i += 1}
            } Else {
                $Names = If ($Uncut) {$Uncut--} Else {$Object.PSStandardMembers.DefaultDisplayPropertySet.ReferencedPropertyNames}
                If (!$Names) {$Names = $Object.PSObject.Properties | Where {$_.IsGettable} | Select -Expand Name}
                If ($Names) {$Names | ForEach {$Iterate.$_ = $Object.$_}}
            }
        }
        If (@($Iterate.Keys).Count) {
            $Iterate.Keys | ForEach {
                Flatten-Object @(,$Iterate.$_) $Separator $Base $Depth $Uncut $ToString ($Path + $_)
            }
        }  Else {$Property.(($Path | Where {$_}) -Join $Separator) = $Object}
    } ElseIf ($Objects -ne $Null) {
        @($Objects) | ForEach -Begin {$Output = @(); $Names = @()} {
            New-Variable -Force -Option AllScope -Name Property -Value (New-Object System.Collections.Specialized.OrderedDictionary)
            Flatten-Object @(,$_) $Separator $Base $Depth $Uncut $ToString $Path
            $Output += New-Object PSObject -Property $Property
            $Names += $Output[-1].PSObject.Properties | Select -Expand Name
        }
        $Output | Select ([String[]]($Names | Select -Unique))
    }
}; Set-Alias Flatten Flatten-Object

语法

<Object[]> Flatten-Object [-Separator <String>] [-Base "" | 0 | 1] [-Depth <Int>] [-Uncut<Int>] [ToString <Type[]>]

或者:

Flatten-Object <Object[]> [[-Separator] <String>] [[-Base] "" | 0 | 1] [[-Depth] <Int>] [[-Uncut] <Int>] [[ToString] <Type[]>]

参数

-Object[] <Object[]>
要展开的对象(或对象)。

-Separator <String> (默认值:.
递归属性名称之间使用的分隔符。

-Depth <Int> (默认值:5
展开递归属性的最大深度。任何负值都将导致无限深度,并可能导致无限循环。

-Uncut <Int> (默认值:1
将保留的对象迭代次数,进一步限制对象属性仅为DefaultDisplayPropertySet。任何负值都将显示所有对象的所有属性。

-Base "" | 0 | 1 (默认值:1
嵌套数组的第一个索引名称:

  • 1,数组将以1为基准: <Parent>.1<Parent>.2<Parent>.3,...
  • 0,数组将以0为基准: <Parent>.0<Parent>.1<Parent>.2,...
  • "",数组中的第一项将没有名称,然后以1开始命名: <Parent><Parent>.1<Parent>.2,...
-ToString <Type[]= [String], [DateTime], [TimeSpan]>
一个值类型的列表(默认为[String],[DateTime],[TimeSpan]),将被转换为字符串而不是进一步展开。例如,一个[DateTime]可以展开为额外的属性,如Date,Day,DayOfWeek等,但将被转换为单个(String)属性。
注意: 参数-Path仅供内部使用,但可以用于前缀属性名称。
示例:
回答具体问题:
(Get-Content "PATH_TO\test.json" -Raw | ConvertFrom-Json) | Flatten-Object | Convertto-CSV -NoTypeInformation | Set-Content "PATH_TO\test.csv"

结果:

{
    "url":  "http://test.test",
    "slug":  "slug",
    "id":  10011,
    "link":  "http://test.er",
    "level":  1,
    "areas.2":  "area_b",
    "areas.1":  "area_a",
    "disciplines.3":  "discipline_c",
    "disciplines.2":  "discipline_b",
    "disciplines.1":  "discipline_a",
    "subject":  "testing",
    "title":  "Test procedure",
    "email":  "test@test.com"
}

压力测试一个更复杂的自定义对象:
New-Object PSObject @{
    String    = [String]"Text"
    Char      = [Char]65
    Byte      = [Byte]66
    Int       = [Int]67
    Long      = [Long]68
    Null      = $Null
    Booleans  = $False, $True
    Decimal   = [Decimal]69
    Single    = [Single]70
    Double    = [Double]71
    Array     = @("One", "Two", @("Three", "Four"), "Five")
    HashTable = @{city="New York"; currency="Dollar"; postalCode=10021; Etc = @("Three", "Four", "Five")}
    Object    = New-Object PSObject -Property @{Name = "One";   Value = 1; Text = @("First", "1st")}
} | Flatten

结果:

Double               : 71
Decimal              : 69
Long                 : 68
Array.1              : One
Array.2              : Two
Array.3.1            : Three
Array.3.2            : Four
Array.4              : Five
Object.Name          : One
Object.Value         : 1
Object.Text.1        : First
Object.Text.2        : 1st
Int                  : 67
Byte                 : 66
HashTable.postalCode : 10021
HashTable.currency   : Dollar
HashTable.Etc.1      : Three
HashTable.Etc.2      : Four
HashTable.Etc.3      : Five
HashTable.city       : New York
Booleans.1           : False
Booleans.2           : True
String               : Text
Char                 : A
Single               : 70
Null                 :

扁平化分组对象:

$csv | Group Name | Flatten | Format-Table # https://stackoverflow.com/a/47409634/1701026

扁平化常见对象:

(Get-Process)[0] | Flatten-Object

或者对象列表(数组):

Get-Service | Flatten-Object -Depth 3 | Export-CSV Service.csv

请注意,以下命令需要几个小时来计算:

Get-Process | Flatten-Object | Export-CSV Process.csv

为什么?因为它会生成一个包含几百行和几千列的表格。所以,如果您想要使用此命令进行扁平化处理,最好限制行数(使用Where-Object cmdlet)或列数(使用Select-Object cmdlet)。


1
@ste_irl:从技术上讲,是的,但是您将会失去一些细节,因为您无法在扁平格式中区分对象和哈希表,此外,一个数组@("one", "two")和一个哈希表/对象@{'1' = 'one', '2' = 'two'}之间没有区别,还有一个asp.net将被展开为asp = @{net = @{...。无论如何,您可以尝试一下,如果遇到问题,请创建一个新的问题来寻求帮助。 - iRon
1
干得不错。请注意,在您的示例结果中,索引的顺序是错误的(例如,"areas.2""areas.1"之前),而且属性的顺序与输入不匹配(当我运行实际函数时,一切都按预期工作)。 - undefined
1
@mklement0,谢谢,实际上这是一个相当古老而尘封的脚本。我已经把它列入了修订清单。但我也在考虑更大的问题:对于这个函数(以及类似的函数,如ConvertTo-Expression和一个深度PSObject比较器),我正在考虑构建一个通用的PSObject迭代器类,通过标准方式递归迭代所有嵌套的数组/字典/对象结构,甚至拥有类似AST可用的Find/FindAll方法(假设这尚不存在)。 - undefined

4
CSV 转换/导出 cmdlet 没有"展平"对象的方法,我可能遗漏了一些内容,但是我知道没有内置 cmdlet 或功能可以实现这一点。如果您能保证“disclipines”和“areas”始终具有相同数量的元素,那么您可以使用派生属性与 Select-Object 来轻松处理它。
$properties=@('id','title','slug','url','email','link','subject','level',
    @{Name='discipline_1';Expression={$_.disciplines[0]}}
    @{Name='discipline_2';Expression={$_.disciplines[1]}}
    @{Name='discipline_3';Expression={$_.disciplines[2]}}
    @{Name='area_1';Expression={$_.areas[0]}}
    @{Name='area_2';Expression={$_.areas[1]}}
)
(Get-Content 'PATH_TO\test.json' -Raw | ConvertFrom-Json)| Select-Object -Property $properties | Export-CSV -NoTypeInformation -Path 'PATH_TO\test.csv'

然而,我假设对于每个记录,disciplinesareas的长度都是可变的。在这种情况下,您需要遍历输入并提取出disciplinesareas的最大计数值,然后动态构建属性数组:

$inputData = Get-Content 'PATH_TO\test.json' -Raw | ConvertFrom-Json
$counts = $inputData | Select-Object -Property     @{Name='disciplineCount';Expression={$_.disciplines.Count}},@{Name='areaCount';Expression={$_.areas.count}}
$maxDisciplines = $counts | Measure-Object -Maximum -Property disciplineCount | Select-Object -ExpandProperty     Maximum
$maxAreas = $counts | Measure-Object -Maximum -Property areaCount | Select-Object -ExpandProperty Maximum

$properties=@('id','title','slug','url','email','link','subject','level')

1..$maxDisciplines | % {
  $properties += @{Name="discipline_$_";Expression=[scriptblock]::create("`$_.disciplines[$($_ - 1)]")}
}

1..$maxAreas | % {
  $properties += @{Name="area_$_";Expression=[scriptblock]::create("`$_.areas[$($_ - 1)]")}
}

$inputData | Select-Object -Property $properties | Export-CSV -NoTypeInformation -Path 'PATH_TO\test.csv'

这段代码还没有经过完全测试,所以可能需要进行调整才能达到100%的工作效果,但我相信这些思路是很可靠的 =)


非常感谢!它就像魔法一样奏效。正是我所需要的!我使用了您提供的第二段代码。 - user2758935

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接