Condividi tramite


Che cosa sono le espressioni jq nell'anteprima del processore di dati IoT di Azure?

Importante

Anteprima delle operazioni di Azure IoT: abilitata da Azure Arc è attualmente disponibile in ANTEPRIMA. Non è consigliabile usare questo software di anteprima negli ambienti di produzione.

Vedere le condizioni per l'utilizzo supplementari per le anteprime di Microsoft Azure per termini legali aggiuntivi che si applicano a funzionalità di Azure in versione beta, in anteprima o in altro modo non ancora disponibili a livello generale.

Le espressioni jq consentono di eseguire calcoli e manipolazioni sui messaggi della pipeline di dati. Questa guida illustra i modelli di linguaggio e gli approcci per le esigenze comuni di calcolo ed elaborazione nelle pipeline di dati.

Suggerimento

Per provare gli esempi in questa guida, è possibile usare jq playground e incollare gli input e le espressioni di esempio nell'editor.

Nozioni fondamentali sul linguaggio

Se non si ha familiarità con jq come linguaggio, questa sezione nozioni fondamentali sul linguaggio fornisce alcune informazioni di base.

Programmazione funzionale

Il linguaggio jq è un linguaggio di programmazione funzionale. Ogni operazione accetta un input e produce un output. Più operazioni vengono combinate per eseguire logica complessa. Ad esempio, dato l'input seguente:

{
  "payload": {
    "temperature": 25
  }
}

Ecco una semplice espressione jq che specifica un percorso da recuperare:

.payload.temperature

Questo percorso è un'operazione che accetta un valore come input e restituisce un altro valore. In questo esempio il valore di output è 25.

Quando si lavora con operazioni complesse concatenati in jq, tenere presenti alcune considerazioni importanti:

  • Tutti i dati non restituiti da un'operazione non sono più disponibili nel resto dell'espressione. Esistono alcuni modi per aggirare questo vincolo, ma in generale è consigliabile considerare i dati necessari più avanti nell'espressione e impedire che vengano eliminati dalle operazioni precedenti.
  • Le espressioni sono considerate meglio come una serie di trasformazioni di dati anziché un set di calcoli da eseguire. Anche le operazioni come le assegnazioni sono solo una trasformazione del valore complessivo in cui un campo è stato modificato.

Tutto è un'espressione

Nella maggior parte dei linguaggi non funzionali esiste una distinzione tra due tipi di operazione:

  • Espressioni che producono un valore che può essere usato nel contesto di un'altra espressione.
  • Istruzioni che creano una forma di effetto collaterale anziché modificare direttamente un input e un output.

Con alcune eccezioni, tutto in jq è un'espressione. I cicli, le operazioni if/else e anche le assegnazioni sono tutte espressioni che producono un nuovo valore, anziché creare un effetto collaterale nel sistema. Ad esempio, dato l'input seguente:

{
  "temperature": 21,
  "humidity": 65
}

Se si vuole modificare il humidity campo in 63, è possibile usare un'espressione di assegnazione:

.humidity = 63

Anche se questa espressione sembra modificare l'oggetto di input, in jq sta producendo un nuovo oggetto con un nuovo valore per humidity:

{
  "temperature": 21,
  "humidity": 63
}

Questa differenza sembra sottile, ma significa che è possibile concatenare il risultato di questa operazione con altre operazioni usando |, come descritto più avanti.

Concatenare le operazioni con una pipe: |

L'esecuzione di calcoli e manipolazione dei dati in jq spesso richiede di combinare più operazioni. È possibile concatenare le operazioni inserendole | tra di esse. Ad esempio, per calcolare la lunghezza di una matrice di dati in un messaggio:

{
  "data": [5, 2, 4, 1]
}

Prima di tutto, isolare la parte del messaggio che contiene la matrice:

.data

Questa espressione offre solo la matrice:

[5, 2, 4, 1]

Usare quindi l'operazione length per calcolare la lunghezza di tale matrice:

length

Questa espressione offre la risposta:

4

Usare l'operatore | come separatore tra i passaggi, quindi, come singola espressione jq, il calcolo diventa:

.data | length

Se si sta tentando di eseguire una trasformazione complessa e non viene visualizzato un esempio che corrisponde esattamente al problema, è probabile che sia possibile risolvere il problema concatenando più soluzioni in questa guida con il | simbolo .

Input e argomenti della funzione

Una delle operazioni principali in jq chiama una funzione. Le funzioni in jq sono disponibili in molte forme e possono accettare diversi numeri di input. Gli input delle funzioni sono disponibili in due forme:

  • Contesto dati: i dati inseriti automaticamente nella funzione da jq. In genere i dati prodotti dall'operazione prima del simbolo più recente | .
  • Argomenti della funzione: altre espressioni e valori forniti per configurare il comportamento di una funzione.

Molte funzioni hanno zero argomenti ed eseguono tutte le operazioni usando il contesto dati fornito da jq. La length funzione è un esempio:

["a", "b", "c"] | length

Nell'esempio precedente l'input a length è la matrice creata a sinistra del | simbolo. La funzione non richiede altri input per calcolare la lunghezza della matrice di input. È possibile chiamare le funzioni con zero argomenti usando solo il nome. In altre parole, usare length, non length().

Alcune funzioni combinano il contesto dati con un singolo argomento per definirne il comportamento. Ad esempio, la map funzione :

[1, 2, 3] | map(. * 2)

Nell'esempio precedente l'input a map è la matrice di numeri creati a sinistra del | simbolo. La map funzione esegue un'espressione su ogni elemento della matrice di input. L'espressione viene fornita come argomento a map, in questo caso . * 2 per moltiplicare il valore di ogni voce nella matrice per 2 per restituire la matrice [2, 4, 6]. È possibile configurare qualsiasi comportamento interno desiderato con la funzione map.

Alcune funzioni accettano più argomenti. Queste funzioni funzionano allo stesso modo delle funzioni a argomento singolo e usano il ; simbolo per separare gli argomenti. Ad esempio, la sub funzione :

"Hello World" | sub("World"; "jq!")

Nell'esempio precedente la sub funzione riceve "Hello World" come contesto dati di input e quindi accetta due argomenti:

  • Espressione regolare da cercare nella stringa.
  • Stringa per sostituire qualsiasi sottostringa corrispondente. Separare gli argomenti con il ; simbolo . Lo stesso modello si applica alle funzioni con più di due argomenti.

Importante

Assicurarsi di usare ; come separatore di argomenti e non ,.

Usare oggetti

Esistono molti modi per estrarre dati da, modificare e costruire oggetti in jq. Le sezioni seguenti descrivono alcuni dei modelli più comuni:

Estrarre valori da un oggetto

Per recuperare le chiavi, in genere si usa un'espressione di percorso. Questa operazione viene spesso combinata con altre operazioni per ottenere risultati più complessi.

È facile recuperare i dati dagli oggetti . Quando è necessario recuperare molti dati da strutture non oggetto, un modello comune consiste nel convertire strutture non oggetto in oggetti. Con l'input seguente:

{
  "payload": {
    "values": {
      "temperature": 45,
      "humidity": 67
    }
  }
}

Usare l'espressione seguente per recuperare il valore di umidità:

.payload.values.humidity

Questa espressione genera l'output seguente:

67

Modificare le chiavi in un oggetto

Per rinominare o modificare le chiavi oggetto, è possibile usare la with_entries funzione . Questa funzione accetta un'espressione che opera sulle coppie chiave/valore di un oggetto e restituisce un nuovo oggetto con i risultati dell'espressione.

Nell'esempio seguente viene illustrato come rinominare il temp campo in per allinearlo temperature a uno schema downstream. Con l'input seguente:

{
  "payload": {
    "temp": 45,
    "humidity": 67
  }
}

Usare l'espressione seguente per rinominare il temp campo in temperature:

.payload |= with_entries(if .key == "temp" then .key = "temperature" else . end)

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • with_entries(<expression>) è una sintassi abbreviata per l'esecuzione di diverse operazioni insieme. Esegue le operazioni seguenti:
    • Accetta un oggetto come input e converte ogni coppia chiave/valore in una voce con struttura {"key": <key>, "value": <value>}.
    • Viene eseguito <expression> su ogni voce generata dall'oggetto , sostituendo il valore di input di tale voce con il risultato dell'esecuzione <expression>di .
    • Converte nuovamente il set di voci trasformato in un oggetto usando key come chiave nella coppia chiave/valore e value come valore della chiave.
  • if .key == "temp" then .key = "temperature" else . end esegue la logica condizionale sulla chiave della voce. Se la chiave viene temp quindi convertita in temperature modo da lasciare invariato il valore. Se la chiave non tempè , la voce viene lasciata invariata restituendo . dall'espressione.

Il codice JSON seguente mostra l'output dell'espressione precedente:

{
  "payload": {
    "temperature": 45,
    "humidity": 67
  }
}

Conversione di un oggetto in una matrice

Anche se gli oggetti sono utili per accedere ai dati, le matrici sono spesso più utili quando si desidera suddividere i messaggi o combinare in modo dinamico le informazioni. Utilizzare to_entries per convertire un oggetto in una matrice.

Nell'esempio seguente viene illustrato come convertire il payload campo in una matrice. Con l'input seguente:

{
  "id": "abc",
  "payload": {
    "temperature": 45,
    "humidity": 67
  }
}

Usare l'espressione seguente per convertire il campo payload in una matrice:

.payload | to_entries

Il codice JSON seguente è l'output dell'espressione jq precedente:

[
  {
    "key": "temperature",
    "value": 45
  },
  {
    "key": "humidity",
    "value": 67
  }
]

Suggerimento

Questo esempio estrae semplicemente la matrice e rimuove tutte le altre informazioni nel messaggio. Per mantenere il messaggio complessivo, ma scambiare la struttura di .payload in una matrice, usare .payload |= to_entries invece .

Creare oggetti

Si costruiscono oggetti usando una sintassi simile a JSON, in cui è possibile fornire una combinazione di informazioni statiche e dinamiche.

Nell'esempio seguente viene illustrato come ristrutturare completamente un oggetto creando un nuovo oggetto con campi rinominati e una struttura aggiornata. Con l'input seguente:

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 5, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Usare l'espressione jq seguente per creare un oggetto con la nuova struttura:

{
  payload: {
    humidity: .payload.Payload["dtmi:com:prod1:slicer3345:humidity"].Value,
    lineStatus: .payload.Payload["dtmi:com:prod1:slicer3345:lineStatus"].Value,
    temperature: .payload.Payload["dtmi:com:prod1:slicer3345:temperature"].Value
  },
  (.payload.DataSetWriterName): "active"
}

