Ich verwende PowerShell, um Daten aus einem API-Aufruf zu extrahieren, zu aktualisieren und dann an die API zurückzugeben.
Was ich gerne wissen würde, ist, ob es eine einfache Möglichkeit gibt, das JSON-Objekt zu ändern, um alle Eigenschaften herauszufiltern, die an keiner Stelle innerhalb der JSON-Struktur erwünscht sind?
Ich habe Folgendes versucht, aber im resultierenden JSON wurden nur die niedrigstes Level-Eigenschaften entfernt (z. B. "p2")
$example = ConvertFrom-Json '{"a":{"p1": "value1"},"p2": "value2", "b":"valueb"}'
$exclude = "p1", "p2"
$clean = $example | Select-Object -Property * -ExcludeProperty $exclude
ConvertTo-Json $clean -Compress
Ergebnis => {"a":{"p1":"value1"},"b":"valueb"}
Ich möchte, dass alle $exlude
-Einträge entfernt werden, unabhängig davon, wo sie sich innerhalb des JSON befinden. Gibt es eine einfache Lösung?
Aktualisieren
Hier ist ein weiteres (komplizierteres) JSON-Beispiel:
{
"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"
}
Das erwartete Ergebnis (alle p1- und p2-Schlüssel entfernt):
{
"a": {
"c": "value c",
"d": {
"e": "value e"
},
"f": [
{
"g": "value ga"
},
{
"g": "value gb"
}
]
},
"b": "value b"
}
🤔 А знаете ли вы, что...
PowerShell поддерживает работу с REST API и веб-службами.
Leider scheint es keinen einfach-Weg zu geben. Es erwies sich tatsächlich als ziemlich herausfordernd, Arrays korrekt zu handhaben. Mein Ansatz besteht darin, das Eingabeobjekt (JSON) einschließlich aller Arrays rekursiv aufzurollen, damit wir problemlos Filter anwenden und dann ein neues Objekt aus den gefilterten Eigenschaften erstellen können.
Die Schritte eins und drei sind in die folgenden wiederverwendbaren Hilfsfunktionen verpackt, eine zum Entrollen (ConvertTo-FlatObjectValues
) und eine zum Neuaufbau des Objekts (ConvertFrom-FlatObjectValues
). Es gibt noch eine dritte Funktion (ConvertFrom-TreeHashTablesToArrays
), die aber nur intern von ConvertFrom-FlatObjectValues
verwendet wird.
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
}
}
}
Anwendungsbeispiel:
$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
Ausgabe:
{
"a": {
"c": "value c",
"d": {
"e": "value e"
},
"f": [
{
"g": "value ga"
},
{
"g": "value gb"
}
]
},
"b": "value b"
}
Nutzungshinweise:
-KeepEmptyObjects
und/oder -KeepEmptyArrays
an ConvertTo-FlatObjectValues
übergeben.ConvertTo-FlatObjectValues
übergeben, anstatt es zu leiten (was es entrollen würde und die Funktion nicht mehr wissen würde, dass es sich um ein Array handelt).P1
-Eigenschaft nur innerhalb des a
-Objekts zu entfernen, könnten Sie Where-Object Path -ne a.p1
schreiben. Um zu sehen, wie Pfade aussehen, rufen Sie einfach ConvertTo-FlatObjectValues $example
auf, was die flache Liste von Eigenschaften und Array-Elementen ausgibt:
Path Name Value
---- ---- -----
a.p1 p1 value 1
a.c c value c
a.d.e e value e
a.d.p2 p2 value 3
a.f.#0.g g value ga
a.f.#0.p1 p1 value 4a
a.f.#1.g g value gb
a.f.#1.p1 p1 value 4b
p2 p2 value 2
b b value b
Hinweise zur Implementierung:
Während des Entrollens erstellt ConvertTo-FlatObjectValues
separate Pfadsegmente (Schlüssel) für Array-Elemente, die wie „#n“ aussehen, wobei n der Array-Index ist. Dadurch können wir Arrays und Objekte einheitlicher behandeln, wenn wir das Objekt in ConvertFrom-FlatObjectValues
neu erstellen.
ConvertFrom-FlatObjectValues
erstellt zuerst verschachtelte Hashtabellen für alle Objekte und Arrays in seinem Abschnitt process
. Dies macht es einfach, Eigenschaften in ihren jeweiligen Objekten zu speichern. In diesem Teil des Codes gibt es noch keine spezielle Behandlung von Arrays. Das Zwischenergebnis sieht nun so aus:
{
"a": {
"c": "value c",
"d": {
"e": "value e"
},
"f": {
"#0": {
"g": "value ga"
},
"#1": {
"g": "value gb"
}
}
},
"b": "value b"
}
Nur im Abschnitt end
von ConvertFrom-FlatObjectValues
werden die Arrays aus den Hashtabellen neu erstellt, was durch die Funktion ConvertFrom-TreeHashTablesToArrays
erledigt wird. Es wandelt Hashtabellen, deren Schlüssel mit „#“ beginnen, wieder in tatsächliche Arrays um. Aufgrund der Filterung könnten die Indizes nicht fortlaufend sein, sodass wir einfach die Werte sammeln und die Indizes ignorieren könnten. Obwohl für den gegebenen Anwendungsfall nicht erforderlich, werden die Array-Indizes sortiert, um die Funktion robuster zu machen und Indizes zu unterstützen, die in beliebiger Reihenfolge empfangen werden.
Die Rekursion in PowerShell-Funktionen ist aufgrund des Parameterbindungs-Overheads vergleichsweise langsam. Wenn Leistung im Vordergrund steht, sollte der Code in Inline-C# neu geschrieben werden oder Datenstrukturen wie Collections.Queue
verwenden, um Rekursion zu vermeiden (auf Kosten der Lesbarkeit des Codes).