不幸的是,似乎没有easy条路.事实证明,正确处理数组非常具有挑战性.我的方法是递归地展开输入(JSON)对象,包括任何数组,这样我们就可以轻松地应用过滤,然后根据过滤后的属性构建一个新对象.
步骤1和3包含在以下可重用的辅助函数中,一个用于展开(ConvertTo-FlatObjectValues
),另一个用于重建对象(ConvertFrom-FlatObjectValues
).还有第三个函数(ConvertFrom-TreeHashTablesToArrays
),但它只在ConvertFrom-FlatObjectValues
内部使用.
Function ConvertTo-FlatObjectValues {
<#
.SYNOPSIS
Unrolls a nested PSObject/PSCustomObject "property bag".
.DESCRIPTION
Unrolls a nested PSObject/PSCustomObject "property bag" such as created by ConvertFrom-Json into flat objects consisting of path, name and value.
Fully supports arrays at the root as well as for properties and nested arrays.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)] $InputObject,
[string] $Separator = '.',
[switch] $KeepEmptyObjects,
[switch] $KeepEmptyArrays,
[string] $Path, # Internal parameter for recursion.
[string] $Name # Internal parameter for recursion.
)
process {
if( $InputObject -is [System.Collections.IList] ) {
if( $KeepEmptyArrays ) {
# Output a special item to keep empty array.
[PSCustomObject]@{
Path = ($Path, "#").Where{ $_ } -join $Separator
Name = $Name
Value = $null
}
}
$i = 0
$InputObject.ForEach{
# Recursively unroll array elements.
$childPath = ($Path, "#$i").Where{ $_ } -join $Separator
ConvertTo-FlatObjectValues -InputObject $_ -Path $childPath -Name $Name `
-Separator $Separator -KeepEmptyObjects:$KeepEmptyObjects -KeepEmptyArrays:$KeepEmptyArrays
$i++
}
}
elseif( $InputObject -is [PSObject] ) {
if( $KeepEmptyObjects ) {
# Output a special item to keep empty object.
[PSCustomObject]@{
Path = $Path
Name = $Name
Value = [ordered] @{}
}
}
$InputObject.PSObject.Properties.ForEach{
# Recursively unroll object properties.
$childPath = ($Path, $_.Name).Where{ $_ } -join $Separator
ConvertTo-FlatObjectValues -InputObject $_.Value -Path $childPath -Name $_.Name `
-Separator $Separator -KeepEmptyObjects:$KeepEmptyObjects -KeepEmptyArrays:$KeepEmptyArrays
}
}
else {
# Output scalar
[PSCustomObject]@{
Path = $Path
Name = $Name
Value = $InputObject
}
}
}
}
function ConvertFrom-FlatObjectValues {
<#
.SYNOPSIS
Convert a flat list consisting of path and value into tree(s) of PSCustomObject.
.DESCRIPTION
Convert a flat list consisting of path and value, such as generated by ConvertTo-FlatObjectValues, into tree(s) of PSCustomObject.
The output can either be an array (not unrolled) or a PSCustomObject, depending on the structure of the input data.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $Path,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)] [AllowNull()] $Value,
[Parameter()] [string] $Separator = '.'
)
begin {
$tree = [ordered]@{}
}
process {
# At first store everything (including array elements) into hashtables.
$branch = $Tree
do {
# Split path into root key and path remainder.
$key, $path = $path.Split( $Separator, 2 )
if( $path ) {
# We have multiple path components, so we may have to create nested hash table.
if( -not $branch.Contains( $key ) ) {
$branch[ $key ] = [ordered] @{}
}
# Enter sub tree.
$branch = $branch[ $key ]
}
else {
# We have arrived at the leaf -> set its value
$branch[ $key ] = $value
}
}
while( $path )
}
end {
# So far we have stored the original arrays as hashtables with keys like '#0', '#1', ... (possibly non-consecutive).
# Now convert these hashtables back into actual arrays and generate PSCustomObject's from the remaining hashtables.
ConvertFrom-TreeHashTablesToArrays $tree
}
}
Function ConvertFrom-TreeHashTablesToArrays {
<#
.SYNOPSIS
Internal function called by ConvertFrom-FlatObjectValues.
.DESCRIPTION
- Converts arrays stored as hashtables into actual arrays.
- Converts any remaining hashtables into PSCustomObject's.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)] [Collections.IDictionary] $InputObject
)
process {
# Check if $InputObject has been generated from an array.
$isArray = foreach( $key in $InputObject.Keys ) { $key.StartsWith('#'); break }
if( $isArray ) {
# Sort array indices as they might be unordered. A single '#' as key will be skipped, because it denotes an empty array.
$sortedByKeyNumeric = $InputObject.GetEnumerator().Where{ $_.Key -ne '#' } |
Sort-Object { [int]::Parse( $_.Key.SubString( 1 ) ) }
$outArray = $sortedByKeyNumeric.ForEach{
if( $_.Value -is [Collections.IDictionary] ) {
# Recursion. Output array element will either be an object or a nested array.
ConvertFrom-TreeHashTablesToArrays $_.Value
}
else {
# Output array element is a scalar value.
$_.Value
}
}
, $outArray # Comma-operator prevents unrolling of the array, to support nested arrays.
}
else {
# $InputObject has been generated from an object. Copy it to $outProps recursively and output as PSCustomObject.
$outProps = [ordered] @{}
$InputObject.GetEnumerator().ForEach{
$outProps[ $_.Key ] = if( $_.Value -is [Collections.IDictionary] ) {
# Recursion. Output property will either be an object or an array.
ConvertFrom-TreeHashTablesToArrays $_.Value
}
else {
# Output property is a scalar value.
$_.Value
}
}
[PSCustomObject] $outProps
}
}
}
Usage example:
$example = ConvertFrom-Json @'
{
"a": {
"p1": "value 1",
"c": "value c",
"d": {
"e": "value e",
"p2": "value 3"
},
"f": [
{
"g": "value ga",
"p1": "value 4a"
},
{
"g": "value gb",
"p1": "value 4b"
}
]
},
"p2": "value 2",
"b": "value b"
}
'@
$exclude = "p1", "p2"
$clean = ConvertTo-FlatObjectValues $example | # Step 1: unroll properties
Where-Object Name -notin $exclude | # Step 2: filter
ConvertFrom-FlatObjectValues # Step 3: rebuild object
$clean | ConvertTo-Json -Depth 9
Output:
{
"a": {
"c": "value c",
"d": {
"e": "value e"
},
"f": [
{
"g": "value ga"
},
{
"g": "value gb"
}
]
},
"b": "value b"
}
Usage Notes:
- 如果过滤后子对象不包含任何属性,则会将其删除.空数组也会被删除.您可以通过将
-KeepEmptyObjects
和/或-KeepEmptyArrays
传递给函数ConvertTo-FlatObjectValues
来防止这种情况.
- 如果输入JSON是根级别的数组,请确保将其作为参数传递给
ConvertTo-FlatObjectValues
,而不是管道传输(这样会展开它,函数将不再知道它是数组).
- 过滤也可以在属性的整个路径上进行.例如,要删除
a
对象中的P1
属性,可以写Where-Object Path -ne a.p1
.要查看路径的外观,只需调用ConvertTo-FlatObjectValues $example
,它将输出属性和数组元素的平面列表:
Implementation Notes:
在展开过程中,ConvertTo-FlatObjectValues
为看起来像"#n"的数组元素创建单独的路径段(键),其中n是数组索引.在ConvertFrom-FlatObjectValues
中重建对象时,这使我们能够更统一地处理数组和对象.
ConvertFrom-FlatObjectValues
首先为其process
部分中的所有对象和数组创建嵌套哈希表.这样可以很容易地将属性重新收集到各自的对象中.在这部分代码中,仍然没有对数组进行特殊处理.中间结果如下所示:
{
"a": {
"c": "value c",
"d": {
"e": "value e"
},
"f": {
"#0": {
"g": "value ga"
},
"#1": {
"g": "value gb"
}
}
},
"b": "value b"
}
只有在ConvertFrom-FlatObjectValues
的end
部分中,数组是从哈希表重建的,这是由函数ConvertFrom-TreeHashTablesToArrays
完成的.它将键以"#"开头的哈希表转换回实际array.由于过滤,索引可能是非连续的,因此我们可以只收集值而忽略索引.虽然对于给定的用例不是必需的,但数组索引将被排序,以使函数更健壮,并支持以任何顺序接收的索引.
由于参数绑定开销,PowerShell函数中的递归相对较慢.如果性能至关重要,那么代码应该用内联C#重写,或者使用像Collections.Queue
这样的数据 struct 来避免递归(以牺牲代码可读性为代价).