Nell'espressione jq precedente:

  • {payload: {<fields>}} crea un oggetto con un campo letterale denominato payload che è un oggetto letterale contenente più campi. Questo approccio è il modo più semplice per costruire oggetti.
  • humidity: .payload.Payload["dtmi:com:prod1:slicer3345:humidity"].Value, crea un nome di chiave statica con un valore calcolato in modo dinamico. Il contesto dati per tutte le espressioni all'interno della costruzione di oggetti è l'input completo per l'espressione di costruzione dell'oggetto, in questo caso il messaggio completo.
  • (.payload.DataSetWriterName): "active" è un esempio di chiave oggetto dinamico. In questo esempio viene eseguito il mapping del valore di .payload.DataSetWriterName a un valore statico. Usare chiavi e valori statici e dinamici in qualsiasi combinazione quando si crea un oggetto .

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "humidity": 10,
    "lineStatus": [1, 5, 2],
    "temperature": 46
  },
  "slicer-3345": "active"
}

Aggiungere campi a un oggetto

È possibile aumentare un oggetto aggiungendo campi per fornire un contesto aggiuntivo per i dati. Usare un'assegnazione a un campo che non esiste.

Nell'esempio seguente viene illustrato come aggiungere un averageVelocity campo al payload. Con l'input seguente:

{
  "payload": {
    "totalDistance": 421,
    "elapsedTime": 1598
  }
}

Usare l'espressione jq seguente per aggiungere un averageVelocity campo al payload:

.payload.averageVelocity = (.payload.totalDistance / .payload.elapsedTime)

A differenza di altri esempi che usano il |= simbolo , questo esempio usa un'assegnazione standard, =. Pertanto, non definisce l'ambito dell'espressione sul lato destro del campo a sinistra. Questo approccio è necessario in modo da poter accedere ad altri campi nel payload.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "totalDistance": 421,
    "elapsedTime": 1598,
    "averageVelocity": 0.2634543178973717
  }
}

Aggiungere campi in modo condizionale a un oggetto

La combinazione della logica condizionale con la sintassi per l'aggiunta di campi a un oggetto consente scenari come l'aggiunta di valori predefiniti per i campi non presenti.

Nell'esempio seguente viene illustrato come aggiungere un'unità alle misurazioni di temperatura che non ne hanno una. L'unità predefinita è celsius. Con l'input seguente:

{
  "payload": [
    {
      "timestamp": 1689712296407,
      "temperature": 59.2,
      "unit": "fahrenheit"
    },
    {
      "timestamp": 1689712399609,
      "temperature": 52.2
    },
    {
      "timestamp": 1689712400342,
      "temperature": 50.8,
      "unit": "celsius"
    }
  ]
}

Usare l'espressione jq seguente per aggiungere un'unità alle misurazioni di temperatura che non ne hanno una:

.payload |= map(.unit //= "celsius")

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • map(<expression>)<expression> viene eseguito su ogni voce della matrice e sostituisce il valore di input con qualsiasi prodotto<expression>.
  • .unit //= "celsius" utilizza l'assegnazione speciale //= . Questa assegnazione combina (=) con l'operatore alternativo (//) per assegnare il valore esistente di .unit a se stesso se non false è o null. Se .unit è false o null, l'espressione assegna "celsius" come valore di .unit, se .unit necessario.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": [
    {
      "timestamp": 1689712296407,
      "temperature": 59.2,
      "unit": "fahrenheit"
    },
    {
      "timestamp": 1689712399609,
      "temperature": 52.2,
      "unit": "celsius"
    },
    {
      "timestamp": 1689712400342,
      "temperature": 50.8,
      "unit": "celsius"
    }
  ]
}

Rimuovere campi da un oggetto

Usare la del funzione per rimuovere campi non necessari da un oggetto .

Nell'esempio seguente viene illustrato come rimuovere il timestamp campo perché non è rilevante per il resto del calcolo. Con l'input seguente:

{
  "payload": {
    "timestamp": "2023-07-18T20:57:23.340Z",
    "temperature": 153,
    "pressure": 923,
    "humidity": 24
  }
}

Usare l'espressione jq seguente rimuove il timestamp campo:

del(.payload.timestamp)

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "temperature": 153,
    "pressure": 923,
    "humidity": 24
  }
}

Usare le matrici

Le matrici sono il blocco predefinito principale per l'iterazione e la suddivisione dei messaggi in jq. Gli esempi seguenti illustrano come modificare le matrici.

Estrarre valori da una matrice

Le matrici sono più difficili da esaminare rispetto agli oggetti perché i dati possono trovarsi in indici diversi della matrice in messaggi diversi. Pertanto, per estrarre i valori da una matrice è spesso necessario cercare nella matrice i dati necessari.

L'esempio seguente illustra come estrarre alcuni valori da una matrice per creare un nuovo oggetto che contiene i dati a cui si è interessati. Con l'input seguente:

{
  "payload": {
    "data": [
      {
        "field": "dtmi:com:prod1:slicer3345:humidity",
        "value": 10
      },
      {
        "field": "dtmi:com:prod1:slicer3345:lineStatus",
        "value": [1, 5, 2]
      },
      {
        "field": "dtmi:com:prod1:slicer3345:speed",
        "value": 85
      },
      {
        "field": "dtmi:com:prod1:slicer3345:temperature",
        "value": 46
      }
    ],
    "timestamp": "2023-07-18T20:57:23.340Z"
  }
}

Usare l'espressione jq seguente per estrarre i timestampvalori , temperaturehumidity, e pressure dalla matrice per creare un nuovo oggetto:

.payload |= {
    timestamp,
    temperature: .data | map(select(.field == "dtmi:com:prod1:slicer3345:temperature"))[0]?.value,
    humidity: .data | map(select(.field == "dtmi:com:prod1:slicer3345:humidity"))[0]?.value,
    pressure: .data | map(select(.field == "dtmi:com:prod1:slicer3345:pressure"))[0]?.value,
}

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • {timestamp, <other-fields>} è abbreviato per timestamp: .timestamp, che aggiunge il timestamp come campo all'oggetto utilizzando il campo dello stesso nome dall'oggetto originale. <other-fields> aggiunge altri campi all'oggetto .
  • temperature: <expression>, humidity: <expression>, pressure: <expression> impostare temperatura, umidità e pressione nell'oggetto risultante in base ai risultati delle tre espressioni.
  • .data | <expression> definisce l'ambito del calcolo del valore nella data matrice del payload ed esegue <expression> nella matrice.
  • map(<expression>)[0]?.value esegue diverse operazioni:
    • map(<expression>)<expression> viene eseguito su ogni elemento della matrice restituendo il risultato dell'esecuzione di tale espressione su ogni elemento.
    • [0] estrae il primo elemento della matrice risultante.
    • ? abilita un ulteriore concatenamento di un segmento di percorso, anche se il valore precedente è Null. Quando il valore precedente è Null, il percorso successivo restituisce anche null anziché non ha esito negativo.
    • .value estrae il value campo dal risultato.
  • select(.field == "dtmi:com:prod1:slicer3345:temperature") esegue l'espressione booleana all'interno di select() rispetto all'input. Se il risultato è true, l'input viene passato. Se il risultato è false, l'input viene eliminato. map(select(<expression>)) è una combinazione comune usata per filtrare gli elementi in una matrice.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "timestamp": "2023-07-18T20:57:23.340Z",
    "temperature": 46,
    "humidity": 10,
    "pressure": null
  }
}

Modificare le voci della matrice

Modificare le voci in una matrice con un'espressione map() . Usare queste espressioni per modificare ogni elemento della matrice.

Nell'esempio seguente viene illustrato come convertire il timestamp di ogni voce della matrice da un millisecondo unix a una stringa RFC3339. Con l'input seguente:

{
  "payload": [
    {
      "field": "humidity",
      "timestamp": 1689723806615,
      "value": 10
    },
    {
      "field": "lineStatus",
      "timestamp": 1689723849747,
      "value": [1, 5, 2]
    },
    {
      "field": "speed",
      "timestamp": 1689723868830,
      "value": 85
    },
    {
      "field": "temperature",
      "timestamp": 1689723880530,
      "value": 46
    }
  ]
}

Usare l'espressione jq seguente per convertire il timestamp di ogni voce nella matrice da un millisecondo unix a una stringa RFC3339:

.payload |= map(.timestamp |= (. / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ")))

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • map(<expression>)<expression> viene eseguito su ogni elemento della matrice, sostituendo ognuno con l'output dell'esecuzione <expression>di .
  • .timestamp |= <expression> imposta timestamp su un nuovo valore in base all'esecuzione <expression>di , dove il contesto dei dati per <expression> è il valore di .timestamp.
  • (. / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ")) converte il tempo di millisecondo in secondi e usa un formattatore di stringhe temporali per produrre un timestamp ISO 8601.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": [
    {
      "field": "humidity",
      "timestamp": "2023-07-18T23:43:26Z",
      "value": 10
    },
    {
      "field": "lineStatus",
      "timestamp": "2023-07-18T23:44:09Z",
      "value": [1, 5, 2]
    },
    {
      "field": "speed",
      "timestamp": "2023-07-18T23:44:28Z",
      "value": 85
    },
    {
      "field": "temperature",
      "timestamp": "2023-07-18T23:44:40Z",
      "value": 46
    }
  ]
}

Conversione di una matrice in un oggetto

Per ristrutturare una matrice in un oggetto in modo che sia più semplice accedere o conformarsi a uno schema desiderato, usare from_entries. Con l'input seguente:

{
  "payload": [
    {
      "field": "humidity",
      "timestamp": 1689723806615,
      "value": 10
    },
    {
      "field": "lineStatus",
      "timestamp": 1689723849747,
      "value": [1, 5, 2]
    },
    {
      "field": "speed",
      "timestamp": 1689723868830,
      "value": 85
    },
    {
      "field": "temperature",
      "timestamp": 1689723880530,
      "value": 46
    }
  ]
}

Usare l'espressione jq seguente per convertire la matrice in un oggetto :

.payload |= (
    map({key: .field, value: {timestamp, value}})
    | from_entries
)

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • map({key: <expression>, value: <expression>}) converte ogni elemento della matrice in un oggetto del formato {"key": <data>, "value": <data>}, ovvero la struttura from_entries necessaria.
  • {key: .field, value: {timestamp, value}} crea un oggetto da una voce di matrice, il mapping field alla chiave e la creazione di un valore che contiene un oggetto che contiene timestamp e value. {timestamp, value} è abbreviato per {timestamp: .timestamp, value: .value}.
  • <expression> | from_entries converte una matrice con <expression> valori di matrice in un oggetto, mappando il key campo di ogni voce di matrice alla chiave dell'oggetto e il value campo di ogni voce di matrice al valore della chiave.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "humidity": {
      "timestamp": 1689723806615,
      "value": 10
    },
    "lineStatus": {
      "timestamp": 1689723849747,
      "value": [1, 5, 2]
    },
    "speed": {
      "timestamp": 1689723868830,
      "value": 85
    },
    "temperature": {
      "timestamp": 1689723880530,
      "value": 46
    }
  }
}

Creare matrici

La creazione di valori letterali di matrice è simile alla creazione di valori letterali oggetto. La sintassi jq per un valore letterale di matrice è simile a JSON e JavaScript.

