¿Qué son las expresiones jq en el procesador de datos?
Importante
Operaciones de IoT de Azure, habilitado por Azure Arc, está actualmente en VERSIÓN PRELIMINAR. No se debería usar este software en versión preliminar en entornos de producción.
Tendrá que implementar una nueva instalación de Azure IoT Operations cuando esté disponible una versión disponible con carácter general, no podrá actualizar una instalación en versión preliminar.
Consulte Términos de uso complementarios para las versiones preliminares de Microsoft Azure para conocer los términos legales que se aplican a las características de Azure que se encuentran en la versión beta, en versión preliminar o que todavía no se han publicado para que estén disponibles con carácter general.
Las expresiones jq proporcionan una manera eficaz de realizar cálculos y manipulaciones en mensajes de canalización de datos. En esta guía se muestran los patrones de lenguaje y los enfoques para las necesidades comunes de cálculo y procesamiento en las canalizaciones de datos.
Sugerencia
Para probar los ejemplos de esta guía, puede usar el área de juegos jq de y pegar las entradas y expresiones de ejemplo en el editor.
Aspectos básicos del lenguaje
Si no está familiarizado con jq como lenguaje, esta sección de aspectos básicos del lenguaje proporciona información general.
Programación funcional
El lenguaje jq es un lenguaje de programación funcional. Cada operación toma una entrada y genera una salida. Se combinan varias operaciones para realizar lógica compleja. Por ejemplo, dada la siguiente entrada:
{
"payload": {
"temperature": 25
}
}
Esta es una expresión jq simple que especifica una ruta de acceso que se va a recuperar:
.payload.temperature
Esta ruta de acceso es una operación que toma un valor como entrada y genera otro valor. En este ejemplo, el valor de salida es 25
.
Hay algunas consideraciones importantes al trabajar con operaciones complejas encadenadas en jq:
- Los datos no devueltos por una operación ya no están disponibles en el resto de la expresión. Hay algunas maneras de evitar esta restricción, pero en general debe pensar en qué datos necesita más adelante en la expresión y evitar que se quiten de las operaciones anteriores.
- Las expresiones se piensan mejor como una serie de transformaciones de datos en lugar de un conjunto de cálculos que se van a realizar. Incluso las operaciones como las asignaciones son simplemente una transformación del valor general en el que un campo ha cambiado.
Todo es una expresión
En la mayoría de los lenguajes no funcionales, hay una distinción entre dos tipos de operación:
- Expresiones que generan un valor que se puede usar en el contexto de otra expresión.
- Instrucciones que crean algún tipo de efecto secundario en lugar de manipular directamente una entrada y salida.
Con algunas excepciones, todo en jq es una expresión. Los bucles, las operaciones if/else e incluso las asignaciones son todas las expresiones que generan un nuevo valor, en lugar de crear un efecto secundario en el sistema. Por ejemplo, dada la siguiente entrada:
{
"temperature": 21,
"humidity": 65
}
Si quería cambiar el campo humidity
a 63
, puede usar una expresión de asignación:
.humidity = 63
Aunque esta expresión parece cambiar el objeto de entrada, en jq se genera un nuevo objeto con un nuevo valor para humidity
:
{
"temperature": 21,
"humidity": 63
}
Esta diferencia parece sutil, pero significa que puede encadenar el resultado de esta operación con más operaciones mediante |
, como se describe más adelante.
Encadenar operaciones con una canalización: |
La realización de cálculos y manipulación de datos en jq a menudo requiere combinar varias operaciones. Para encadenar las operaciones, coloque una |
entre ellas. Por ejemplo, para calcular la longitud de una matriz de datos en un mensaje:
{
"data": [5, 2, 4, 1]
}
En primer lugar, aísle la parte del mensaje que contiene la matriz:
.data
Esta expresión proporciona solo la matriz:
[5, 2, 4, 1]
A continuación, use la operación length
para calcular la longitud de esa matriz:
length
Esta expresión le proporciona la respuesta:
4
Use el operador |
como separador entre los pasos, por lo que, como una única expresión jq, el cálculo se convierte en:
.data | length
Si intenta realizar una transformación compleja y no ve un ejemplo aquí que coincida exactamente con el problema, es probable que pueda resolver el problema encadenando varias soluciones en esta guía con el símbolo |
.
Entradas y argumentos de función
Una de las operaciones principales de jq es llamar a una función. Las funciones de jq vienen en muchas formas y pueden tomar distintos números de entradas. Las entradas de función vienen en dos formas:
- Contexto de datos: los datos que se introducen automáticamente en la función mediante jq. Normalmente, los datos generados por la operación antes del símbolo
|
más reciente. - Argumentos de función: otras expresiones y valores que se proporcionan para configurar el comportamiento de una función.
Muchas funciones tienen cero argumentos y realizan todo su trabajo mediante el contexto de datos que proporciona jq. La length
función es un ejemplo:
["a", "b", "c"] | length
En el ejemplo anterior, la entrada para length
es la matriz creada a la izquierda del símbolo |
. La función no necesita ninguna otra entrada para calcular la longitud de la matriz de entrada. Solo se llama a funciones con cero argumentos mediante su nombre. En otras palabras, use length
, no length()
.
Algunas funciones combinan el contexto de datos con un único argumento para definir su comportamiento. Por ejemplo, la función map
:
[1, 2, 3] | map(. * 2)
En el ejemplo anterior, la entrada para map
es la matriz de números creados a la izquierda del símbolo |
. La función map
ejecuta una expresión en cada elemento de la matriz de entrada. Proporcione la expresión como argumento para map
, en este caso . * 2
multiplicar el valor de cada entrada de la matriz en 2 para generar la matriz [2, 4, 6]
. Puede configurar cualquier comportamiento interno que desee con la función de asignación.
Algunas funciones toman más de un argumento. Estas funciones funcionan de la misma manera que las funciones de argumento único y usan el símbolo ;
para separar los argumentos. Por ejemplo, la función sub
:
"Hello World" | sub("World"; "jq!")
En el ejemplo anterior, la función sub
recibe "Hola mundo" como contexto de datos de entrada y, a continuación, toma dos argumentos:
- Expresión regular que se va a buscar en la cadena.
- Cadena que se va a reemplazar cualquier sub cadena coincidente. Separe los argumentos con el símbolo
;
. El mismo patrón se aplica a las funciones con más de dos argumentos.
Importante
Asegúrese de usar ;
como separador de argumentos y no ,
.
Trabajar con objetos
Hay muchas maneras de extraer datos de, manipular y construir objetos en jq. En las secciones siguientes se describen algunos de los patrones más comunes:
Extracción de valores de un objeto
Para recuperar claves, normalmente se usa una expresión de ruta de acceso. Esta operación suele combinarse con otras operaciones para obtener resultados más complejos.
Es fácil recuperar datos de objetos. Cuando necesite recuperar muchos fragmentos de datos de estructuras que no son objetos, puede usar el patrón común de convertir en objetos estructuras que no son objetos. Dada la entrada siguiente:
{
"payload": {
"values": {
"temperature": 45,
"humidity": 67
}
}
}
Use la expresión siguiente para recuperar el valor de humedad:
.payload.values.humidity
Esta expresión genera la siguiente salida:
67
Cambio de claves en un objeto
Para cambiar el nombre o modificar las claves de objeto, puede usar la función with_entries
. Esta función toma una expresión que funciona en los pares clave-valor de un objeto y devuelve un nuevo objeto con los resultados de la expresión.
En el ejemplo siguiente se muestra cómo cambiar el nombre del campo temp
a temperature
para alinearse con un esquema de bajada. Dada la entrada siguiente:
{
"payload": {
"temp": 45,
"humidity": 67
}
}
Use la expresión siguiente para cambiar el nombre del campo temp
a temperature
:
.payload |= with_entries(if .key == "temp" then .key = "temperature" else . end)
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.with_entries(<expression>)
es una abreviatura para ejecutar varias operaciones juntas. Realiza las siguientes operaciones:- Toma un objeto como entrada y convierte cada par clave-valor en una entrada con estructura
{"key": <key>, "value": <value>}
. - Ejecuta
<expression>
en cada entrada generada a partir del objeto, reemplazando el valor de entrada de esa entrada por el resultado de ejecutar<expression>
. - Convierte el conjunto transformado de entradas de nuevo en un objeto, usando
key
como clave en el par clave-valor yvalue
como valor de la clave.
- Toma un objeto como entrada y convierte cada par clave-valor en una entrada con estructura
if .key == "temp" then .key = "temperature" else . end
realiza la lógica condicional en la clave de la entrada. Si la clave estemp
, se convierte atemperature
y se deja el valor sin cambios. Si la clave no estemp
, la entrada se deja sin cambios devolviendo.
de la expresión.
El siguiente JSON muestra la salida de la expresión anterior:
{
"payload": {
"temperature": 45,
"humidity": 67
}
}
Conversión de un objeto en una matriz
Aunque los objetos son útiles para acceder a datos, las matrices suelen ser más útiles cuando se desea dividir mensajes o combinar información dinámicamente. Use to_entries
para convertir un objeto en una matriz.
En el ejemplo siguiente se muestra cómo convertir el campo payload
en una matriz. Dada la entrada siguiente:
{
"id": "abc",
"payload": {
"temperature": 45,
"humidity": 67
}
}
Use la expresión siguiente para convertir el campo de carga en una matriz:
.payload | to_entries
El siguiente JSON es la salida de la expresión jq anterior:
[
{
"key": "temperature",
"value": 45
},
{
"key": "humidity",
"value": 67
}
]
Sugerencia
Este ejemplo simplemente extrae la matriz y descarta cualquier otra información del mensaje. Para conservar el mensaje general, pero intercambiar la estructura del .payload
a una matriz, use .payload |= to_entries
en su lugar.
Crear objetos
Los objetos se crean mediante sintaxis similar a JSON, donde puede proporcionar una combinación de información estática y dinámica.
En el ejemplo siguiente se muestra cómo reestructurar completamente un objeto mediante la creación de un nuevo objeto con campos cambiados y una estructura actualizada. Dada la entrada siguiente:
{
"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
}
}
Use la siguiente expresión jq para crear un objeto con la nueva estructura:
{
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"
}
En la expresión jq anterior:
{payload: {<fields>}}
crea un objeto con un campo literal denominadopayload
que es un objeto literal que contiene más campos. Este enfoque es la manera más básica de construir objetos.humidity: .payload.Payload["dtmi:com:prod1:slicer3345:humidity"].Value,
crea un nombre de clave estática con un valor calculado dinámicamente. El contexto de datos de todas las expresiones dentro de la construcción de objetos es la entrada completa de la expresión de construcción de objetos, en este caso el mensaje completo.(.payload.DataSetWriterName): "active"
es un ejemplo de una clave de objeto dinámico. En este ejemplo, el valor de.payload.DataSetWriterName
se asigna a un valor estático. Use claves y valores estáticos y dinámicos en cualquier combinación al crear un objeto.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"humidity": 10,
"lineStatus": [1, 5, 2],
"temperature": 46
},
"slicer-3345": "active"
}
Agregar campos a un objeto
Puede aumentar un objeto agregando campos para proporcionar contexto adicional para los datos. Use una asignación a un campo que no exista.
En el ejemplo siguiente se muestra cómo agregar un campo averageVelocity
a la carga útil. Dada la entrada siguiente:
{
"payload": {
"totalDistance": 421,
"elapsedTime": 1598
}
}
Use la siguiente expresión jq para agregar un campo averageVelocity
a la carga útil:
.payload.averageVelocity = (.payload.totalDistance / .payload.elapsedTime)
A diferencia de otros ejemplos que usan el símbolo |=
, en este ejemplo se usa una asignación estándar, =
. Por lo tanto, no limita la expresión en el lado derecho al campo de la izquierda. Este enfoque es necesario para que pueda acceder a otros campos en la carga.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"totalDistance": 421,
"elapsedTime": 1598,
"averageVelocity": 0.2634543178973717
}
}
Agregar campos condicionalmente a un objeto
La combinación de lógica condicional con la sintaxis para agregar campos a un objeto permite escenarios como agregar valores predeterminados para los campos que no están presentes.
En el ejemplo siguiente se muestra cómo agregar una unidad a cualquier medida de temperatura que no tenga una. La unidad predeterminada es celsius. Dada la entrada siguiente:
{
"payload": [
{
"timestamp": 1689712296407,
"temperature": 59.2,
"unit": "fahrenheit"
},
{
"timestamp": 1689712399609,
"temperature": 52.2
},
{
"timestamp": 1689712400342,
"temperature": 50.8,
"unit": "celsius"
}
]
}
Use la siguiente expresión jq para agregar una unidad a las medidas de temperatura que no tengan una:
.payload |= map(.unit //= "celsius")
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.map(<expression>)
ejecuta<expression>
en cada entrada de la matriz y reemplaza el valor de entrada por cualquier<expression>
que genere..unit //= "celsius"
usa la asignación especial//=
. Esta asignación combina (=
) con el operador alternativo (//
) para asignar el valor existente de.unit
a sí mismo si no esfalse
onull
. Si.unit
es false o null, la expresión asigna"celsius"
como valor de.unit
, creando.unit
si es necesario.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": [
{
"timestamp": 1689712296407,
"temperature": 59.2,
"unit": "fahrenheit"
},
{
"timestamp": 1689712399609,
"temperature": 52.2,
"unit": "celsius"
},
{
"timestamp": 1689712400342,
"temperature": 50.8,
"unit": "celsius"
}
]
}
Quitar campos de un objeto
Use la función del
para quitar campos innecesarios de un objeto.
En el ejemplo siguiente se muestra cómo quitar el campo timestamp
porque no es relevante para el resto del cálculo. Dada la entrada siguiente:
{
"payload": {
"timestamp": "2023-07-18T20:57:23.340Z",
"temperature": 153,
"pressure": 923,
"humidity": 24
}
}
Use la siguiente expresión jq quita el campo timestamp
:
del(.payload.timestamp)
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"temperature": 153,
"pressure": 923,
"humidity": 24
}
}
Trabajo con matrices
Las matrices son el bloque de creación principal para la iteración y la división de mensajes en jq. En los ejemplos siguientes se muestra cómo manipular matrices.
Extracción de valores de una matriz
Las matrices son más difíciles de inspeccionar que los objetos porque los datos se pueden ubicar en índices diferentes de la matriz en mensajes diferentes. Por lo tanto, para extraer valores de una matriz, a menudo tiene que buscar en la matriz los datos que necesita.
En el ejemplo siguiente se muestra cómo extraer algunos valores de una matriz para crear un nuevo objeto que contenga los datos que le interesen. Dada la entrada siguiente:
{
"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"
}
}
Use la siguiente expresión jq para extraer los valores timestamp
, temperature
, humidity
y pressure
de la matriz para crear un nuevo objeto:
.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,
}
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.{timestamp, <other-fields>}
es abreviada paratimestamp: .timestamp
, que agrega la marca de tiempo como un campo al objeto utilizando el campo del mismo nombre del objeto original.<other-fields>
agrega más campos al objeto.temperature: <expression>, humidity: <expression>, pressure: <expression>
establecer temperatura, humedad y presión en el objeto resultante en función de los resultados de las tres expresiones..data | <expression>
limita el cálculo del valor a la matriz dedata
la carga y ejecuta<expression>
en la matriz.map(<expression>)[0]?.value
hace varias cosas:map(<expression>)
ejecuta<expression>
en cada elemento de la matriz que devuelve el resultado de ejecutar esa expresión en cada elemento.[0]
extrae el primer elemento de la matriz resultante.?
habilita el encadenamiento adicional de un segmento de ruta de acceso, incluso si el valor anterior es null. Cuando el valor anterior es null, la ruta de acceso posterior también devuelve null en lugar de con errores..value
extrae el campovalue
del resultado.
select(.field == "dtmi:com:prod1:slicer3345:temperature")
ejecuta la expresión booleana dentro deselect()
en la entrada. Si el resultado es true, la entrada se pasa a través. Si el resultado es false, se quita la entrada.map(select(<expression>))
es una combinación común que se usa para filtrar elementos de una matriz.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"timestamp": "2023-07-18T20:57:23.340Z",
"temperature": 46,
"humidity": 10,
"pressure": null
}
}
Cambiar entradas de matriz
Modifique las entradas de una matriz con una expresión map()
. Use estas expresiones para modificar cada elemento de la matriz.
En el ejemplo siguiente se muestra cómo convertir la marca de tiempo de cada entrada de la matriz de un tiempo de milisegundos de unix a una cadena de RFC3339. Dada la entrada siguiente:
{
"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
}
]
}
Use la siguiente expresión jq para convertir la marca de tiempo de cada entrada de la matriz de un tiempo de milisegundos de unix a una cadena de RFC3339:
.payload |= map(.timestamp |= (. / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ")))
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.map(<expression>)
ejecuta<expression>
en cada elemento de la matriz, reemplazando cada uno por la salida de la ejecución de<expression>
..timestamp |= <expression>
establece la marca de tiempo en un nuevo valor basado en la ejecución de<expression>
, donde el contexto de datos de<expression>
es el valor de.timestamp
.(. / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ"))
convierte el tiempo de milisegundos en segundos y usa un formateador de cadena de tiempo para generar una marca de tiempo ISO 8601.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"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
}
]
}
Conversión de una matriz en un objeto
Para reestructurar una matriz en un objeto para que sea más fácil tener acceso o se ajuste a un esquema deseado, use from_entries
. Dada la entrada siguiente:
{
"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
}
]
}
Use la siguiente expresión jq para convertir la matriz en un objeto :
.payload |= (
map({key: .field, value: {timestamp, value}})
| from_entries
)
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.map({key: <expression>, value: <expression>})
convierte cada elemento de la matriz en un objeto del formulario{"key": <data>, "value": <data>}
, que es la estructurafrom_entries
necesita.{key: .field, value: {timestamp, value}}
crea un objeto a partir de una entrada de matriz, asignandofield
a la clave y creando un valor que contienetimestamp
yvalue
.{timestamp, value}
es abreviada para{timestamp: .timestamp, value: .value}
.<expression> | from_entries
convierte un<expression>
con valores de matriz en un objeto, asignando el campokey
de cada entrada de matriz a la clave de objeto y el campovalue
de cada entrada de matriz al valor de esa clave.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"humidity": {
"timestamp": 1689723806615,
"value": 10
},
"lineStatus": {
"timestamp": 1689723849747,
"value": [1, 5, 2]
},
"speed": {
"timestamp": 1689723868830,
"value": 85
},
"temperature": {
"timestamp": 1689723880530,
"value": 46
}
}
}
Creación de matrices
La creación de literales de matriz es similar a la creación de literales de objeto. La sintaxis jq de un literal de matriz es similar a JSON y JavaScript.
En el ejemplo siguiente se muestra cómo extraer algunos valores en una matriz simple para su procesamiento posterior.
Dada la entrada siguiente:
{
"payload": {
"temperature": 14,
"humidity": 56,
"pressure": 910
}
}
Use la siguiente expresión jq crea una matriz a partir de los valores de los campos temperature
, humidity
y pressure
:
.payload |= ([.temperature, .humidity, .pressure])
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": [14, 56, 910]
}
Agregar entradas a una matriz
Puede agregar entradas al principio o al final de una matriz mediante el operador +
con la matriz y sus nuevas entradas. El operador +=
simplifica esta operación actualizando automáticamente la matriz con las nuevas entradas al final. Dada la entrada siguiente:
{
"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
}
}
Use la siguiente expresión jq para agregar los valores 12
y 41
al final de la matriz de valores de lineStatus
:
.payload.Payload["dtmi:com:prod1:slicer3345:lineStatus"].Value += [12, 41]
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"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
}
}
Quitar entradas de una matriz
Use la función del
para quitar entradas de una matriz de la misma manera que para un objeto. Dada la entrada siguiente:
{
"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
}
}
Use la siguiente expresión jq para quitar la segunda entrada de la matriz de valores de lineStatus
:
del(.payload.Payload["dtmi:com:prod1:slicer3345:lineStatus"].Value[1])
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"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
}
}
Quitar entradas de matriz duplicadas
Si los elementos de matriz se superponen, puede quitar las entradas duplicadas. En la mayoría de los lenguajes de programación, puede quitar duplicados mediante variables de búsqueda en paralelo. En jq, el mejor enfoque es organizar los datos en cómo se debe procesar y, a continuación, realizar cualquier operación antes de convertirlo de nuevo al formato deseado.
En el ejemplo siguiente se muestra cómo tomar un mensaje con algunos valores en él y, a continuación, filtrarlo para que solo tenga la lectura más reciente para cada valor. Dada la entrada siguiente:
{
"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
}
]
}
Use la siguiente expresión jq para filtrar la entrada para que solo tenga la lectura más reciente para cada valor:
.payload |= (group_by(.name) | map(sort_by(.timestamp)[-1]))
Sugerencia
Si no le interesa recuperar el valor más reciente de cada nombre, puede simplificar la expresión para .payload |= unique_by(.name)
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.- Dada una matriz como entrada,
group_by(.name)
coloca los elementos en submatrices en función del valor de.name
en cada elemento. Cada submatriz contiene todos los elementos de la matriz original con el mismo valor de.name
. map(<expression>)
toma la matriz de matrices producidas porgroup_by
y ejecuta<expression>
en cada una de las submatrices.sort_by(.timestamp)[-1]
extrae el elemento que le interesa de cada submatriz:sort_by(.timestamp)
ordena los elementos aumentando el valor de su campo de.timestamp
para la submatriz actual.[-1]
recupera el último elemento de la submatriz ordenada, que es la entrada con la hora más reciente de cada nombre.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": [
{
"name": "humidity",
"value": 51,
"timestamp": 1689727944440
},
{
"name": "temperature",
"value": 15,
"timestamp": 1689727994085
}
]
}
Valores de proceso entre elementos de matriz
Puede combinar los valores de los elementos de matriz para calcular valores como los promedios en los elementos.
En este ejemplo se muestra cómo reducir la matriz recuperando la marca de tiempo más alta y el valor medio de las entradas que comparten el mismo nombre. Dada la entrada siguiente:
{
"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
}
]
}
Use la siguiente expresión jq para recuperar la marca de tiempo más alta y el valor medio de las entradas que comparten el mismo nombre:
.payload |= (group_by(.name) | map(
{
name: .[0].name,
value: map(.value) | (add / length),
timestamp: map(.timestamp) | max
}
))
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.group_by(.name)
toma una matriz como entrada, coloca los elementos en submatrices en función del valor de.name
en cada elemento. Cada submatriz contiene todos los elementos de la matriz original con el mismo valor de.name
.map(<expression>)
toma la matriz de matrices producidas porgroup_by
y ejecuta<expression>
en cada una de las submatrices.{name: <expression>, value: <expression>, timestamp: <expression>}
construye un objeto de la submatriz de entrada con los camposname
,value
ytimestamp
. Cada<expression>
genera el valor deseado para la clave asociada..[0].name
recupera el primer elemento de la submatriz y extrae el camponame
de él. Todos los elementos de la submatriz tienen el mismo nombre, por lo que solo necesita recuperar el primero.map(.value) | (add / length)
calcula el promedio devalue
de cada submatriz:map(.value)
convierte la submatriz en una matriz del campovalue
en cada entrada, en este caso devolviendo una matriz de números.add
es una función jq integrada que calcula la suma de una matriz de números.length
es una función jq integrada que calcula el recuento o la longitud de una matriz.add / length
divide la suma por el recuento para determinar el promedio.
map(.timestamp) | max
busca el valor máximotimestamp
de cada submatriz:map(.timestamp)
convierte la submatriz en una matriz de los campostimestamp
de cada entrada, en este caso devolviendo una matriz de números.max
es una función jq integrada que encuentra el valor máximo en una matriz.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": [
{
"name": "humidity",
"value": 38,
"timestamp": 1689727944440
},
{
"name": "temperature",
"value": 19.333333333333332,
"timestamp": 1689727994085
}
]
}
Operaciones con cadenas
jq proporciona varias utilidades para manipular y construir cadenas. En los ejemplos siguientes se muestran algunos casos de uso comunes.
Dividir cadenas
Si una cadena contiene varios fragmentos de información separados por un carácter común, puede usar la función split()
para extraer las partes individuales.
En el ejemplo siguiente se muestra cómo dividir una cadena de tema y devolver un segmento específico del tema. Esta técnica suele ser útil cuando se trabaja con expresiones de clave de partición. Dada la entrada siguiente:
{
"systemProperties": {
"timestamp": "2023-01-11T10:02:07Z"
},
"qos": 1,
"topic": "assets/slicer-3345/tags/rpm",
"properties": {
"contentType": "application/json"
},
"payload": {
"Timestamp": 1681926048,
"Value": 142
}
}
Use la siguiente expresión jq para dividir la cadena de tema, usando /
como separador y devolver un segmento específico del tema:
.topic | split("/")[1]
En la expresión jq anterior:
.topic | <expression>
selecciona la clavetopic
del objeto raíz y ejecuta<expression>
en los datos que contiene.split("/")
divide la cadena de tema en una matriz dividiendo la cadena separada cada vez que encuentra/
carácter en la cadena. En este caso, genera["assets", "slicer-3345", "tags", "rpm"]
.[1]
recupera el elemento en el índice 1 de la matriz del paso anterior, en este casoslicer-3345
.
El siguiente JSON muestra la salida de la expresión jq anterior:
"slicer-3345"
Construir cadenas dinámicamente
jq permite construir cadenas mediante plantillas de cadena con la sintaxis \(<expression>)
dentro de una cadena. Use estas plantillas para compilar cadenas dinámicamente.
En el ejemplo siguiente se muestra cómo agregar un prefijo a cada clave de un objeto mediante plantillas de cadena. Dada la entrada siguiente:
{
"temperature": 123,
"humidity": 24,
"pressure": 1021
}
Use la siguiente expresión jq para agregar un prefijo a cada clave del objeto :
with_entries(.key |= "current-\(.)")
En la expresión jq anterior:
with_entries(<expression>)
convierte el objeto en una matriz de pares clave-valor con estructura{key: <key>, value: <value>}
, ejecuta<expression>
en cada par clave-valor y convierte los pares en un objeto..key |= <expression>
actualiza el valor de.key
en el objeto del par clave-valor al resultado de<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
en el valor de.key
, en lugar del objeto de par clave-valor completo."current-\(.)"
genera una cadena que comienza con "current-" y, a continuación, inserta el valor del contexto de datos actual.
, en este caso el valor de la clave. La sintaxis\(<expression>)
dentro de la cadena indica que desea reemplazar esa parte de la cadena por el resultado de ejecutar<expression>
.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"current-temperature": 123,
"current-humidity": 24,
"current-pressure": 1021
}
Trabajar con expresiones regulares
jq admite expresiones regulares estándar. Puede usar expresiones regulares para extraer, reemplazar y comprobar patrones dentro de cadenas. Las funciones de expresión regular comunes para jq incluyen test()
, match()
, split()
, capture()
, sub()
, y gsub()
.
Extracción de valores mediante expresiones regulares
Si no puede usar la separación de cadenas para extraer un valor de una cadena, use expresiones regulares para extraer los valores que necesita.
En el ejemplo siguiente se muestra cómo normalizar las claves de objeto mediante las pruebas de una expresión regular y, a continuación, reemplazarlas por un formato diferente. Dada la entrada siguiente:
{
"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
}
}
Use la siguiente expresión jq para normalizar las claves de objeto:
.payload.Payload |= with_entries(
.key |= if test("^dtmi:.*:(?<tag>[^:]+)$") then
capture("^dtmi:.*:(?<tag>[^:]+)$").tag
else
.
end
)
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.with_entries(<expression>)
convierte el objeto en una matriz de pares clave-valor con estructura{key: <key>, value: <value>}
, ejecuta<expression>
en cada par clave-valor y convierte los pares en un objeto..key |= <expression>
actualiza el valor de.key
en el objeto del par clave-valor al resultado de<expression>
. usando|=
en lugar de=
establece el contexto de datos de<expression>
en el valor de.key
, en lugar del objeto de par clave-valor completo.if test("^dtmi:.*:(?<tag>[^:]+)$") then capture("^dtmi:.*:(?<tag>[^:]+)$").tag else . end
comprueba y actualiza la clave en función de una expresión regular:test("^dtmi:.*:(?<tag>[^:]+)$")
comprueba el contexto de datos de entrada, la clave en este caso, en la expresión regular^dtmi:.*:(?<tag>[^:]+)$
. Si la expresión regular coincide, devuelve true. Si no es así, devuelve false.capture("^dtmi:.*:(?<tag>[^:]+)$").tag
ejecuta la expresión regular^dtmi:.*:(?<tag>[^:]+)$
en el contexto de datos de entrada, la clave en este caso y coloca los grupos de captura de la expresión regular, indicados por(?<tag>...)
, en un objeto como salida. A continuación, la expresión extrae.tag
de ese objeto para devolver la información extraída por la expresión regular..
en la ramaelse
, la expresión pasa los datos sin cambios.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"Timestamp": 1681926048,
"Payload": {
"humidity": {
"SourceTimestamp": 1681926048,
"Value": 10
},
"speed": {
"SourceTimestamp": 1681926048,
"Value": 85
},
"temperature": {
"SourceTimestamp": 1681926048,
"Value": 46
}
},
"DataSetWriterName": "slicer-3345",
"SequenceNumber": 461092
}
}
Separación de mensajes
Una característica útil del lenguaje jq es su capacidad para generar varias salidas a partir de una sola entrada. Esta característica permite dividir los mensajes en varios mensajes independientes para que la canalización se procese. La clave de esta técnica es .[]
, que divide las matrices en valores independientes. En los ejemplos siguientes se muestran algunos escenarios que usan esta sintaxis.
Número dinámico de salidas
Normalmente, cuando desea dividir un mensaje en varias salidas, el número de salidas que desee depende de la estructura del mensaje. La sintaxis []
le permite realizar este tipo de división.
Por ejemplo, tiene un mensaje con una lista de etiquetas que desea colocar en mensajes independientes. Dada la entrada siguiente:
{
"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
}
}
Use la siguiente expresión jq para dividir el mensaje en varios mensajes:
.payload.Payload = (.payload.Payload | to_entries[])
| .payload |= {
DataSetWriterName,
SequenceNumber,
Tag: .Payload.key,
Value: .Payload.value.value,
Timestamp: .Payload.value.sourceTimestamp
}
En la expresión jq anterior:
.payload.Payload = (.payload.Payload | to_entries[])
divide el mensaje en varios mensajes:.payload.Payload = <expression>
asigna el resultado de ejecutar<expression>
a.payload.Payload
. Normalmente, se usa|=
en este caso para limitar el contexto de<expression>
a.payload.Payload
, pero|=
no admite la división del mensaje, por lo que debe usar=
en su lugar.(.payload.Payload | <expression>)
limita el lado derecho de la expresión de asignación hasta.payload.Payload
para que<expression>
funcione con la parte correcta del mensaje.to_entries[]
es dos operaciones y es una abreviatura deto_entries | .[]
:to_entries
convierte el objeto en una matriz de pares clave-valor con esquema{"key": <key>, "value": <value>}
. Esta información es lo que desea separar en mensajes diferentes.[]
realiza la división de mensajes. Cada entrada de la matriz se convierte en un valor independiente en jq. Cuando se produce la asignación a.payload.Payload
, cada valor independiente da como resultado una copia del mensaje general que se realiza, con.payload.Payload
establecido en el valor correspondiente generado por el lado derecho de la asignación.
.payload |= <expression>
reemplaza el valor de.payload
por el resultado de ejecutar<expression>
. En este punto, la consulta está tratando con una secuencia de valores en lugar de un solo valor como resultado de la división en la operación anterior. Por lo tanto, la asignación se ejecuta una vez para cada mensaje que genera la operación anterior en lugar de simplemente ejecutarse una vez en general.{DataSetWriterName, SequenceNumber, ...}
construye un nuevo objeto que es el valor de.payload
.DataSetWriterName
ySequenceNumber
no se modifican, por lo que puede usar la sintaxis abreviada en lugar de escribirDataSetWriterName: .DataSetWriterName
ySequenceNumber: .SequenceNumber
.Tag: .Payload.key,
extrae la clave de objeto original delPayload
interno y los niveles superiores al objeto primario. La operaciónto_entries
anterior en la consulta creó el campokey
.Value: .Payload.value.value
y realizanTimestamp: .Payload.value.sourceTimestamp
una extracción similar de datos de la carga interna. Esta vez a partir del valor del par clave-valor original. El resultado es un objeto de carga plana que se puede usar en un procesamiento posterior.
En el siguiente JSON se muestran las salidas de la expresión jq anterior. Cada salida se convierte en un mensaje independiente para las fases de procesamiento posteriores de la canalización:
{
"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
}
}
Número fijo de salidas
Para dividir un mensaje en un número fijo de salidas en lugar de un número dinámico de salidas en función de la estructura del mensaje, use el operador ,
en lugar de []
.
En el ejemplo siguiente se muestra cómo dividir los datos en dos mensajes en función de los nombres de campo existentes. Dada la entrada siguiente:
{
"topic": "test/topic",
"payload": {
"minTemperature": 12,
"maxTemperature": 23,
"minHumidity": 52,
"maxHumidity": 92
}
}
Use la siguiente expresión jq para dividir el mensaje en dos mensajes:
.payload = (
{
field: "temperature",
minimum: .payload.minTemperature,
maximum: .payload.maxTemperature
},
{
field: "humidity",
minimum: .payload.minHumidity,
maximum: .payload.maxHumidity
}
)
En la expresión jq anterior:
.payload = ({<fields>},{<fields>})
asigna los dos literales de objeto a.payload
en el mensaje. Los objetos separados por comas generan dos valores independientes y se asignan a.payload
, lo que hace que todo el mensaje se divida en dos mensajes. Cada mensaje nuevo tiene.payload
establecido en uno de los valores.{field: "temperature", minimum: .payload.minTemperature, maximum: .payload.maxTemperature}
es un constructor de objeto literal que rellena los campos de un objeto con una cadena literal y otros datos capturados del objeto.
En el siguiente JSON se muestran las salidas de la expresión jq anterior. Cada salida se convierte en un mensaje independiente para realizar más fases de procesamiento:
{
"topic": "test/topic",
"payload": {
"field": "temperature",
"minimum": 12,
"maximum": 23
}
}
{
"topic": "test/topic",
"payload": {
"field": "humidity",
"minimum": 52,
"maximum": 92
}
}
Operaciones matemáticas
jq admite operaciones matemáticas comunes. Algunas operaciones son operadores como +
y -
. Otras operaciones son funciones como sin
y exp
.
Aritméticos
jq admite cinco operaciones aritméticas comunes: suma (+
), resta (-
), multiplicación (*
), división (/
) y módulo (%
). A diferencia de muchas características de jq, estas operaciones son operaciones de infijo que permiten escribir la expresión matemática completa en una sola expresión sin separadores de |
.
En el ejemplo siguiente se muestra cómo convertir una temperatura de fahrenheit a celsius y extraer la lectura de segundos actuales de una marca de tiempo de milisegundos unix. Dada la entrada siguiente:
{
"payload": {
"temperatureF": 94.2,
"timestamp": 1689766750628
}
}
Use la siguiente expresión jq para convertir la temperatura de fahrenheit a celsius y extraer los segundos actuales leídos de una marca de tiempo de milisegundos unix:
.payload.temperatureC = (5/9) * (.payload.temperatureF - 32)
| .payload.seconds = (.payload.timestamp / 1000) % 60
En la expresión jq anterior:
.payload.temperatureC = (5/9) * (.payload.temperatureF - 32)
crea un nuevo campo detemperatureC
en la carga que se establece en la conversión detemperatureF
de Fahrenheit a Celsius..payload.seconds = (.payload.timestamp / 1000) % 60
tarda un tiempo de milisegundos unix y lo convierte en segundos y, a continuación, extrae el número de segundos en el minuto actual mediante un cálculo de módulo.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"temperatureF": 94.2,
"timestamp": 1689766750628,
"temperatureC": 34.55555555555556,
"seconds": 10
}
}
Funciones matemáticas
jq incluye varias funciones que realizan operaciones matemáticas. Puede encontrar la lista completa en el manual de jq.
En el ejemplo siguiente se muestra cómo calcular la energía cinética a partir de campos de masa y velocidad. Dada la entrada siguiente:
{
"userProperties": [
{ "key": "mass", "value": 512.1 },
{ "key": "productType", "value": "projectile" }
],
"payload": {
"velocity": 97.2
}
}
Use la siguiente expresión jq para calcular la energía cinética a partir de los campos de masa y velocidad:
.payload.energy = (0.5 * (.userProperties | from_entries).mass * pow(.payload.velocity; 2) | round)
En la expresión jq anterior:
.payload.energy = <expression>
crea un nuevo campo deenergy
en la carga que es el resultado de ejecutar<expression>
.(0.5 * (.userProperties | from_entries).mass * pow(.payload.velocity; 2) | round)
es la fórmula de energía:(.userProperties | from_entries).mass
extrae la entradamass
de la lista deuserProperties
. Los datos ya están configurados como objetos conkey
yvalue
, por lo quefrom_entries
puede convertirlo directamente en un objeto. La expresión recupera la clavemass
del objeto resultante y devuelve su valor.pow(.payload.velocity; 2)
extrae la velocidad de la carga útil y la eleva a la potencia de 2.<expression> | round
redondea el resultado al número entero más cercano para evitar una precisión engañosamente alta en el resultado.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"userProperties": [
{ "key": "mass", "value": 512.1 },
{ "key": "productType", "value": "projectile" }
],
"payload": {
"velocity": 97.2,
"energy": 2419119
}
}
Lógica booleana
Las canalizaciones de procesamiento de datos suelen usar jq para filtrar mensajes. El filtrado normalmente usa expresiones booleanas y operadores. Además, la lógica booleana es útil para realizar el flujo de control en transformaciones y casos de uso de filtrado más avanzados.
En los ejemplos siguientes se muestran algunas de las funciones más comunes que se usan en expresiones booleanas en jq:
Operadores booleanos y condicionales básicos
jq proporciona los operadores lógicos booleanos básicos and
, or
, y not
. Los operadores and
y or
son operadores infijos. not
es una función que se invoca como filtro, por ejemplo, <expression> | not
.
jq tiene los operadores condicionales >
, <
, ==
, !=
, >=
, y <=
. Estos operadores son operadores de infijo.
En el ejemplo siguiente se muestra cómo realizar alguna lógica booleana básica mediante condicionales. Dada la entrada siguiente:
{
"payload": {
"temperature": 50,
"humidity": 92,
"site": "Redmond"
}
}
Use la siguiente expresión jq para comprobar si:
- La temperatura está entre 30 grados y 60 grados inclusive en el límite superior.
- La humedad es inferior a 80 y el sitio es Redmond.
.payload
| ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond"
| not
En la expresión jq anterior:
.payload | <expression>
ámbitos<expression>
al contenido de.payload
. Esta sintaxis hace que el resto de la expresión sea menos detallada.((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond"
devuelve true si la temperatura está entre 30 grados y 60 grados (inclusive en el límite superior) o la humedad es inferior a 80, solo devuelve true si el sitio también es Redmond.<expression> | not
toma el resultado de la expresión anterior y aplica un valor NOT lógico, en este ejemplo se invierte el resultado detrue
afalse
.
El siguiente JSON muestra la salida de la expresión jq anterior:
false
Comprobación de la existencia de la clave de objeto
Puede crear un filtro que compruebe la estructura de un mensaje en lugar de su contenido. Por ejemplo, podría comprobar si una clave determinada está presente en un objeto. Para ello, use la función has
o una comprobación con null. En el ejemplo siguiente se muestran ambos enfoques. Dada la entrada siguiente:
{
"payload": {
"temperature": 51,
"humidity": 41,
"site": null
}
}
Use la siguiente expresión jq para comprobar si la carga tiene un campo temperature
, si el campo site
no es null y otras comprobaciones:
.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)
}
En la expresión jq anterior:
.payload | <expression>
limita el contexto de datos de<expression>
al valor de.payload
para que<expression>
menos detallado.hasTemperature: has("temperature"),
esta y otras expresiones similares muestran cómo se comporta la funciónhas
con un objeto de entrada. La función devuelve true solo si la clave está presente.hasSite
es true a pesar del valor desite
sernull
.temperatureNotNull: (.temperature != null),
esta y otras expresiones similares muestran cómo la comprobación de!= null
realiza una comprobación similar ahas
. Una clave inexistente de un objeto senull
si se tiene acceso mediante la sintaxis de.<key>
o existe una clave, pero tiene un valor denull
. TantositeNotNull
ymissingNotNull
son false, aunque hay una clave presente y la otra está ausente.hasNested: (has("nested") and (.nested | has("inner")))
realiza una comprobación en un objeto anidado conhas
, donde es posible que el objeto primario no exista. El resultado es una cascada de comprobaciones en cada nivel para evitar un error.nestedNotNull: (.nested?.inner != null)
realiza la misma comprobación en un objeto anidado mediante!= null
y el?
para habilitar el encadenamiento de rutas de acceso en campos que pueden no existir. Este enfoque genera una sintaxis más limpia para cadenas profundamente anidadas que pueden existir o no, pero no puede diferenciar valores clave denull
de aquellos que no existen.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"hasTemperature": true,
"temperatureNotNull": true,
"hasSite": true,
"siteNotNull": false,
"hasMissing": false,
"missingNotNull": false,
"hasNested": false,
"nestedNotNull": false
}
Comprobación de la existencia de entradas de matriz
Use la función any
para comprobar la existencia de una entrada en una matriz. Dada la entrada siguiente:
{
"userProperties": [
{ "key": "mass", "value": 512.1 },
{ "key": "productType", "value": "projectile" }
],
"payload": {
"velocity": 97.2,
"energy": 2419119
}
}
Use la siguiente expresión jq para comprobar si la matriz de userProperties
tiene una entrada con una clave de mass
y ninguna entrada con una clave de missing
:
.userProperties | any(.key == "mass") and (any(.key == "missing") | not)
En la expresión jq anterior:
.userProperties | <expression>
limita el contexto de datos de<expression>
al valor deuserProperties
para que el resto de<expression>
menos detallados.any(.key == "mass")
ejecuta la expresión.key == "mass"
en cada elemento de la matriz deuserProperties
, devolviendo true si la expresión se evalúa como true para al menos un elemento de la matriz.(any(.key == "missing") | not)
ejecuta.key == "missing"
en cada elemento de la matriz deuserProperties
, devolviendo true si algún elemento se evalúa como true y, a continuación, niega el resultado general con| not
.
El siguiente JSON muestra la salida de la expresión jq anterior:
true
Flujo de control
El flujo de control en jq es diferente de la mayoría de los lenguajes, ya que la mayoría de las formas de flujo de control están directamente controladas por datos. Todavía hay compatibilidad con expresiones if/else con semántica de programación funcional tradicional, pero puede lograr la mayoría de las estructuras de bucle mediante combinaciones de las funciones map
y reduce
.
En los ejemplos siguientes se muestran algunos escenarios comunes de flujo de control en jq.
Instrucciones If-else
jq admite condiciones mediante if <test-expression> then <true-expression> else <false-expression> end
. Puede insertar más casos agregando elif <test-expression> then <true-expression>
en el medio. Una diferencia clave entre jq y muchos otros lenguajes es que cada expresión then
y else
genera un resultado que se usa en las operaciones posteriores en la expresión jq general.
En el ejemplo siguiente se muestra cómo usar instrucciones if
para generar información condicional. Dada la entrada siguiente:
{
"payload": {
"temperature": 25,
"humidity": 52
}
}
Use la siguiente expresión jq para comprobar si la temperatura es alta, baja o normal:
.payload.status = if .payload.temperature > 80 then
"high"
elif .payload.temperature < 30 then
"low"
else
"normal"
end
En la expresión jq anterior:
.payload.status = <expression>
asigna el resultado de ejecutar<expression>
a un nuevo campo destatus
en la carga.if ... end
es la expresión principalif/elif/else
:if .payload.temperature > 80 then "high"
comprueba la temperatura con un valor alto, devolviendo"high"
si es true; de lo contrario, continúa.elif .payload.temperature < 30 then "low"
realiza una segunda comprobación con respecto a la temperatura de un valor bajo, estableciendo el resultado en"low"
si es true; de lo contrario, continúa.else "normal" end
devuelve"normal"
si ninguna de las comprobaciones anteriores era true y cierra la expresión conend
.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"temperature": 25,
"humidity": 52,
"status": "low"
}
}
Asignar
En lenguajes funcionales como jq, la forma más común de realizar lógica iterativa es crear una matriz y, a continuación, asignar los valores de esa matriz a otros nuevos. Esta técnica se logra en jq mediante la función map
, que aparece en muchos de los ejemplos de esta guía. Si desea realizar alguna operación con varios valores, es probable que map
sea la respuesta.
En el ejemplo siguiente se muestra cómo usar map
para quitar un prefijo de las claves de un objeto. Esta solución se puede escribir de forma más concisa mediante with_entries
, pero la versión más detallada que se muestra aquí muestra la asignación real que se produce bajo el capó en el enfoque abreviado. Dada la entrada siguiente:
{
"payload": {
"rotor_rpm": 150,
"rotor_temperature": 51,
"rotor_cycles": 1354
}
}
Use la siguiente expresión jq para quitar el prefijo rotor_
de las claves de la carga:
.payload |= (to_entries | map(.key |= ltrimstr("rotor_")) | from_entries)
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.(to_entries | map(<expression) | from_entries)
realiza la conversión de matriz de objetos y asigna cada entrada a un nuevo valor con<expression>
. Este enfoque es semánticamente equivalente awith_entries(<expression>)
:to_entries
convierte un objeto en una matriz, con cada par clave-valor convirtiéndose en un objeto independiente con estructura{"key": <key>, "value": <value>}
.map(<expression>)
ejecuta<expression>
en cada elemento de la matriz y genera una matriz de salida con los resultados de cada expresión.from_entries
es el inverso deto_entries
. La función convierte una matriz de objetos con estructura{"key": <key>, "value": <value>}
en un objeto con los camposkey
yvalue
asignados a pares clave-valor.
.key |= ltrimstr("rotor_")
actualiza el valor de.key
en cada entrada con el resultado deltrimstr("rotor_")
. La sintaxis|=
limita el contexto de datos del lado derecho al valor de.key
.ltrimstr
quita el prefijo especificado de la cadena si está presente.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": {
"rpm": 150,
"temperature": 51,
"cycles": 1354
}
}
Reducir
Reducir es la forma principal de realizar operaciones iterativas o de bucle en los elementos de una matriz. La operación de reducción consta de un acumulador y una operación que usa el acumulador y el elemento actual de la matriz como entradas. Cada iteración del bucle devuelve el siguiente valor del acumulador y la salida final de la operación de reducción es el último valor del acumulador. La reducción se conoce como plegado en otros lenguajes de programación funcionales.
Use la operación reduce
en jq para realizar la reducción. La mayoría de los casos de uso no necesitan esta manipulación de bajo nivel y, en su lugar, pueden usar funciones de nivel superior, pero reduce
es una herramienta general útil.
En el ejemplo siguiente se muestra cómo calcular el cambio medio en el valor de una métrica en los puntos de datos que tiene. Dada la entrada siguiente:
{
"payload": [
{
"value": 65,
"timestamp": 1689796743559
},
{
"value": 55,
"timestamp": 1689796771131
},
{
"value": 59,
"timestamp": 1689796827766
},
{
"value": 62,
"timestamp": 1689796844883
},
{
"value": 58,
"timestamp": 1689796864853
}
]
}
Use la siguiente expresión jq para calcular el cambio medio en el valor en los puntos de datos:
.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
)
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.reduce .[] as $item (<init>; <expression>)
es la scaffolding de una operación de reducción típica con las siguientes partes:.[] as $item
siempre debe ser<expression> as <variable>
y suele ser.[] as $item
. El<expression>
genera un flujo de valores, cada uno de los cuales se guarda en<variable>
para una iteración de la operación de reducción. Si tiene una matriz en la que desea iterar,.[]
la divide en una secuencia. Esta sintaxis es la misma que la sintaxis usada para dividir los mensajes, pero la operación dereduce
no usa la secuencia para generar varias salidas.reduce
no separa el mensaje.<init>
en este casonull
es el valor inicial del acumulador que se usa en la operación de reducción. Este valor suele establecerse en vacío o cero. Este valor se convierte en el contexto de datos,.
en este bucle<expression>
, para la primera iteración.<expression>
es la operación realizada en cada iteración de la operación de reducción. Tiene acceso al valor actual del acumulador, a través de.
, y el valor actual de la secuencia a través de la<variable>
declarada anteriormente, en este caso$item
.
if . == null then {totalChange: 0, previous: $item.value, count: 0}
es un condicional para controlar la primera iteración de reducción. Configura la estructura del acumulador para la siguiente iteración. Dado que la expresión calcula las diferencias entre las entradas, la primera entrada configura los datos que se usan para calcular una diferencia en la segunda iteración de reducción. Los campostotalChange
,previous
ycount
sirven como variables de bucle y se actualizan en cada iteración..totalChange += (($item.value - .previous) | length) | .previous = $item.value | .count += 1
es la expresión en el casoelse
. Esta expresión establece cada campo del objeto acumulador en un nuevo valor basado en un cálculo. ParatotalChange
, encuentra la diferencia entre los valores actuales y anteriores y obtiene el valor absoluto. Contra intuitivamente usa la funciónlength
para obtener el valor absoluto.previous
se establece en el$item
actual delvalue
para que se use la siguiente iteración y se incrementacount
..totalChange / .count
calcula el cambio medio en los puntos de datos una vez completada la operación de reducción y tiene el valor de acumulador final.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": 5.25
}
Bucles
Los bucles de jq normalmente se reservan para casos de uso avanzados. Dado que cada operación de jq es una expresión que genera un valor, la semántica controlada por instrucciones de bucles en la mayoría de los lenguajes no es un ajuste natural en jq. Considere la posibilidad de usar map
o reduce
para satisfacer sus necesidades.
Hay dos tipos principales de bucle tradicional en jq. Existen otros tipos de bucle, pero son para casos de uso más especializados:
while
aplica una operación repetidamente en el contexto de datos de entrada, actualizando el valor del contexto de datos para su uso en la siguiente iteración y produciendo ese valor como salida. La salida de un buclewhile
es una matriz que contiene los valores generados por cada iteración del bucle.until
comowhile
aplica una operación repetidamente en el contexto de datos de entrada, actualizando el valor del contexto de datos para su uso en la siguiente iteración. A diferencia dewhile
, el bucleuntil
genera el valor generado por la última iteración del bucle.
En el ejemplo siguiente se muestra cómo usar un bucle until
para eliminar progresivamente los puntos de datos atípicos de una lista de lecturas hasta que la desviación estándar cae por debajo de un valor predefinido. Dada la entrada siguiente:
{
"payload": [
{
"value": 65,
"timestamp": 1689796743559
},
{
"value": 55,
"timestamp": 1689796771131
},
{
"value": 59,
"timestamp": 1689796827766
},
{
"value": 62,
"timestamp": 1689796844883
},
{
"value": 58,
"timestamp": 1689796864853
}
]
}
Use la siguiente expresión jq para eliminar progresivamente los puntos de datos atípicos de una lista de lecturas hasta que la desviación estándar cae por debajo de 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
)
)
En la expresión jq anterior:
def avg: add / length;
define una nueva función denominadaavg
que se usa para calcular los promedios más adelante en la expresión. La expresión a la derecha de la:
es la expresión lógica que se usa siempre que se usaavg
. La expresión<expression> | avg
es equivalente a<expression> | add / length
def stdev: avg as $mean | (map(. - $mean | . * .) | add) / (length - 1) | sqrt;
define una nueva función denominadastdev
. La función calcula la desviación estándar de ejemplo de una matriz mediante una versión modificada de respuesta de la comunidad en StackOverflow..payload |= <expression>
los dos primerosdef
son solo declaraciones e inician la expresión real. La expresión ejecuta<expression>
con un objeto de datos de entrada de.payload
y asigna el resultado a.payload
.sort_by(.value)
ordena la matriz de entradas de matriz por su campo devalue
. Esta solución requiere que identifique y manipule los valores más altos y mínimos de una matriz, por lo que ordenar los datos de antemano reduce el cálculo y simplifica el código.until(<condition>; <expression>)
ejecuta<expression>
en la entrada hasta que<condition>
devuelve true. La entrada para cada ejecución de<expression>
y<condition>
es la salida de la ejecución anterior de<expression>
. El resultado de la última ejecución de<expression>
se devuelve del bucle.(map(.value) | stdev) < 2 or length == 0
es la condición del bucle:map(.value)
convierte la matriz en una lista de números puros para su uso en el cálculo posterior.(<expression> | stdev) < 2
calcula la desviación estándar de la matriz y devuelve true si la desviación estándar es inferior a 2.length == 0
obtiene la longitud de la matriz de entrada y devuelve true si es 0. Para protegerse contra el caso en el que se eliminan todas las entradas, el resultado seor
-ed con la expresión general.
(map(.value) | avg) as $avg
convierte la matriz en una matriz de números y calcula su promedio y, a continuación, guarda el resultado en una variable$avg
. Este enfoque ahorra costos de cálculo porque se reutiliza el promedio varias veces en la iteración del bucle. Las expresiones de asignación de variables no cambian el contexto de datos de la siguiente expresión después de|
, por lo que el resto del cálculo todavía tiene acceso a la matriz completa.if <condition> then <expression> else <expression> end
es la lógica principal de la iteración del bucle. Usa<condition>
para determinar el<expression>
para ejecutar y devolver.((.[0].value - $avg) | length) > ((.[-1].value - $avg) | length)
es la condiciónif
que compara los valores más altos y mínimos con el valor medio y, a continuación, compara esas diferencias:(.[0].value - $avg) | length
recupera el campovalue
de la primera entrada de matriz y obtiene la diferencia entre ella y el promedio general. La primera entrada de matriz es la más baja debido a la ordenación anterior. Este valor puede ser negativo, por lo que el resultado se canaliza alength
, que devuelve el valor absoluto cuando se proporciona un número como entrada.(.[-1].value - $avg) | length
realiza la misma operación en la última entrada de matriz y calcula también el valor absoluto para la seguridad. La última entrada de matriz es la más alta debido a la ordenación anterior. A continuación, los valores absolutos se comparan en la condición general mediante>
.
del(.[0])
es la expresiónthen
que se ejecuta cuando la primera entrada de matriz era el valor atípico más grande. La expresión quita el elemento en.[0]
de la matriz. La expresión devuelve los datos que quedan en la matriz después de la operación.del(.[-1])
es la expresiónelse
que se ejecuta cuando la última entrada de matriz era el valor atípico más grande. La expresión quita el elemento en.[-1]
, que es la última entrada, de la matriz. La expresión devuelve los datos que quedan en la matriz después de la operación.
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"payload": [
{
"value": 58,
"timestamp": 1689796864853
},
{
"value": 59,
"timestamp": 1689796827766
},
{
"value": 60,
"timestamp": 1689796844883
}
]
}
Quitar mensajes
Al escribir una expresión de filtro, puede indicar al sistema que quite los mensajes que no desee devolviendo false. Este comportamiento es el comportamiento básico de las expresiones condicionales en jq. Sin embargo, hay ocasiones en las que se transforman mensajes o se realizan filtros más avanzados cuando se quiere que el sistema quite mensajes explícitamente o implícitamente. En los ejemplos siguientes se muestra cómo implementar este comportamiento.
Colocación explícita
Para quitar explícitamente un mensaje en una expresión de filtro, devuelva false
de la expresión.
También puede quitar un mensaje desde dentro de una transformación mediante la función builtin empty
en jq.
En el ejemplo siguiente se muestra cómo calcular un promedio de puntos de datos en el mensaje y quitar los mensajes con un promedio por debajo de un valor fijo. Es posible y válido lograr este comportamiento con la combinación de una fase de transformación y una fase de filtro. Use el enfoque que mejor se adapte a su situación. Dadas las siguientes entradas:
Mensaje 1
{
"payload": {
"temperature": [23, 42, 63, 61],
"humidity": [64, 36, 78, 33]
}
}
Mensaje 2
{
"payload": {
"temperature": [42, 12, 32, 21],
"humidity": [92, 63, 57, 88]
}
}
Use la siguiente expresión jq para calcular el promedio de los puntos de datos y quitar los mensajes con una temperatura media inferior a 30 o una humedad media mayor que 90:
.payload |= map_values(add / length)
| if .payload.temperature > 30 and .payload.humidity < 90 then . else empty end
En la expresión jq anterior:
.payload |= <expression>
use|=
para actualizar el valor de.payload
con el resultado de ejecutar<expression>
. El uso de|=
en lugar de=
establece el contexto de datos de<expression>
a.payload
en lugar de.
.map_values(add / length)
ejecutaadd / length
para cada valor del subobjeto.payload
. La expresión suma los elementos de la matriz de valores y, a continuación, se divide por la longitud de la matriz para calcular el promedio.if .payload.temperature > 30 and .payload.humidity < 90 then . else empty end
comprueba dos condiciones en el mensaje resultante. Si el filtro se evalúa como true, como en la primera entrada, el mensaje completo se genera como salida. Si el filtro se evalúa como false, como en la segunda entrada, devuelveempty
, lo que da como resultado una secuencia vacía con cero valores. Este resultado hace que la expresión quite el mensaje correspondiente.
Salida 1
{
"payload": {
"temperature": 47.25,
"humidity": 52.75
}
}
Salida 2
(sin salida)
Eliminación implícita mediante errores
Las expresiones de filtro y transformación pueden quitar mensajes implícitamente provocando que jq genere un error. Aunque este enfoque no es un procedimiento recomendado porque la canalización no puede diferenciar entre un error causado intencionadamente y uno causado por una entrada inesperada en la expresión. El sistema controla actualmente un error en tiempo de ejecución en el filtro o transformación quitando el mensaje y registrando el error.
Un escenario común que usa este enfoque es cuando una entrada a una canalización puede tener mensajes que están estructuralmente separados. En el ejemplo siguiente se muestra cómo recibir dos tipos de mensajes, uno de los cuales se evalúa correctamente con el filtro y el otro que es estructuralmente incompatible con la expresión. Dadas las siguientes entradas:
Mensaje 1
{
"payload": {
"sensorData": {
"temperature": 15,
"humidity": 62
}
}
}
Mensaje 2
{
"payload": [
{
"rpm": 12,
"timestamp": 1689816609514
},
{
"rpm": 52,
"timestamp": 1689816628580
}
]
}
Use la siguiente expresión jq para filtrar los mensajes con una temperatura inferior a 10 y una humedad mayor que 80:
.payload.sensorData.temperature > 10 and .payload.sensorData.humidity < 80
En el ejemplo anterior, la propia expresión es una expresión booleana compuesta simple. La expresión está diseñada para trabajar con la estructura del primero de los mensajes de entrada mostrados anteriormente. Cuando la expresión recibe el segundo mensaje, la estructura de matriz de .payload
no es compatible con el acceso al objeto en la expresión y produce un error. Si desea filtrar en función de los valores de temperatura/humedad y quitar mensajes con una estructura incompatible, esta expresión funciona. Otro enfoque que no produce ningún error es agregar (.payload | type) == "object" and
al inicio de la expresión.
Salida 1
true
Salida 2
(error)
Utilidades de tiempo
jq no admite el tiempo como un tipo nativo. Sin embargo, algunos formatos aceptados y emitidos por el procesador de datos admiten el tiempo como un tipo nativo. Normalmente, estos tipos se representan mediante el tipo time.Time
de Go.
Para permitirle interactuar con estos valores desde jq, el procesador de datos proporciona un módulo con un conjunto de funciones que le permiten:
- Convertir entre la hora nativa, las cadenas ISO 8601 y las marcas de tiempo numéricas de Unix.
- Realizar varias operaciones específicas de tiempo en todos estos tipos.
El módulo time
Todas las funciones de tiempo especiales se especifican un módulo time
que se puede importar en una consulta.
Importe el módulo al principio de la consulta de una de estas dos maneras:
import" "time" as time;
include "time"
El primer método coloca todas las funciones del módulo en un espacio de nombres, por ejemplo, time::totime
. El segundo método simplemente coloca todas las funciones de tiempo en el nivel superior, por ejemplo, totime
. Ambas sintaxis son válidas y funcionalmente equivalentes.
Formatos y conversión
El módulo de tiempo funciona con tres formatos de hora:
time
es un valor de hora nativo. Solo puede usarlo con las funciones del módulo de tiempo. Se reconoce como un tipo de datostime
al serializar.unix
es una marca de tiempo numérica de Unix que representa el tiempo como segundos desde la época de Unix. Puede ser un número entero o de punto flotante. Se reconoce como el tipo numérico correspondiente al serializar.iso
es una representación de formato de cadena ISO 8601 de tiempo. Se reconoce como una cadena al serializar.
El módulo de tiempo proporciona las siguientes funciones para comprobar y manipular estos tipos:
time::totime
convierte cualquiera de los tres tipos entime
.time::tounix
convierte cualquiera de los tres tipos enunix
.time::toiso
convierte cualquiera de los tres tipos eniso
.time::istime
devuelve true si los datos están en formato detime
.
Operaciones de fecha y hora
El módulo de tiempo proporciona varias operaciones específicas del tiempo que funcionan en todos los tipos. Las siguientes funciones pueden tomar cualquiera de los tipos admitidos como entrada y devolver el mismo tipo que su salida. Las marcas de tiempo de número entero se pueden convertir en marcas de tiempo de punto flotante si necesita más precisión.
time::utc
convierte la fecha y hora en UTC.time::zone(zone)
convierte la fecha y hora a la zona proporcionada.zone
es una cadena de zona ISO 8601. Por ejemplo,time::zone("-07")
.time::local
convierte la fecha y hora a la hora local.time::offset(duration)
avanza o retrasa la fecha y hora según la duración proporcionada.duration
usa la sintaxis de cadena de duración de Go. Por ejemplo,time::offset("1m2s")
.time::offset(value;unit)
avanza o retrasa la fecha y hora según la duración proporcionada. Esta función usa un número y una cadena de unidad. Por ejemplo,time::offset(2;"s")
. Esta función es útil cuando la duración procede de otra propiedad.
Nota:
Ninguna de las tres funciones de zona horaria tiene un efecto significativo en las marcas de tiempo de Unix.
Utilidades varias
El módulo util
es una colección de utilidades que expande las funcionalidades del entorno de ejecución de jq.
El módulo util
Todas las utilidades varias se especifican en un módulo util
que se puede importar en una consulta.
Importe el módulo al principio de la consulta de una de estas dos maneras:
import" "util" as util;
include "util"
El primer método coloca todas las funciones del módulo en un espacio de nombres, por ejemplo, util::uuid
. El segundo método simplemente coloca todas las funciones varias en el nivel superior, por ejemplo, uuid
. Ambas sintaxis son válidas y funcionalmente equivalentes.
El módulo util
incluye actualmente la función uuid
que devuelve un UUID nuevo y aleatorio en el formato de cadena estándar.
Manipulación binaria
jq se ha diseñado para trabajar con datos que se pueden representar como JSON. Sin embargo, las canalizaciones de procesador de datos también admiten un formato de datos sin procesar que contiene datos binarios no preparados. Para trabajar con datos binarios, la versión de jq que se incluye con el procesador de datos contiene un paquete diseñado para ayudarle a procesar datos binarios. Le permite:
- Convierta entre formatos binarios y otros, como base64 y matrices de enteros.
- Use funciones integradas para leer valores numéricos y de cadena de un mensaje binario.
- Realice modificaciones puntuales de datos binarios mientras conserva su formato.
Importante
No puede usar ninguna función o operadores jq integrados que modifiquen un valor binario. Esto significa que no hay concatenación con +
, no map
que funcione con los bytes y ninguna asignación mixta con valores binarios como |=
, +=
, //=
. Puede usar la asignación estándar (==
). Si intenta usar datos binarios con una operación no admitida, el sistema produce un error de jqImproperBinaryUsage
. Si necesita manipular los datos binarios de maneras personalizadas, considere la posibilidad de usar una de las siguientes funciones para convertirlos en base64 o en una matriz de enteros para el cálculo y, a continuación, convertirlo de nuevo en binario.
En las secciones siguientes se describe la compatibilidad binaria en el motor jq del procesador de datos.
El módulo binary
Toda la compatibilidad binaria en el motor jq del procesador de datos se especifica en un binary
módulo que se puede importar.
Importe el módulo al principio de la consulta de una de estas dos maneras:
import "binary" as binary;
include "binary"
El primer método coloca todas las funciones del módulo en un espacio de nombres, por ejemplo, binary::tobase64
. El segundo método simplemente coloca todas las funciones binarias en el nivel superior, por ejemplo, tobase64
. Ambas sintaxis son válidas y funcionalmente equivalentes.
Formatos y conversión
El módulo binario funciona con tres tipos:
- binario: un valor binario, solo se puede usar directamente con las funciones del módulo binario. Reconocido por una canalización como un tipo de datos binario al serializar. Use este tipo para la serialización sin procesar.
- matriz: un formato que convierte el binario en una matriz de números para permitirle realizar su propio procesamiento. Reconocido por una canalización como una matriz de enteros al serializar.
- base64: una representación de formato de cadena de binario. Principalmente útil si desea convertir entre cadenas y binarias. Reconocido por una canalización como una cadena al serializar.
Puede convertir entre los tres tipos de las consultas jq en función de sus necesidades. Por ejemplo, puede convertir de binario a una matriz, realizar alguna manipulación personalizada y, a continuación, volver a convertirla en binaria al final para conservar la información de tipo.
Functions
Se proporcionan las siguientes funciones para comprobar y manipular entre estos tipos:
binary::tobinary
convierte cualquiera de los tres tipos en binario.binary::toarray
convierte cualquiera de los tres tipos en matriz.binary::tobase64
convierte cualquiera de los tres tipos en base64.binary::isbinary
devuelve true si los datos están en formato binario.binary::isarray
devuelve true si los datos están en formato de matriz.binary::isbase64
devuelve true si los datos están en formato base64.
El módulo también proporciona la función binary::edit(f)
para modificaciones rápidas de datos binarios. La función convierte la entrada en el formato de matriz, aplica la función en ella y, a continuación, convierte el resultado en binario.
Extracción de datos de binarios
El módulo binario permite extraer valores de los datos binarios para usarlos en el desempaquetado de cargas binarias personalizadas. En general, esta funcionalidad sigue a la de otras bibliotecas de desempaquetado binario y sigue una nomenclatura similar. Los siguientes tipos se pueden desempaquetar:
- Enteros (int8, int16, int32, int64, uint8, uint16, uint32, uint64)
- Floats (float, double)
- Cadenas (utf8)
El módulo también permite especificar desplazamientos y endianidad, si procede.
Funciones para leer datos binarios
El módulo binario proporciona las siguientes funciones para extraer datos de valores binarios. Puede usar todas las funciones con cualquiera de los tres tipos entre los que puede convertir el paquete.
Todos los parámetros de función son opcionales, offset
el valor predeterminado es 0
y length
el resto de los datos.
binary::read_int8(offset)
lee un valor int8 de un valor binario.binary::read_int16_be(offset)
lee un valor int16 de un valor binario en orden big-endian.binary::read_int16_le(offset)
lee un valor int16 de un valor binario en orden little-endian.binary::read_int32_be(offset)
lee un valor int32 de un valor binario en orden big-endian.binary::read_int32_le(offset)
lee un valor int32 de un valor binario en orden little-endian.binary::read_int64_be(offset)
lee un valor int64 de un valor binario en orden big-endian.binary::read_int64_le(offset)
lee un valor int64 de un valor binario en orden little-endian.binary::read_uint8(offset)
lee un uint8 de un valor binario.binary::read_uint16_be(offset)
lee un uint16 de un valor binario en orden big-endian.binary::read_uint16_le(offset)
lee un uint16 de un valor binario en orden little-endian.binary::read_uint32_be(offset)
lee un uint32 de un valor binario en orden big-endian.binary::read_uint32_le(offset)
lee un uint32 de un valor binario en orden little-endian.binary::read_uint64_be(offset)
lee un uint64 de un valor binario en orden big-endian.binary::read_uint64_le(offset)
lee un uint64 de un valor binario en orden little-endian.binary::read_float_be(offset)
lee un float de un valor binario en orden big-endian.binary::read_float_le(offset)
lee un valor float de un valor binario en orden little-endian.binary::read_double_be(offset)
lee un doble de un valor binario en orden big-endian.binary::read_double_le(offset)
lee un doble de un valor binario en orden little-endian.binary::read_bool(offset; bit)
lee una bool de un valor binario, comprobando el bit especificado para el valor.binary::read_bit(offset; bit)
lee un bit de un valor binario mediante el índice de bits especificado.binary::read_utf8(offset; length)
lee una cadena UTF-8 de un valor binario.
Escritura de datos binarios
El módulo binario permite codificar y escribir valores binarios. Esta funcionalidad le permite construir o realizar modificaciones en cargas binarias directamente en jq. La escritura de datos admite el mismo conjunto de tipos de datos que la extracción de datos y también permite especificar la endianidad que se va a usar.
La escritura de datos tiene dos formas:
write_*
funcionesactualizar datos en contexto en un valor binario, que se usan para actualizar o manipular valores existentes.append_*
funcionesagregar datos al final de un valor binario, que se usa para agregar o construir nuevos valores binarios.
Funciones para escribir datos binarios
El módulo binario proporciona las siguientes funciones para escribir datos en valores binarios. Todas las funciones se pueden ejecutar en cualquiera de los tres tipos válidos entre los que puede convertirse este paquete.
El parámetro value
es necesario para todas las funciones, pero offset
es opcional, donde el valor predeterminado y válido es 0
.
Funciones de escritura:
binary::write_int8(value; offset)
escribe un valor int8 en un valor binario.binary::write_int16_be(value; offset)
escribe un valor int16 en un valor binario en orden big-endian.binary::write_int16_le(value; offset)
escribe un valor int16 en un valor binario en orden little-endian.binary::write_int32_be(value; offset)
escribe un valor int32 en un valor binario en orden big-endian.binary::write_int32_le(value; offset)
escribe un valor int32 en un valor binario en orden little-endian.binary::write_int64_be(value; offset)
escribe un valor int64 en un valor binario en orden big-endian.binary::write_int64_le(value; offset)
escribe un valor int64 en un valor binario en orden little-endian.binary::write_uint8(value; offset)
escribe un uint8 en un valor binario.binary::write_uint16_be(value; offset)
escribe un uint16 en un valor binario en orden big-endian.binary::write_uint16_le(value; offset)
escribe un uint16 en un valor binario en orden little-endian.binary::write_uint32_be(value; offset)
escribe un uint32 en un valor binario en orden big-endian.binary::write_uint32_le(value; offset)
escribe un uint32 en un valor binario en orden little-endian.binary::write_uint64_be(value; offset)
escribe un uint64 en un valor binario en orden big-endian.binary::write_uint64_le(value; offset)
escribe un valor uint64 en un valor binario en orden little-endian.binary::write_float_be(value; offset)
escribe un valor float en un valor binario en orden big-endian.binary::write_float_le(value; offset)
escribe un valor float en un valor binario en orden little-endian.binary::write_double_be(value; offset)
escribe un valor double en un valor binario en orden big-endian.binary::write_double_le(value; offset)
escribe un doble en un valor binario en orden little-endian.binary::write_bool(value; offset; bit)
escribe una bool en un solo byte en un valor binario, estableciendo el bit especificado en el valor bool.binary::write_bit(value; offset; bit)
escribe un solo bit en un valor binario, dejando otros bits en el byte tal cual.binary::write_utf8(value; offset)
escribe una cadena UTF-8 en un valor binario.
Anexar funciones:
binary::append_int8(value)
anexa un valor int8 a un valor binario.binary::append_int16_be(value)
anexa un valor int16 a un valor binario en orden big-endian.binary::append_int16_le(value)
anexa un valor int16 a un valor binario en orden little-endian.binary::append_int32_be(value)
anexa un valor int32 a un valor binario en orden big-endian.binary::append_int32_le(value)
anexa un valor int32 a un valor binario en orden little-endian.binary::append_int64_be(value)
anexa un valor int64 a un valor binario en orden big-endian.binary::append_int64_le(value)
anexa un valor int64 a un valor binario en orden little-endian.binary::append_uint8(value)
anexa un uint8 a un valor binario.binary::append_uint16_be(value)
anexa un uint16 a un valor binario en orden big-endian.binary::append_uint16_le(value)
anexa un uint16 a un valor binario en orden little-endian.binary::append_uint32_be(value)
anexa un uint32 a un valor binario en orden big-endian.binary::append_uint32_le(value)
anexa un uint32 a un valor binario en orden little-endian.binary::append_uint64_be(value)
anexa un uint64 a un valor binario en orden big-endian.binary::append_uint64_le(value)
anexa un uint64 a un valor binario en orden little-endian.binary::append_float_be(value)
anexa un valor float a un valor binario en orden big-endian.binary::append_float_le(value)
anexa un valor float a un valor binario en orden little-endian.binary::append_double_be(value)
anexa un valor double a un valor binario en orden big-endian.binary::append_double_le(value)
anexa un valor double a un valor binario en orden little-endian.binary::append_bool(value; bit)
anexa una bool a un solo byte en un valor binario, estableciendo el bit especificado en el valor bool.binary::append_utf8(value)
anexa una cadena UTF-8 a un valor binario.
Ejemplos binarios
En esta sección se muestran algunos casos de uso comunes para trabajar con datos binarios. En los ejemplos se usa un mensaje de entrada común.
Supongamos que tiene un mensaje con una carga útil que es un formato binario personalizado que contiene varias secciones. Cada sección contiene los siguientes datos en orden de bytes big-endian:
- Uint32 que contiene la longitud del nombre del campo en bytes.
- Cadena utf-8 que contiene el nombre del campo cuya longitud especifica la uint32 anterior.
- Doble que contiene el valor del campo.
En este ejemplo, tiene tres de estas secciones, que contienen:
(uint32) 11
(utf-8) temperatura
(double) 86.0
(uint32) 8
(utf-8) humedad
(double) 51.290
(uint32) 8
(utf-8) presión
(double) 346.23
Estos datos tienen este aspecto cuando se imprimen en la sección payload
de un mensaje:
{
"payload": "base64::AAAAC3RlbXBlcmF0dXJlQFWAAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"
}
Nota:
La representación base64::<string>
de datos binarios es simplemente para facilitar la diferenciación de otros tipos y no es representativa del formato de datos físicos durante el procesamiento.
Extracción de valores directamente
Si conoce la estructura exacta del mensaje, puede recuperar los valores de él mediante los desplazamientos adecuados.
Use la siguiente expresión jq para extraer los valores:
import "binary" as binary;
.payload | {
temperature: binary::read_double_be(15),
humidity: binary::read_double_be(35),
pressure: binary::read_double_be(55)
}
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"humidity": 51.29,
"pressure": 346.23,
"temperature": 86
}
Extracción de valores dinámicamente
Si el mensaje puede contener campos en cualquier orden, puede extraer dinámicamente el mensaje completo:
Use la siguiente expresión jq para extraer los valores:
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
El siguiente JSON muestra la salida de la expresión jq anterior:
{
"humidity": 51.29,
"pressure": 346.23,
"temperature": 86
}
Editar valores directamente
En este ejemplo se muestra cómo editar uno de los valores. Como en el caso de extracción, es más fácil si sabe dónde se encuentra el valor que desea editar en los datos binarios. En este ejemplo se muestra cómo convertir la temperatura de fahrenheit a celsius.
Use la siguiente expresión jq convertir la temperatura de fahrenheit a celsius en el mensaje binario:
import "binary" as binary;
15 as $index
| .payload
| binary::write_double_be(
((5 / 9) * (binary::read_double_be($index) - 32));
$index
)
El siguiente JSON muestra la salida de la expresión jq anterior:
"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"
Si aplica la lógica de extracción mostrada anteriormente, obtendrá la siguiente salida:
{
"humidity": 51.29,
"pressure": 346.23,
"temperature": 30
}
Editar valores dinámicamente
En este ejemplo se muestra cómo lograr el mismo resultado que el ejemplo anterior mediante la localización dinámica del valor deseado en la consulta.
Use la siguiente expresión jq convertir la temperatura de fahrenheit a celsius en el mensaje binario, localizando dinámicamente los datos para editar:
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
)
El siguiente JSON muestra la salida de la expresión jq anterior:
"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"
Insertar nuevos valores
Agregue nuevos valores mediante las funciones append del paquete. Por ejemplo, para agregar un campo de windSpeed
con un valor de 31.678
a la entrada mientras se conserva el formato binario entrante, use la siguiente expresión jq:
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)
El siguiente JSON muestra la salida de la expresión jq anterior:
"base64:AAAAC3RlbXBlcmF0dXJlQFWAAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFIAAAACXdpbmRTcGVlZEA/rZFocrAh"