Compartir por


¿Qué son las expresiones de jq en la versión preliminar del Procesador de datos de Azure IoT?

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.

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 y value como valor de la clave.
  • if .key == "temp" then .key = "temperature" else . end realiza la lógica condicional en la clave de la entrada. Si la clave es temp, se convierte a temperature y se deja el valor sin cambios. Si la clave no es temp, 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 denominado payload 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 es false o null. 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, humidityy 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 para timestamp: .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 de data 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 campo value del resultado.
  • select(.field == "dtmi:com:prod1:slicer3345:temperature") ejecuta la expresión booleana dentro de select() 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 estructura from_entries necesita.
  • {key: .field, value: {timestamp, value}} crea un objeto a partir de una entrada de matriz, asignando field a la clave y creando un valor que contiene timestamp y value. {timestamp, value} es abreviada para {timestamp: .timestamp, value: .value}.
  • <expression> | from_entriesconvierte un <expression> con valores de matriz en un objeto, asignando el campo key de cada entrada de matriz a la clave de objeto y el campo value 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, humidityy 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 por group_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 por group_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 campos name, value y timestamp. Cada <expression> genera el valor deseado para la clave asociada.
  • .[0].name recupera el primer elemento de la submatriz y extrae el campo name 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 de value de cada submatriz:
    • map(.value) convierte la submatriz en una matriz del campo value 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áximo timestamp de cada submatriz:
    • map(.timestamp) convierte la submatriz en una matriz de los campos timestamp 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 clave topic 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 caso slicer-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 rama else, 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 de to_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 y SequenceNumber no se modifican, por lo que puede usar la sintaxis abreviada en lugar de escribir DataSetWriterName: .DataSetWriterName y SequenceNumber: .SequenceNumber.
  • Tag: .Payload.key, extrae la clave de objeto original del Payload interno y los niveles superiores al objeto primario. La operación to_entries anterior en la consulta creó el campo key.
  • Value: .Payload.value.value y realizan Timestamp: .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 de temperatureC en la carga que se establece en la conversión de temperatureF 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 de energy 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 entrada mass de la lista de userProperties. Los datos ya están configurados como objetos con key y value, por lo que from_entries puede convertirlo directamente en un objeto. La expresión recupera la clave mass 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 de true a false.

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ón has con un objeto de entrada. La función devuelve true solo si la clave está presente. hasSite es true a pesar del valor de site ser null.
  • temperatureNotNull: (.temperature != null), esta y otras expresiones similares muestran cómo la comprobación de != null realiza una comprobación similar a has. Una clave inexistente de un objeto se null si se tiene acceso mediante la sintaxis de .<key> o existe una clave, pero tiene un valor de null. Tanto siteNotNull y missingNotNull 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 con has, 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 de null 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 de userProperties para que el resto de <expression> menos detallados.
  • any(.key == "mass") ejecuta la expresión .key == "mass" en cada elemento de la matriz de userProperties, 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 de userProperties, 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 de status en la carga.
  • if ... end es la expresión principal if/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 con end.

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 a with_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 de to_entries. La función convierte una matriz de objetos con estructura {"key": <key>, "value": <value>} en un objeto con los campos key y value asignados a pares clave-valor.
  • .key |= ltrimstr("rotor_") actualiza el valor de .key en cada entrada con el resultado de ltrimstr("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 de reduce no usa la secuencia para generar varias salidas. reduce no separa el mensaje.
    • <init> en este caso null 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 campos totalChange, previous y count 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 caso else. Esta expresión establece cada campo del objeto acumulador en un nuevo valor basado en un cálculo. Para totalChange, encuentra la diferencia entre los valores actuales y anteriores y obtiene el valor absoluto. Contra intuitivamente usa la función length para obtener el valor absoluto. previous se establece en el $itemactual delvalue para que se use la siguiente iteración y se incrementa count.
  • .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 bucle while es una matriz que contiene los valores generados por cada iteración del bucle.
  • until como 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. A diferencia de while, el bucle until 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 denominada avg 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 usa avg. 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 denominada stdev. 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 primeros defson 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 de value. 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 se or-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ón if 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 campo value 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 a length, 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ón then 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ón else 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) ejecuta add / 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, devuelve empty, 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 sí 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 datos time 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 en time.
  • time::tounix convierte cualquiera de los tres tipos en unix.
  • time::toiso convierte cualquiera de los tres tipos en iso.
  • time::istime devuelve true si los datos están en formato de time.

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 la versión preliminar del procesador de datos de Azure IoT 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 módulo binary 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"