Nell'esempio seguente viene illustrato come estrarre alcuni valori in una matrice semplice per un'elaborazione successiva.

Con l'input seguente:

{
  "payload": {
    "temperature": 14,
    "humidity": 56,
    "pressure": 910
  }
}

Usare l'espressione jq seguente crea una matrice dai valori dei temperaturecampi , humiditye pressure :

.payload |= ([.temperature, .humidity, .pressure])

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": [14, 56, 910]
}

Aggiungere voci a una matrice

È possibile aggiungere voci all'inizio o alla fine di una matrice usando l'operatore + con la matrice e le relative nuove voci. L'operatore += semplifica questa operazione aggiornando automaticamente la matrice con le nuove voci alla fine. Con l'input seguente:

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 5, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Usare l'espressione jq seguente per aggiungere i valori e 41 alla fine della lineStatus matrice di valori12:

.payload.Payload["dtmi:com:prod1:slicer3345:lineStatus"].Value += [12, 41]

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 5, 2, 12, 41]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Rimuovere voci da una matrice

Usare la del funzione per rimuovere le voci da una matrice nello stesso modo di un oggetto . Con l'input seguente:

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 5, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Usare l'espressione jq seguente per rimuovere la seconda voce dalla matrice di lineStatus valori:

del(.payload.Payload["dtmi:com:prod1:slicer3345:lineStatus"].Value[1])

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Rimuovere voci di matrice duplicate

Se gli elementi della matrice si sovrappongono, è possibile rimuovere le voci duplicate. Nella maggior parte dei linguaggi di programmazione è possibile rimuovere i duplicati usando le variabili di ricerca lato. In jq, l'approccio migliore consiste nell'organizzare i dati in come devono essere elaborati, quindi eseguire tutte le operazioni prima di convertirli nel formato desiderato.

Nell'esempio seguente viene illustrato come accettare un messaggio con alcuni valori e quindi filtrarlo in modo che sia disponibile solo la lettura più recente per ogni valore. Con l'input seguente:

{
  "payload": [
    {
      "name": "temperature",
      "value": 12,
      "timestamp": 1689727870701
    },
    {
      "name": "humidity",
      "value": 51,
      "timestamp": 1689727944440
    },
    {
      "name": "temperature",
      "value": 15,
      "timestamp": 1689727994085
    },
    {
      "name": "humidity",
      "value": 25,
      "timestamp": 1689727914558
    },
    {
      "name": "temperature",
      "value": 31,
      "timestamp": 1689727987072
    }
  ]
}

Usare l'espressione jq seguente per filtrare l'input in modo che sia disponibile solo la lettura più recente per ogni valore:

.payload |= (group_by(.name) | map(sort_by(.timestamp)[-1]))

Suggerimento

Se non ci si preoccupa di recuperare il valore più recente per ogni nome, è possibile semplificare l'espressione in .payload |= unique_by(.name)

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • group_by(.name) dato una matrice come input, inserisce gli elementi in sottoarray in base al valore di .name in ogni elemento. Ogni sottomaschera contiene tutti gli elementi della matrice originale con lo stesso valore di .name.
  • map(<expression>) accetta la matrice di matrici prodotte da group_by ed esegue <expression> su ogni sottoarray.
  • sort_by(.timestamp)[-1] estrae l'elemento di cui ci si occupa da ogni sottomaschera:
    • sort_by(.timestamp) ordina gli elementi aumentando il valore del relativo .timestamp campo per la sottomaschera corrente.
    • [-1] recupera l'ultimo elemento dalla sottomaschera ordinata, ovvero la voce con l'ora più recente per ogni nome.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": [
    {
      "name": "humidity",
      "value": 51,
      "timestamp": 1689727944440
    },
    {
      "name": "temperature",
      "value": 15,
      "timestamp": 1689727994085
    }
  ]
}

Calcolare i valori tra gli elementi della matrice

È possibile combinare i valori degli elementi della matrice per calcolare valori come le medie tra gli elementi.

In questo esempio viene illustrato come ridurre la matrice recuperando il timestamp più alto e il valore medio per le voci che condividono lo stesso nome. Con l'input seguente:

{
  "payload": [
    {
      "name": "temperature",
      "value": 12,
      "timestamp": 1689727870701
    },
    {
      "name": "humidity",
      "value": 51,
      "timestamp": 1689727944440
    },
    {
      "name": "temperature",
      "value": 15,
      "timestamp": 1689727994085
    },
    {
      "name": "humidity",
      "value": 25,
      "timestamp": 1689727914558
    },
    {
      "name": "temperature",
      "value": 31,
      "timestamp": 1689727987072
    }
  ]
}

Usare l'espressione jq seguente per recuperare il timestamp più alto e il valore medio per le voci che condividono lo stesso nome:

.payload |= (group_by(.name) | map(
  {
    name: .[0].name,
    value: map(.value) | (add / length),
    timestamp: map(.timestamp) | max
  }
))

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • group_by(.name) accetta una matrice come input, inserisce gli elementi in sottoarray in base al valore di .name in ogni elemento. Ogni sottomaschera contiene tutti gli elementi della matrice originale con lo stesso valore di .name.
  • map(<expression>) accetta la matrice di matrici prodotte da group_by ed esegue <expression> su ogni sottoarray.
  • {name: <expression>, value: <expression>, timestamp: <expression>} costruisce un oggetto dalla sottomaschera di input con namei campi , valuee timestamp . Ognuno <expression> produce il valore desiderato per la chiave associata.
  • .[0].name recupera il primo elemento dalla sottomaschera ed estrae il name campo da esso. Tutti gli elementi nella sottomaschera hanno lo stesso nome, quindi è sufficiente recuperare il primo.
  • map(.value) | (add / length) calcola la media value di ogni sottomaschera:
    • map(.value) converte la sottomaschera in una matrice del value campo in ogni voce, in questo caso restituendo una matrice di numeri.
    • add è una funzione jq predefinita che calcola la somma di una matrice di numeri.
    • length è una funzione jq predefinita che calcola il conteggio o la lunghezza di una matrice.
    • add / length divide la somma per il conteggio per determinare la media.
  • map(.timestamp) | max trova il valore massimo timestamp di ogni sottomaschera:
    • map(.timestamp) converte la sottomaschera in una matrice dei timestamp campi in ogni voce, in questo caso restituendo una matrice di numeri.
    • max è una funzione jq predefinita che trova il valore massimo in una matrice.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": [
    {
      "name": "humidity",
      "value": 38,
      "timestamp": 1689727944440
    },
    {
      "name": "temperature",
      "value": 19.333333333333332,
      "timestamp": 1689727994085
    }
  ]
}

Usare le stringhe

jq fornisce diverse utilità per la modifica e la costruzione di stringhe. Negli esempi seguenti vengono illustrati alcuni casi d'uso comuni.

Suddivisione di stringhe

Se una stringa contiene più informazioni separate da un carattere comune, è possibile usare la split() funzione per estrarre i singoli pezzi.

Nell'esempio seguente viene illustrato come suddividere una stringa di argomento e restituire un segmento specifico dell'argomento. Questa tecnica è spesso utile quando si usano espressioni di chiave di partizione. Con l'input seguente:

{
  "systemProperties": {
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345/tags/rpm",
  "properties": {
    "contentType": "application/json"
  },
  "payload": {
    "Timestamp": 1681926048,
    "Value": 142
  }
}

Usare l'espressione jq seguente per suddividere la stringa dell'argomento, usando / come separatore e restituire un segmento specifico dell'argomento:

.topic | split("/")[1]

Nell'espressione jq precedente:

  • .topic | <expression> seleziona la topic chiave dall'oggetto radice e viene eseguita <expression> sui dati contenuti.
  • split("/") suddivide la stringa dell'argomento in una matrice suddividendo la stringa a parte ogni volta che trova / il carattere nella stringa. In questo caso, produce ["assets", "slicer-3345", "tags", "rpm"].
  • [1] recupera l'elemento in corrispondenza dell'indice 1 della matrice dal passaggio precedente, in questo caso slicer-3345.'

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

"slicer-3345"

Costruire stringhe in modo dinamico

jq consente di costruire stringhe usando modelli di stringa con la sintassi \(<expression>) all'interno di una stringa. Usare questi modelli per creare stringhe in modo dinamico.

Nell'esempio seguente viene illustrato come aggiungere un prefisso a ogni chiave di un oggetto usando modelli di stringa. Con l'input seguente:

{
  "temperature": 123,
  "humidity": 24,
  "pressure": 1021
}

Usare l'espressione jq seguente per aggiungere un prefisso a ogni chiave nell'oggetto :

with_entries(.key |= "current-\(.)")

Nell'espressione jq precedente:

  • with_entries(<expression>) converte l'oggetto in una matrice di coppie chiave/valore con la struttura {key: <key>, value: <value>}, viene eseguito <expression> in ogni coppia chiave/valore e converte nuovamente le coppie in un oggetto .
  • .key |= <expression> aggiorna il valore di .key nell'oggetto coppia chiave/valore al risultato di <expression>. Se si usa |= invece di = impostare il contesto dati di <expression> sul valore di .key, anziché sull'oggetto coppia chiave/valore completo.
  • "current-\(.)" produce una stringa che inizia con "current-" e quindi inserisce il valore del contesto .di dati corrente, in questo caso il valore della chiave. La \(<expression>) sintassi all'interno della stringa indica che si vuole sostituire tale parte della stringa con il risultato dell'esecuzione <expression>di .

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "current-temperature": 123,
  "current-humidity": 24,
  "current-pressure": 1021
}

Usare espressioni regolari

jq supporta espressioni regolari standard. È possibile usare espressioni regolari per estrarre, sostituire e controllare i criteri all'interno delle stringhe. Le funzioni di espressione regolare comuni per jq includono test(), split()match(), capture(), sub(), e gsub().

Estrarre valori usando espressioni regolari

Se non è possibile usare la suddivisione di stringhe per estrarre un valore da una stringa, provare a usare espressioni regolari per estrarre i valori necessari.

Nell'esempio seguente viene illustrato come normalizzare le chiavi oggetto testando un'espressione regolare e quindi sostituendola con un formato diverso. Con l'input seguente:

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Usare l'espressione jq seguente per normalizzare le chiavi dell'oggetto:

.payload.Payload |= with_entries(
    .key |= if test("^dtmi:.*:(?<tag>[^:]+)$") then
        capture("^dtmi:.*:(?<tag>[^:]+)$").tag
    else
        .
    end
)

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • with_entries(<expression>) converte l'oggetto in una matrice di coppie chiave/valore con la struttura {key: <key>, value: <value>}, viene eseguito <expression> in ogni coppia chiave/valore e converte nuovamente le coppie in un oggetto .
  • .key |= <expression> aggiorna il valore di .key nell'oggetto coppia chiave/valore al risultato di <expression>. utilizzando |= invece di = impostare il contesto dati di <expression> sul valore di .key, anziché sull'oggetto coppia chiave/valore completo.
  • if test("^dtmi:.*:(?<tag>[^:]+)$") then capture("^dtmi:.*:(?<tag>[^:]+)$").tag else . end controlla e aggiorna la chiave in base a un'espressione regolare:
    • test("^dtmi:.*:(?<tag>[^:]+)$") controlla il contesto dei dati di input, la chiave in questo caso, rispetto all'espressione ^dtmi:.*:(?<tag>[^:]+)$regolare . Se l'espressione regolare corrisponde, restituisce true. In caso contrario, restituisce false.
    • capture("^dtmi:.*:(?<tag>[^:]+)$").tag esegue l'espressione ^dtmi:.*:(?<tag>[^:]+)$ regolare sul contesto dei dati di input, la chiave in questo caso e inserisce eventuali gruppi di acquisizione dall'espressione regolare, indicati da (?<tag>...), in un oggetto come output. L'espressione estrae .tag quindi da tale oggetto per restituire le informazioni estratte dall'espressione regolare.
    • .else nel ramo , l'espressione passa i dati attraverso non modificato.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Divisione dei messaggi

Una funzionalità utile del linguaggio jq è la possibilità di produrre più output da un singolo input. Questa funzionalità consente di suddividere i messaggi in più messaggi separati per l'elaborazione della pipeline. La chiave di questa tecnica è .[], che suddivide le matrici in valori separati. Negli esempi seguenti vengono illustrati alcuni scenari che usano questa sintassi.

Numero dinamico di output

In genere, quando si desidera suddividere un messaggio in più output, il numero di output desiderati dipende dalla struttura del messaggio. La [] sintassi consente di eseguire questo tipo di suddivisione.

Ad esempio, si dispone di un messaggio con un elenco di tag da inserire in messaggi separati. Con l'input seguente:

{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "sourceTimestamp": 1681926048,
        "value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "sourceTimestamp": 1681926048,
        "value": [1, 5, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "sourceTimestamp": 1681926048,
        "value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "sourceTimestamp": 1681926048,
        "value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Usare l'espressione jq seguente per suddividere il messaggio in più messaggi:

.payload.Payload = (.payload.Payload | to_entries[])
| .payload |= {
  DataSetWriterName,
  SequenceNumber,
  Tag: .Payload.key,
  Value: .Payload.value.value,
  Timestamp: .Payload.value.sourceTimestamp
}

Nell'espressione jq precedente:

  • .payload.Payload = (.payload.Payload | to_entries[]) suddivide il messaggio in diversi messaggi:
    • .payload.Payload = <expression> assegna il risultato dell'esecuzione <expression> a .payload.Payload. In genere, si usa |= in questo caso per definire l'ambito del contesto di <expression> fino a .payload.Payload, ma |= non supporta la suddivisione del messaggio a parte, quindi usare = invece .
    • (.payload.Payload | <expression>) definisce l'ambito del lato destro dell'espressione di assegnazione in .payload.Payload modo che <expression> funzioni sulla parte corretta del messaggio.
    • to_entries[] è due operazioni ed è una sintassi abbreviata per to_entries | .[]:
      • to_entries converte l'oggetto in una matrice di coppie chiave/valore con lo schema {"key": <key>, "value": <value>}. Queste informazioni sono ciò che si vuole separare in messaggi diversi.
      • [] esegue la suddivisione del messaggio. Ogni voce della matrice diventa un valore separato in jq. Quando si verifica l'assegnazione a .payload.Payload , ogni valore separato restituisce una copia del messaggio complessivo creato, con .payload.Payload impostato sul valore corrispondente prodotto dal lato destro dell'assegnazione.
  • .payload |= <expression> sostituisce il valore di .payload con il risultato dell'esecuzione <expression>di . A questo punto, la query si occupa di un flusso di valori anziché di un singolo valore in seguito alla suddivisione nell'operazione precedente. Pertanto, l'assegnazione viene eseguita una sola volta per ogni messaggio che l'operazione precedente produce anziché eseguirsi una sola volta nel complesso.
  • {DataSetWriterName, SequenceNumber, ...} costruisce un nuovo oggetto che rappresenta il valore di .payload. DataSetWriterName e SequenceNumber sono invariati, quindi è possibile usare la sintassi abbreviata anziché scrivere DataSetWriterName: .DataSetWriterName e SequenceNumber: .SequenceNumber.
  • Tag: .Payload.key, estrae la chiave dell'oggetto originale dai livelli interni Payload e superiori all'oggetto padre. L'operazione to_entries precedente nella query ha creato il key campo.
  • Value: .Payload.value.value ed Timestamp: .Payload.value.sourceTimestamp eseguire un'estrazione simile di dati dal payload interno. Questa volta dal valore della coppia chiave/valore originale. Il risultato è un oggetto payload flat che è possibile usare in un'ulteriore elaborazione.

Il codice JSON seguente mostra gli output dell'espressione jq precedente. Ogni output diventa un messaggio autonomo per le fasi di elaborazione successive nella pipeline:

{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092,
    "Tag": "dtmi:com:prod1:slicer3345:humidity",
    "Value": 10,
    "Timestamp": 1681926048
  }
}
{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092,
    "Tag": "dtmi:com:prod1:slicer3345:lineStatus",
    "Value": [1, 5, 2],
    "Timestamp": 1681926048
  }
}
{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092,
    "Tag": "dtmi:com:prod1:slicer3345:speed",
    "Value": 85,
    "Timestamp": 1681926048
  }
}
{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092,
    "Tag": "dtmi:com:prod1:slicer3345:temperature",
    "Value": 46,
    "Timestamp": 1681926048
  }
}

Numero fisso di output

Per suddividere un messaggio in un numero fisso di output anziché in un numero dinamico di output in base alla struttura del messaggio, usare l'operatore , anziché [].

Nell'esempio seguente viene illustrato come suddividere i dati in due messaggi in base ai nomi di campo esistenti. Con l'input seguente:

{
  "topic": "test/topic",
  "payload": {
    "minTemperature": 12,
    "maxTemperature": 23,
    "minHumidity": 52,
    "maxHumidity": 92
  }
}

Usare l'espressione jq seguente per suddividere il messaggio in due messaggi:

.payload = (
  {
    field: "temperature",
    minimum: .payload.minTemperature,
    maximum: .payload.maxTemperature
  },
  {
    field: "humidity",
    minimum: .payload.minHumidity,
    maximum: .payload.maxHumidity
  }
)

Nell'espressione jq precedente:

  • .payload = ({<fields>},{<fields>}) assegna i due valori letterali oggetto a .payload nel messaggio. Gli oggetti delimitati da virgole producono due valori separati e vengono assegnati in .payload, il che determina la suddivisione dell'intero messaggio in due messaggi. Ogni nuovo messaggio è .payload impostato su uno dei valori.
  • {field: "temperature", minimum: .payload.minTemperature, maximum: .payload.maxTemperature} è un costruttore di oggetti letterali che popola i campi di un oggetto con una stringa letterale e altri dati recuperati dall'oggetto .

Il codice JSON seguente mostra gli output dell'espressione jq precedente. Ogni output diventa un messaggio autonomo per ulteriori fasi di elaborazione:

{
  "topic": "test/topic",
  "payload": {
    "field": "temperature",
    "minimum": 12,
    "maximum": 23
  }
}
{
  "topic": "test/topic",
  "payload": {
    "field": "humidity",
    "minimum": 52,
    "maximum": 92
  }
}

Operazioni matematiche

jq supporta operazioni matematiche comuni. Alcune operazioni sono operatori come + e -. Altre operazioni sono funzioni come sin e exp.

Aritmetico

jq supporta cinque operazioni aritmetiche comuni: addizione (+), sottrazione (-), moltiplicazione (*), divisione (/) e modulo (%). A differenza di molte funzionalità di jq, queste operazioni sono operazioni di prefisso che consentono di scrivere l'espressione matematica completa in una singola espressione senza | separatori.

L'esempio seguente illustra come convertire una temperatura da fahrenheit a celsius ed estrarre i secondi correnti letti da un timestamp di millisecondo unix. Con l'input seguente:

{
  "payload": {
    "temperatureF": 94.2,
    "timestamp": 1689766750628
  }
}

Usare l'espressione jq seguente per convertire la temperatura da fahrenheit a celsius ed estrarre i secondi correnti letti da un timestamp di millisecondo unix:

.payload.temperatureC = (5/9) * (.payload.temperatureF - 32)
| .payload.seconds = (.payload.timestamp / 1000) % 60

Nell'espressione jq precedente:

  • .payload.temperatureC = (5/9) * (.payload.temperatureF - 32) crea un nuovo temperatureC campo nel payload impostato sulla conversione di temperatureF da fahrenheit a celsius.
  • .payload.seconds = (.payload.timestamp / 1000) % 60 richiede un millisecondo unix e lo converte in secondi, quindi estrae il numero di secondi nel minuto corrente usando un calcolo modulo.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "temperatureF": 94.2,
    "timestamp": 1689766750628,
    "temperatureC": 34.55555555555556,
    "seconds": 10
  }
}

Funzioni matematiche

jq include diverse funzioni che eseguono operazioni matematiche. L'elenco completo è disponibile nel manuale di jq.

L'esempio seguente illustra come calcolare l'energia cinetica dai campi di massa e velocità. Con l'input seguente:

{
  "userProperties": [
    { "key": "mass", "value": 512.1 },
    { "key": "productType", "value": "projectile" }
  ],
  "payload": {
    "velocity": 97.2
  }
}

Usare l'espressione jq seguente per calcolare l'energia cinetica dai campi di massa e velocità:

.payload.energy = (0.5 * (.userProperties | from_entries).mass * pow(.payload.velocity; 2) | round)

Nell'espressione jq precedente:

  • .payload.energy = <expression> crea un nuovo energy campo nel payload che è il risultato dell'esecuzione <expression>di .
  • (0.5 * (.userProperties | from_entries).mass * pow(.payload.velocity; 2) | round) è la formula per l'energia:
    • (.userProperties | from_entries).mass estrae la mass voce dall'elenco userProperties . I dati sono già configurati come oggetti con key e value, quindi from_entries possono convertirli direttamente in un oggetto . L'espressione recupera la mass chiave dall'oggetto risultante e ne restituisce il valore.
    • pow(.payload.velocity; 2) estrae la velocità dal payload e piazzala aumentandola alla potenza di 2.
    • <expression> | round arrotonda il risultato al numero intero più vicino per evitare precisione fuorviante nel risultato.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "userProperties": [
    { "key": "mass", "value": 512.1 },
    { "key": "productType", "value": "projectile" }
  ],
  "payload": {
    "velocity": 97.2,
    "energy": 2419119
  }
}

Logica booleana

Le pipeline di elaborazione dati usano spesso jq per filtrare i messaggi. Il filtro usa in genere espressioni e operatori booleani. Inoltre, la logica booleana è utile per eseguire il flusso di controllo nelle trasformazioni e casi d'uso di filtri più avanzati.

Gli esempi seguenti illustrano alcune delle funzionalità più comuni usate nelle espressioni booleane in jq:

Operatori booleani e condizionali di base

jq fornisce gli operatori logici andbooleani di base , ore not. Gli and operatori e or sono operatori di prefisso. not è una funzione richiamata come filtro, <expression> | notad esempio .

jq ha gli operatori >condizionali , <, !===, >=, e <=. Questi operatori sono operatori di prefisso.

L'esempio seguente illustra come eseguire una logica booleana di base usando le istruzioni condizionali. Con l'input seguente:

{
  "payload": {
    "temperature": 50,
    "humidity": 92,
    "site": "Redmond"
  }
}

Usare l'espressione jq seguente per verificare se:

  • La temperatura è compresa tra 30 gradi e 60 gradi inclusi nel limite superiore.
  • L'umidità è minore di 80 e il sito è Redmond.
.payload
| ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond"
| not

Nell'espressione jq precedente:

  • .payload | <expression><expression> ambiti per il contenuto di .payload. Questa sintassi rende meno dettagliato il resto dell'espressione.
  • ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond" restituisce true se la temperatura è compresa tra 30 gradi e 60 gradi (inclusi nel limite superiore) o l'umidità è minore di 80, restituisce true solo se il sito è anche Redmond.
  • <expression> | not accetta il risultato dell'espressione precedente e applica un NOT logico, in questo esempio il risultato viene ripristinato da true a false.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

false

Controllare l'esistenza della chiave dell'oggetto

È possibile creare un filtro che controlla la struttura di un messaggio anziché il relativo contenuto. Ad esempio, è possibile verificare se una chiave specifica è presente in un oggetto . A tale scopo, usare la has funzione o un controllo su Null. L'esempio seguente illustra entrambi questi approcci. Con l'input seguente:

{
  "payload": {
    "temperature": 51,
    "humidity": 41,
    "site": null
  }
}

Usare l'espressione jq seguente per verificare se il payload contiene un temperature campo, se il site campo non è Null e altri controlli:

.payload | {
    hasTemperature: has("temperature"),
    temperatureNotNull: (.temperature != null),
    hasSite: has("site"),
    siteNotNull: (.site != null),
    hasMissing: has("missing"),
    missingNotNull: (.missing != null),
    hasNested: (has("nested") and (.nested | has("inner"))),
    nestedNotNull: (.nested?.inner != null)
}

Nell'espressione jq precedente:

  • .payload | <expression> definisce l'ambito del contesto dati di al valore di <expression>.payload per rendere <expression> meno dettagliato.
  • hasTemperature: has("temperature"), questa e altre espressioni simili illustrano il comportamento della has funzione con un oggetto di input. La funzione restituisce true solo se la chiave è presente. hasSite è true nonostante il valore di site sia null.
  • temperatureNotNull: (.temperature != null), questa e altre espressioni simili illustrano come il != null controllo esegue un controllo simile a has. Una chiave inesistente in un oggetto è null se si accede usando la .<key> sintassi o la chiave esiste ma ha un valore .null Sia siteNotNull che missingNotNull sono false, anche se è presente una chiave e l'altra è assente.
  • hasNested: (has("nested") and (.nested | has("inner"))) esegue un controllo su un oggetto annidato con has, in cui l'oggetto padre potrebbe non esistere. Il risultato è una catena di controlli a ogni livello per evitare un errore.
  • nestedNotNull: (.nested?.inner != null) esegue lo stesso controllo su un oggetto annidato utilizzando != null e per abilitare il ? concatenamento dei percorsi nei campi che potrebbero non esistere. Questo approccio produce una sintassi più pulita per catene annidate profondamente che potrebbero non esistere, ma non è in grado di distinguere null i valori chiave da quelli che non esistono.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "hasTemperature": true,
  "temperatureNotNull": true,
  "hasSite": true,
  "siteNotNull": false,
  "hasMissing": false,
  "missingNotNull": false,
  "hasNested": false,
  "nestedNotNull": false
}

Controllare l'esistenza della voce di matrice

Usare la any funzione per verificare l'esistenza di una voce in una matrice. Con l'input seguente:

{
  "userProperties": [
    { "key": "mass", "value": 512.1 },
    { "key": "productType", "value": "projectile" }
  ],
  "payload": {
    "velocity": 97.2,
    "energy": 2419119
  }
}

Usare l'espressione jq seguente per verificare se la userProperties matrice ha una voce con una chiave di mass e nessuna voce con una chiave di missing:

.userProperties | any(.key == "mass") and (any(.key == "missing") | not)

Nell'espressione jq precedente:

  • .userProperties | <expression> definisce l'ambito del contesto dati di al valore di <expression>userProperties per rendere il resto meno <expression> dettagliato.
  • any(.key == "mass") esegue l'espressione .key == "mass" su ogni elemento della userProperties matrice, restituendo true se l'espressione restituisce true per almeno un elemento della matrice.
  • (any(.key == "missing") | not).key == "missing" viene eseguito su ogni elemento della userProperties matrice, restituendo true se un elemento restituisce true, quindi nega il risultato complessivo con | not.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

true

Flusso di controllo

Il flusso di controllo in jq è diverso dalla maggior parte dei linguaggi, perché la maggior parte delle forme di flusso di controllo è direttamente guidata dai dati. Esiste ancora il supporto per le espressioni if/else con la semantica di programmazione funzionale tradizionale, ma è possibile ottenere la maggior parte delle strutture di ciclo usando combinazioni di map funzioni e reduce .

Gli esempi seguenti illustrano alcuni scenari comuni del flusso di controllo in jq.

Istruzioni If-else

jq supporta le condizioni tramite if <test-expression> then <true-expression> else <false-expression> end. È possibile inserire altri casi aggiungendo elif <test-expression> then <true-expression> al centro. Una differenza fondamentale tra jq e molti altri linguaggi è che ogni thenelse espressione produce un risultato usato nelle operazioni successive nell'espressione jq complessiva.

Nell'esempio seguente viene illustrato come usare if le istruzioni per produrre informazioni condizionali. Con l'input seguente:

{
  "payload": {
    "temperature": 25,
    "humidity": 52
  }
}

Usare l'espressione jq seguente per verificare se la temperatura è elevata, bassa o normale:

.payload.status = if .payload.temperature > 80 then
  "high"
elif .payload.temperature < 30 then
  "low"
else
  "normal"
end

Nell'espressione jq precedente:

  • .payload.status = <expression> assegna il risultato dell'esecuzione <expression> a un nuovo status campo nel payload.
  • if ... end è l'espressione principale if/elif/else :
    • if .payload.temperature > 80 then "high" controlla la temperatura rispetto a un valore elevato, restituendo "high" se true, in caso contrario continua.
    • elif .payload.temperature < 30 then "low" esegue un secondo controllo sulla temperatura per un valore basso, impostando il risultato su "low" se true; in caso contrario, continua.
    • else "normal" end restituisce "normal" se nessuno dei controlli precedenti era true e chiude l'espressione con end.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "temperature": 25,
    "humidity": 52,
    "status": "low"
  }
}

Mapping

Nei linguaggi funzionali come jq, il modo più comune per eseguire la logica iterativa consiste nel creare una matrice e quindi mappare i valori di tale matrice a quelli nuovi. Questa tecnica viene ottenuta in jq usando la map funzione , che viene visualizzata in molti degli esempi in questa guida. Se si vuole eseguire un'operazione su più valori, map è probabilmente la risposta.

Nell'esempio seguente viene illustrato come usare map per rimuovere un prefisso dalle chiavi di un oggetto . Questa soluzione può essere scritta più concisamente usando with_entries, ma la versione più dettagliata mostrata di seguito illustra il mapping effettivo in corso sotto il cappuccio nell'approccio abbreviato. Con l'input seguente:

{
  "payload": {
    "rotor_rpm": 150,
    "rotor_temperature": 51,
    "rotor_cycles": 1354
  }
}

Usare l'espressione jq seguente per rimuovere il rotor_ prefisso dalle chiavi del payload:

.payload |= (to_entries | map(.key |= ltrimstr("rotor_")) | from_entries)

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • (to_entries | map(<expression) | from_entries) esegue la conversione della matrice di oggetti e esegue il mapping di ogni voce a un nuovo valore con <expression>. Questo approccio è semanticamente equivalente a with_entries(<expression>):
    • to_entries converte un oggetto in una matrice, con ogni coppia chiave/valore che diventa un oggetto separato con struttura {"key": <key>, "value": <value>}.
    • map(<expression>)<expression> esegue su ogni elemento della matrice e produce una matrice di output con i risultati di ogni espressione.
    • from_entries è l'inverso di to_entries. La funzione converte una matrice di oggetti con struttura {"key": <key>, "value": <value>} in un oggetto con i key campi e value mappati in coppie chiave/valore.
  • .key |= ltrimstr("rotor_") aggiorna il valore di .key in ogni voce con il risultato di ltrimstr("rotor_"). La |= sintassi definisce l'ambito del contesto dati del lato destro al valore di .key. ltrimstr rimuove il prefisso specificato dalla stringa, se presente.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": {
    "rpm": 150,
    "temperature": 51,
    "cycles": 1354
  }
}

Riduci

La riduzione è il modo principale per eseguire operazioni iterative o cicli tra gli elementi di una matrice. L'operazione di riduzione è costituita da un enumeratore e da un'operazione che utilizza l'enumeratore e l'elemento corrente della matrice come input. Ogni iterazione del ciclo restituisce il valore successivo dell'riduttore e l'output finale dell'operazione di riduzione è l'ultimo valore dell'riduttore. La riduzione viene definita riduzione in alcuni altri linguaggi di programmazione funzionale.

Usare l'operazione reduce in jq per ridurre le prestazioni. La maggior parte dei casi d'uso non richiede questa manipolazione di basso livello e può invece usare funzioni di livello superiore, ma reduce è uno strumento generale utile.

L'esempio seguente illustra come calcolare la variazione media del valore per una metrica sui punti dati disponibili. Con l'input seguente:

{
  "payload": [
    {
      "value": 65,
      "timestamp": 1689796743559
    },
    {
      "value": 55,
      "timestamp": 1689796771131
    },
    {
      "value": 59,
      "timestamp": 1689796827766
    },
    {
      "value": 62,
      "timestamp": 1689796844883
    },
    {
      "value": 58,
      "timestamp": 1689796864853
    }
  ]
}

Usare l'espressione jq seguente per calcolare la variazione media del valore nei punti dati:

.payload |= (
  reduce .[] as $item (
    null;
    if . == null then
      {totalChange: 0, previous: $item.value, count: 0}
    else
      .totalChange += (($item.value - .previous) | length)
      | .previous = $item.value
      | .count += 1
    end
  )
  | .totalChange / .count
)

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • reduce .[] as $item (<init>; <expression>) è lo scaffolding di un'operazione di riduzione tipica con le parti seguenti:
    • .[] as $item deve essere <expression> as <variable> sempre e è più spesso .[] as $item. Produce <expression> un flusso di valori, ognuno dei quali viene salvato in <variable> per un'iterazione dell'operazione di riduzione. Se si dispone di una matrice su cui eseguire l'iterazione, .[] la suddivide in un flusso. Questa sintassi è la stessa della sintassi usata per suddividere i messaggi, ma l'operazione reduce non usa il flusso per generare più output. reduce non divide il messaggio.
    • <init> in questo caso null è il valore iniziale dell'riduttore utilizzato nell'operazione di riduzione. Questo valore viene spesso impostato su vuoto o zero. Questo valore diventa il contesto dei dati, . in questo ciclo <expression>, per la prima iterazione.
    • <expression> è l'operazione eseguita su ogni iterazione dell'operazione di riduzione. Ha accesso al valore dell'caricabatterie corrente, attraverso .e il valore corrente nel flusso attraverso il <variable> dichiarato in precedenza, in questo caso $item.
  • if . == null then {totalChange: 0, previous: $item.value, count: 0} è un'istruzione condizionale per gestire la prima iterazione di riduzione. Imposta la struttura dell'accumulatore per l'iterazione successiva. Poiché l'espressione calcola le differenze tra le voci, la prima voce configura i dati usati per calcolare una differenza nella seconda iterazione di riduzione. I totalChangecampi , previouse count fungono da variabili di ciclo e aggiornano in ogni iterazione.
  • .totalChange += (($item.value - .previous) | length) | .previous = $item.value | .count += 1 è l'espressione nel else caso. Questa espressione imposta ogni campo nell'oggetto analizzatore su un nuovo valore basato su un calcolo. Per totalChange, trova la differenza tra i valori correnti e precedenti e ottiene il valore assoluto. In modo controintuitivo usa la length funzione per ottenere il valore assoluto. previousè impostato su 's corrente $itemvalue per l'iterazione successiva da usare e count viene incrementato.
  • .totalChange / .count calcola la variazione media tra i punti dati dopo il completamento dell'operazione di riduzione e il valore finale dell'riduttore.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": 5.25
}

Cicli

I cicli in jq sono in genere riservati per i casi d'uso avanzati. Poiché ogni operazione in jq è un'espressione che produce un valore, la semantica basata sulle istruzioni dei cicli nella maggior parte dei linguaggi non è una scelta naturale in jq. Prendere in considerazione l'uso map di o reduce per soddisfare le proprie esigenze.

Esistono due tipi principali di ciclo tradizionale in jq. Esistono altri tipi di ciclo, ma sono per casi d'uso più specializzati:

  • while applica ripetutamente un'operazione al contesto dei dati di input, aggiornando il valore del contesto di dati da usare nell'iterazione successiva e producendo tale valore come output. L'output di un while ciclo è una matrice che contiene i valori generati da ogni iterazione del ciclo.
  • until come while si applica ripetutamente un'operazione sul contesto dei dati di input, aggiornando il valore del contesto di dati da usare nell'iterazione successiva. A differenza di while, il until ciclo restituisce il valore prodotto dall'ultima iterazione del ciclo.

Nell'esempio seguente viene illustrato come usare un until ciclo per eliminare progressivamente i punti dati outlier da un elenco di letture fino a quando la deviazione standard non scende al di sotto di un valore predefinito. Con l'input seguente:

{
  "payload": [
    {
      "value": 65,
      "timestamp": 1689796743559
    },
    {
      "value": 55,
      "timestamp": 1689796771131
    },
    {
      "value": 59,
      "timestamp": 1689796827766
    },
    {
      "value": 62,
      "timestamp": 1689796844883
    },
    {
      "value": 58,
      "timestamp": 1689796864853
    }
  ]
}

Usare l'espressione jq seguente per eliminare progressivamente i punti dati outlier da un elenco di letture fino a quando la deviazione standard non scende al di sotto di 2:

def avg: add / length;
def stdev: avg as $mean | (map(. - $mean | . * .) | add) / (length - 1) | sqrt;
.payload |= (
  sort_by(.value)
  | until(
    (map(.value) | stdev) < 2 or length == 0;
    (map(.value) | avg) as $avg
    | if ((.[0].value - $avg) | length) > ((.[-1].value - $avg) | length) then
      del(.[0])
    else
      del(.[-1])
    end
  )
)

Nell'espressione jq precedente:

  • def avg: add / length; definisce una nuova funzione chiamata avg usata per calcolare le medie più avanti nell'espressione. L'espressione a destra di : è l'espressione logica usata ogni volta che si usa avg. L'espressione <expression> | avg equivale a <expression> | add / length
  • def stdev: avg as $mean | (map(. - $mean | . * .) | add) / (length - 1) | sqrt; definisce una nuova funzione denominata stdev. La funzione calcola la deviazione standard di esempio di una matrice usando una versione modificata della risposta della community in StackOverflow.
  • .payload |= <expression> i primi due defsono solo dichiarazioni e avviano l'espressione effettiva. L'espressione viene eseguita <expression> con un oggetto dati di input di .payload e assegna di nuovo il risultato a .payload.
  • sort_by(.value) ordina la matrice di voci di matrice in base al relativo value campo. Questa soluzione richiede di identificare e modificare i valori più alti e minimi in una matrice, quindi l'ordinamento dei dati in anticipo riduce il calcolo e semplifica il codice.
  • until(<condition>; <expression>)<expression> viene eseguito sull'input fino a <condition> quando non restituisce true. L'input per ogni esecuzione di <expression> e <condition> è l'output dell'esecuzione precedente di <expression>. Il risultato dell'ultima esecuzione di <expression> viene restituito dal ciclo .
  • (map(.value) | stdev) < 2 or length == 0 è la condizione per il ciclo:
    • map(.value) converte la matrice in un elenco di numeri puri da utilizzare nel calcolo successivo.
    • (<expression> | stdev) < 2 calcola la deviazione standard della matrice e restituisce true se la deviazione standard è minore di 2.
    • length == 0 ottiene la lunghezza della matrice di input e restituisce true se è 0. Per proteggersi dal caso in cui tutte le voci vengano eliminate, il risultato è or-ed con l'espressione complessiva.
  • (map(.value) | avg) as $avg converte la matrice in una matrice di numeri e calcola la media e quindi salva il risultato in una $avg variabile. Questo approccio consente di risparmiare sui costi di calcolo perché si riutilizza la media più volte nell'iterazione del ciclo. Le espressioni di assegnazione delle variabili non modificano il contesto dei dati per l'espressione successiva dopo |, quindi il resto del calcolo ha ancora accesso alla matrice completa.
  • if <condition> then <expression> else <expression> end è la logica di base dell'iterazione del ciclo. <condition> Usa per determinare l'oggetto <expression> da eseguire e restituire.
  • ((.[0].value - $avg) | length) > ((.[-1].value - $avg) | length) è la if condizione che confronta i valori più alti e minimi con il valore medio e quindi confronta tali differenze:
    • (.[0].value - $avg) | length recupera il value campo della prima voce della matrice e ottiene la differenza tra di essa e la media complessiva. La prima voce della matrice è la più bassa a causa dell'ordinamento precedente. Questo valore potrebbe essere negativo, quindi il risultato viene inviato tramite pipe a length, che restituisce il valore assoluto quando viene specificato un numero come input.
    • (.[-1].value - $avg) | length esegue la stessa operazione rispetto all'ultima voce di matrice e calcola anche il valore assoluto per la sicurezza. L'ultima voce della matrice è la più alta a causa dell'ordinamento precedente. I valori assoluti vengono quindi confrontati nella condizione complessiva usando >.
  • del(.[0]) è l'espressione then eseguita quando la prima voce della matrice è l'outlier più grande. L'espressione rimuove l'elemento in dalla .[0] matrice. L'espressione restituisce i dati lasciati nella matrice dopo l'operazione.
  • del(.[-1]) è l'espressione else eseguita quando l'ultima voce della matrice è l'outlier più grande. L'espressione rimuove l'elemento in .[-1], ovvero l'ultima voce, dalla matrice. L'espressione restituisce i dati lasciati nella matrice dopo l'operazione.

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "payload": [
    {
      "value": 58,
      "timestamp": 1689796864853
    },
    {
      "value": 59,
      "timestamp": 1689796827766
    },
    {
      "value": 60,
      "timestamp": 1689796844883
    }
  ]
}

Eliminare messaggi

Quando si scrive un'espressione di filtro, è possibile indicare al sistema di eliminare tutti i messaggi che non si desidera restituendo false. Questo comportamento è il comportamento di base delle espressioni condizionali in jq. Tuttavia, ci sono momenti in cui si trasformano i messaggi o si eseguono filtri più avanzati quando si vuole che il sistema rilasci esplicitamente o in modo implicito i messaggi per l'utente. Negli esempi seguenti viene illustrato come implementare questo comportamento.

Eliminazione esplicita

Per eliminare in modo esplicito un messaggio in un'espressione di filtro, restituire false dall'espressione .

È anche possibile eliminare un messaggio dall'interno di una trasformazione usando la funzione predefinita empty in jq.

Nell'esempio seguente viene illustrato come calcolare una media di punti dati nel messaggio ed eliminare i messaggi con una media inferiore a un valore fisso. È possibile e valido ottenere questo comportamento con la combinazione di una fase di trasformazione e di una fase di filtro. Usa l'approccio più adatto alla tua situazione. Dati gli input seguenti:

Message 1

{
  "payload": {
    "temperature": [23, 42, 63, 61],
    "humidity": [64, 36, 78, 33]
  }
}

Message 2

{
  "payload": {
    "temperature": [42, 12, 32, 21],
    "humidity": [92, 63, 57, 88]
  }
}

Usare l'espressione jq seguente per calcolare la media dei punti dati ed eliminare eventuali messaggi con una temperatura media inferiore a 30 o un'umidità media maggiore di 90:

.payload |= map_values(add / length)
| if .payload.temperature > 30 and .payload.humidity < 90 then . else empty end

Nell'espressione jq precedente:

  • .payload |= <expression> usa |= per aggiornare il valore di .payload con il risultato dell'esecuzione <expression>di . Se si usa |= invece di = impostare il contesto dati di <expression> su .payload anziché .su .
  • map_values(add / length)add / length esegue per ogni valore nel .payload sottooggetto. L'espressione somma gli elementi nella matrice di valori e quindi divide per la lunghezza della matrice per calcolare la media.
  • if .payload.temperature > 30 and .payload.humidity < 90 then . else empty end controlla due condizioni rispetto al messaggio risultante. Se il filtro restituisce true, come nel primo input, il messaggio completo viene generato come output. Se il filtro restituisce false, come nel secondo input, restituisce empty, che restituisce un flusso vuoto con valori zero. Questo risultato determina l'eliminazione del messaggio corrispondente da parte dell'espressione.

Output 1

{
  "payload": {
    "temperature": 47.25,
    "humidity": 52.75
  }
}

Output 2

(nessun output)

Eliminazione implicita tramite errori

Sia le espressioni di filtro che di trasformazione possono eliminare i messaggi in modo implicito causando la generazione di un errore da parte di jq. Anche se questo approccio non è una procedura consigliata perché la pipeline non è in grado di distinguere tra un errore causato intenzionalmente e uno causato dall'input imprevisto per l'espressione. Il sistema gestisce attualmente un errore di runtime nel filtro o nella trasformazione eliminando il messaggio e registrando l'errore.

Uno scenario comune che usa questo approccio è quando un input di una pipeline può avere messaggi che sono strutturalmente disgiunti. Nell'esempio seguente viene illustrato come ricevere due tipi di messaggi, uno dei quali valuta correttamente il filtro e l'altro che è strutturalmente incompatibile con l'espressione. Dati gli input seguenti:

Message 1

{
  "payload": {
    "sensorData": {
      "temperature": 15,
      "humidity": 62
    }
  }
}

Message 2

{
  "payload": [
    {
      "rpm": 12,
      "timestamp": 1689816609514
    },
    {
      "rpm": 52,
      "timestamp": 1689816628580
    }
  ]
}

Usare l'espressione jq seguente per filtrare i messaggi con una temperatura inferiore a 10 e un'umidità maggiore di 80:

.payload.sensorData.temperature > 10 and .payload.sensorData.humidity < 80

Nell'esempio precedente l'espressione stessa è un'espressione booleana composta semplice. L'espressione è progettata per funzionare con la struttura del primo dei messaggi di input mostrati in precedenza. Quando l'espressione riceve il secondo messaggio, la struttura di matrice di .payload è incompatibile con l'accesso all'oggetto nell'espressione e genera un errore. Se si vuole filtrare in base ai valori di temperatura/umidità e rimuovere messaggi con struttura incompatibile, questa espressione funziona. Un altro approccio che non genera alcun errore consiste nell'aggiungere (.payload | type) == "object" and all'inizio dell'espressione.

Output 1

true

Output 2

(errore)

Utilità temporali

jq non supporta il tempo come tipo nativo. Tuttavia, alcuni formati accettati e generati dal responsabile del trattamento dei dati supportano il tempo come tipo nativo. Questi tipi sono in genere rappresentati usando il tipo di time.Time Go.

Per consentire l'interazione con questi valori da jq, il responsabile del trattamento dei dati fornisce un set di funzioni che consentono di:

  • Eseguire la conversione tra stringhe native time, ISO 8601 e timestamp Unix numerici.
  • Eseguire varie operazioni specifiche del tempo su tutti questi tipi.

Modulo time

Tutte le funzioni specifiche del tempo speciali vengono specificate in un time modulo che è possibile importare in una query.

Importare il modulo all'inizio della query in uno dei due modi seguenti:

  • import" "time" as time;
  • include "time"

Il primo metodo inserisce tutte le funzioni nel modulo in uno spazio dei nomi, ad esempio time::totime. Il secondo metodo inserisce semplicemente tutte le funzioni temporali al livello superiore, ad esempio totime. Entrambe le sintassi sono valide e equivalenti a livello funzionale.

Formati e conversione

Il modulo time funziona con tre formati temporali:

  • time è un valore di ora nativa. È possibile usarlo solo con le funzioni nel modulo time. Riconosciuto come time tipo di dati durante la serializzazione.
  • unix è un timestamp Unix numerico che rappresenta l'ora come secondi dall'epoca Unix. Può essere un numero intero o a virgola mobile. Riconosciuto come tipo numerico corrispondente durante la serializzazione.
  • iso è una rappresentazione in formato stringa ISO 8601 dell'ora. Riconosciuto come stringa durante la serializzazione.

Il modulo time fornisce le funzioni seguenti per controllare e modificare questi tipi:

  • time::totime converte uno dei tre tipi in time.
  • time::tounix converte uno dei tre tipi in unix.
  • time::toiso converte uno dei tre tipi in iso.
  • time::istime restituisce true se i dati sono in time formato .

Operazioni temporali

Il modulo time fornisce varie operazioni specifiche del tempo che operano su tutti i tipi. Le funzioni seguenti possono accettare uno qualsiasi dei tipi supportati come input e restituire lo stesso tipo dell'output. I timestamp interi possono essere convertiti in timestamp a virgola mobile se è necessaria una maggiore precisione.

  • time::utc converte l'ora in UTC.
  • time::zone(zone) converte l'ora nel fuso specificato. zone è una stringa di zona ISO 8601. Ad esempio: time::zone("-07").
  • time::local converte l'ora in ora locale.
  • time::offset(duration) scosta l'ora in base alla durata specificata. duration usa la sintassi della stringa di durata di Go. Ad esempio: time::offset("1m2s").
  • time::offset(value;unit) scosta l'ora in base alla durata specificata. Questa funzione usa un numero e una stringa di unità. Ad esempio: time::offset(2;"s"). Questa funzione è utile quando la durata proviene da un'altra proprietà.

Nota

Le tre funzioni di fuso orario non hanno alcun effetto significativo sui timestamp Unix.

Utilità varie

Il util modulo è una raccolta di utilità che espande le funzionalità del runtime jq.

Modulo util

Tutte le utilità varie vengono specificate in un util modulo che è possibile importare in una query.

Importare il modulo all'inizio della query in uno dei due modi seguenti:

  • import" "util" as util;
  • include "util"

Il primo metodo inserisce tutte le funzioni nel modulo in uno spazio dei nomi, ad esempio util::uuid. Il secondo metodo inserisce semplicemente tutte le funzioni varie al livello superiore, ad esempio uuid. Entrambe le sintassi sono valide e equivalenti a livello funzionale.

Il util modulo include attualmente la uuid funzione che restituisce un nuovo UUID casuale nel formato stringa standard.

Manipolazione binaria

jq è progettato per lavorare con i dati che possono essere rappresentati come JSON. Tuttavia, le pipeline di anteprima di Azure IoT Data Processor supportano anche un formato di dati non elaborati che contiene dati binari non verificati. Per lavorare con i dati binari, la versione di jq fornita con il responsabile del trattamento dei dati contiene un pacchetto progettato per facilitare l'elaborazione dei dati binari. Consentono di:

  • Eseguire la conversione avanti e indietro tra formati binari e altri formati, ad esempio matrici base64 e integer.
  • Usare le funzioni predefinite per leggere valori numerici e stringa da un messaggio binario.
  • Eseguire le modifiche dei punti dei dati binari mantenendone il formato.

Importante

Non è possibile usare funzioni jq o operatori predefiniti che modificano un valore binario. Ciò significa che non viene eseguita alcuna concatenazione con +, senza map operazioni sui byte e senza assegnazioni miste con valori binari, ad |=esempio , +=, //=. È possibile usare l'assegnazione standard (==). Se si tenta di usare dati binari con un'operazione non supportata, il sistema genera un jqImproperBinaryUsage errore. Se è necessario modificare i dati binari in modi personalizzati, è consigliabile usare una delle funzioni seguenti per convertirla in base64 o in una matrice integer per il calcolo e quindi convertirla di nuovo in binario.

Le sezioni seguenti descrivono il supporto binario nel motore jq del responsabile del trattamento dei dati.

Modulo binary

Tutto il supporto binario nel motore jq del responsabile del trattamento dei dati è specificato in un binary modulo che è possibile importare.

Importare il modulo all'inizio della query in uno dei due modi seguenti:

  • import "binary" as binary;
  • include "binary"

Il primo metodo inserisce tutte le funzioni nel modulo in uno spazio dei nomi, ad esempio binary::tobase64. Il secondo metodo inserisce semplicemente tutte le funzioni binarie al livello superiore, ad esempio tobase64. Entrambe le sintassi sono valide e equivalenti a livello funzionale.

Formati e conversione

Il modulo binario funziona con tre tipi:

  • binary : valore binario, utilizzabile solo direttamente con le funzioni nel modulo binario. Riconosciuto da una pipeline come tipo di dati binari durante la serializzazione. Utilizzare questo tipo per la serializzazione non elaborata.
  • array : un formato che trasforma il file binario in una matrice di numeri per consentire l'elaborazione personalizzata. Riconosciuto da una pipeline come matrice di numeri interi durante la serializzazione.
  • base64 : rappresentazione in formato stringa del file binario. Per lo più utile se si desidera eseguire la conversione tra stringhe e binarie. Riconosciuto da una pipeline come stringa durante la serializzazione.

È possibile eseguire la conversione tra tutti e tre i tipi nelle query jq in base alle esigenze. Ad esempio, è possibile convertire da binario a una matrice, eseguire alcune modifiche personalizzate e quindi riconvertire in binario alla fine per mantenere le informazioni sul tipo.

Funzioni

Per il controllo e la modifica tra questi tipi vengono fornite le funzioni seguenti:

  • binary::tobinary converte uno dei tre tipi in binario.
  • binary::toarray converte uno dei tre tipi in matrice.
  • binary::tobase64 converte uno dei tre tipi in base64.
  • binary::isbinary restituisce true se i dati sono in formato binario.
  • binary::isarray restituisce true se i dati sono in formato matrice.
  • binary::isbase64 restituisce true se i dati sono in formato base64.

Il modulo fornisce anche la binary::edit(f) funzione per le modifiche rapide dei dati binari. La funzione converte l'input nel formato di matrice, applica la funzione su di essa e quindi converte nuovamente il risultato in binario.

Estrarre dati dal file binario

Il modulo binario consente di estrarre valori dai dati binari da usare per decomprimere i payload binari personalizzati. In generale, questa funzionalità segue quella di altre librerie di decompressione binarie e segue un nome simile. È possibile decomprimere i tipi seguenti:

  • Integer (int8, int16, int32, int64, uint8, uint16, uint32, uint64)
  • Floats (float, double)
  • Stringhe (utf8)

Il modulo consente inoltre di specificare offset e endianness, se applicabile.

Funzioni per leggere i dati binari

Il modulo binario fornisce le funzioni seguenti per estrarre i dati dai valori binari. È possibile usare tutte le funzioni con uno dei tre tipi tra cui il pacchetto può eseguire la conversione.

Tutti i parametri di funzione sono facoltativi0, offset per impostazione predefinita e length per impostazione predefinita per il resto dei dati.

  • binary::read_int8(offset) legge un valore int8 da un valore binario.
  • binary::read_int16_be(offset) legge un valore int16 da un valore binario in ordine big-endian.
  • binary::read_int16_le(offset) legge un valore int16 da un valore binario in ordine little-endian.
  • binary::read_int32_be(offset) legge un valore int32 da un valore binario in ordine big-endian.
  • binary::read_int32_le(offset) legge un valore int32 da un valore binario in ordine little-endian.
  • binary::read_int64_be(offset) legge un valore int64 da un valore binario in ordine big-endian.
  • binary::read_int64_le(offset) legge un valore int64 da un valore binario in ordine little-endian.
  • binary::read_uint8(offset) legge un valore uint8 da un valore binario.
  • binary::read_uint16_be(offset) legge un valore uint16 da un valore binario in ordine big-endian.
  • binary::read_uint16_le(offset) legge un valore uint16 da un valore binario in ordine little-endian.
  • binary::read_uint32_be(offset) legge un valore uint32 da un valore binario in ordine big-endian.
  • binary::read_uint32_le(offset) legge un valore uint32 da un valore binario in ordine little-endian.
  • binary::read_uint64_be(offset) legge un valore uint64 da un valore binario in ordine big-endian.
  • binary::read_uint64_le(offset) legge un uint64 da un valore binario in ordine little-endian.
  • binary::read_float_be(offset) legge un valore float da un valore binario in ordine big-endian.
  • binary::read_float_le(offset) legge un valore float da un valore binario in ordine little-endian.
  • binary::read_double_be(offset) legge un valore double da un valore binario in ordine big-endian.
  • binary::read_double_le(offset) legge un valore double da un valore binario in ordine little-endian.
  • binary::read_bool(offset; bit) legge un valore bool da un valore binario, controllando il bit specificato per il valore.
  • binary::read_bit(offset; bit) legge un bit da un valore binario, usando l'indice di bit specificato.
  • binary::read_utf8(offset; length) legge una stringa UTF-8 da un valore binario.

Scrivere dati binari

Il modulo binario consente di codificare e scrivere valori binari. Questa funzionalità consente di costruire o apportare modifiche ai payload binari direttamente in jq. La scrittura di dati supporta lo stesso set di tipi di dati dell'estrazione dei dati e consente anche di specificare l'endianness da usare.

La scrittura dei dati è costituita da due forme:

  • write_* Le funzioni aggiornano i dati sul posto in un valore binario, usati per aggiornare o modificare i valori esistenti.
  • append_* Le funzioni aggiungono dati alla fine di un valore binario, usati per aggiungere o costruire nuovi valori binari.

Funzioni per la scrittura di dati binari

Il modulo binario fornisce le funzioni seguenti per la scrittura di dati in valori binari. Tutte le funzioni possono essere eseguite su uno dei tre tipi validi tra cui questo pacchetto può eseguire la conversione.

Il value parametro è obbligatorio per tutte le funzioni, ma è facoltativo, se offset valido e il valore predefinito è 0.

Scrivere funzioni:

  • binary::write_int8(value; offset) scrive un valore int8 in un valore binario.
  • binary::write_int16_be(value; offset) scrive un int16 in un valore binario in ordine big-endian.
  • binary::write_int16_le(value; offset) scrive un valore int16 in un valore binario in ordine little-endian.
  • binary::write_int32_be(value; offset) scrive un valore int32 in un valore binario in ordine big-endian.
  • binary::write_int32_le(value; offset) scrive un int32 in un valore binario in ordine little-endian.
  • binary::write_int64_be(value; offset) scrive un valore int64 in un valore binario in ordine big-endian.
  • binary::write_int64_le(value; offset) scrive un int64 in un valore binario in ordine little-endian.
  • binary::write_uint8(value; offset) scrive un valore uint8 in un valore binario.
  • binary::write_uint16_be(value; offset) scrive un valore uint16 in un valore binario in ordine big-endian.
  • binary::write_uint16_le(value; offset) scrive un uint16 in un valore binario in ordine little-endian.
  • binary::write_uint32_be(value; offset) scrive un valore uint32 in un valore binario in ordine big-endian.
  • binary::write_uint32_le(value; offset) scrive un valore uint32 in un valore binario in ordine little-endian.
  • binary::write_uint64_be(value; offset) scrive un valore uint64 in un valore binario in ordine big-endian.
  • binary::write_uint64_le(value; offset) scrive un valore uint64 in un valore binario in ordine little-endian.
  • binary::write_float_be(value; offset) scrive un valore float in un valore binario in ordine big-endian.
  • binary::write_float_le(value; offset) scrive un valore float in un valore binario in ordine little-endian.
  • binary::write_double_be(value; offset) scrive un valore double in un valore binario in ordine big-endian.
  • binary::write_double_le(value; offset) scrive un valore double in un valore binario in ordine little-endian.
  • binary::write_bool(value; offset; bit) scrive un valore bool in un singolo byte in un valore binario, impostando il bit specificato sul valore bool.
  • binary::write_bit(value; offset; bit) scrive un singolo bit in un valore binario, lasciando altri bit nel byte così come sono.
  • binary::write_utf8(value; offset) scrive una stringa UTF-8 in un valore binario.

Funzioni di accodamento:

  • binary::append_int8(value) aggiunge un valore int8 a un valore binario.
  • binary::append_int16_be(value) aggiunge un int16 a un valore binario in ordine big-endian.
  • binary::append_int16_le(value) aggiunge un int16 a un valore binario in ordine little-endian.
  • binary::append_int32_be(value) aggiunge un int32 a un valore binario in ordine big-endian.
  • binary::append_int32_le(value) aggiunge un int32 a un valore binario in ordine little-endian.
  • binary::append_int64_be(value) aggiunge un int64 a un valore binario in ordine big-endian.
  • binary::append_int64_le(value) aggiunge un valore int64 a un valore binario in ordine little-endian.
  • binary::append_uint8(value) aggiunge un valore uint8 a un valore binario.
  • binary::append_uint16_be(value) aggiunge un valore uint16 a un valore binario in ordine big-endian.
  • binary::append_uint16_le(value) aggiunge un valore uint16 a un valore binario in ordine little-endian.
  • binary::append_uint32_be(value) aggiunge un valore uint32 a un valore binario in ordine big-endian.
  • binary::append_uint32_le(value) aggiunge un valore uint32 a un valore binario in ordine little-endian.
  • binary::append_uint64_be(value) aggiunge un valore uint64 a un valore binario in ordine big-endian.
  • binary::append_uint64_le(value) aggiunge un valore uint64 a un valore binario in ordine little-endian.
  • binary::append_float_be(value) aggiunge un valore float a un valore binario in ordine big-endian.
  • binary::append_float_le(value) aggiunge un valore float a un valore binario in ordine little-endian.
  • binary::append_double_be(value) aggiunge un valore double a un valore binario in ordine big-endian.
  • binary::append_double_le(value) aggiunge un valore double a un valore binario in ordine little-endian.
  • binary::append_bool(value; bit) aggiunge un valore bool a un singolo byte in un valore binario, impostando il bit specificato sul valore bool.
  • binary::append_utf8(value) aggiunge una stringa UTF-8 a un valore binario.

Esempi binari

Questa sezione illustra alcuni casi d'uso comuni per l'uso di dati binari. Negli esempi viene usato un messaggio di input comune.

Si supponga di avere un messaggio con un payload che è un formato binario personalizzato che contiene più sezioni. Ogni sezione contiene i dati seguenti nell'ordine dei byte big-endian:

  • Valore uint32 che contiene la lunghezza del nome del campo in byte.
  • Stringa utf-8 che contiene il nome del campo la cui lunghezza specifica uint32 precedente.
  • Valore double che contiene il valore del campo.

Per questo esempio sono disponibili tre di queste sezioni, tenendo premuto:

  • (uint32) 11

  • (utf-8) temperatura

  • (double) 86.0

  • (uint32) 8

  • (utf-8) umidità

  • (doppio) 51,290

  • (uint32) 8

  • (utf-8) pressione

  • (double) 346.23

Questi dati sono simili al seguente quando vengono stampati all'interno della payload sezione di un messaggio:

{
  "payload": "base64::AAAAC3RlbXBlcmF0dXJlQFWAAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"
}

Nota

La base64::<string> rappresentazione dei dati binari è solo per semplificare la differenziazione rispetto ad altri tipi e non è rappresentativa del formato di dati fisici durante l'elaborazione.

Estrarre direttamente i valori

Se si conosce la struttura esatta del messaggio, è possibile recuperare i valori da esso usando gli offset appropriati.

Usare l'espressione jq seguente per estrarre i valori:

import "binary" as binary;
.payload | {
  temperature: binary::read_double_be(15),
  humidity: binary::read_double_be(35),
  pressure: binary::read_double_be(55)
}

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "humidity": 51.29,
  "pressure": 346.23,
  "temperature": 86
}

Estrarre i valori in modo dinamico

Se il messaggio può contenere campi in qualsiasi ordine, è possibile estrarre dinamicamente il messaggio completo:

Usare l'espressione jq seguente per estrarre i valori:

import "binary" as binary;
.payload
| {
    parts: {},
    rest: binary::toarray
}
|
until(
    (.rest | length) == 0;
    (.rest | binary::read_uint32_be) as $length
    | {
        parts: (
            .parts +
            {
                (.rest | binary::read_utf8(4; $length)): (.rest | binary::read_double_be(4 + $length))
            }
        ),
        rest: .rest[(12 + $length):]
    }
)
| .parts

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

{
  "humidity": 51.29,
  "pressure": 346.23,
  "temperature": 86
}

Modificare direttamente i valori

In questo esempio viene illustrato come modificare uno dei valori. Come nel caso di estrazione, è più semplice sapere dove si vuole modificare il valore nei dati binari. Questo esempio illustra come convertire la temperatura da fahrenheit a celsius.

Usare l'espressione jq seguente per convertire la temperatura da fahrenheit a celsius nel messaggio binario:

import "binary" as binary;
15 as $index
| .payload
| binary::write_double_be(
    ((5 / 9) * (binary::read_double_be($index) - 32));
    $index
)

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"

Se si applica la logica di estrazione illustrata in precedenza, si ottiene l'output seguente:

{
  "humidity": 51.29,
  "pressure": 346.23,
  "temperature": 30
}

Modificare i valori in modo dinamico

In questo esempio viene illustrato come ottenere lo stesso risultato dell'esempio precedente individuando dinamicamente il valore desiderato nella query.

Usare l'espressione jq seguente per convertire la temperatura da fahrenheit a celsius nel messaggio binario, individuando dinamicamente i dati da modificare:

import "binary" as binary;
.payload
| binary::edit(
    {
        index: 0,
        data: .
    }
    | until(
        (.data | length) <= .index;
        .index as $index
        | (.data | binary::read_uint32_be($index)) as $length
        | if (.data | binary::read_utf8($index + 4; $length)) == "temperature" then
            (
                (.index + 4 + $length) as $index
                | .data |= binary::write_double_be(((5 / 9) * (binary::read_double_be($index) - 32)); $index)
            )
        end
        | .index += $length + 12
    )
    | .data
)

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"

Inserire nuovi valori

Aggiungere nuovi valori usando le funzioni di accodamento del pacchetto. Ad esempio, per aggiungere un windSpeed campo con un valore di 31.678 all'input mantenendo il formato binario in ingresso, usare l'espressione jq seguente:

import "binary" as binary;
"windSpeed" as $key
| 31.678 as $value
| .payload
| binary::append_uint32_be($key | length)
| binary::append_utf8($key)
| binary::append_double_be($value)

Il codice JSON seguente mostra l'output dell'espressione jq precedente:

"base64:AAAAC3RlbXBlcmF0dXJlQFWAAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFIAAAACXdpbmRTcGVlZEA/rZFocrAh"