Compartilhar via


O que são expressões jq na versão prévia do Processador de Dados da Internet das Coisas do Azure?

Importante

O recurso Pré-visualização de Operações do Azure IoT — habilitado pelo Azure Arc — está atualmente em VERSÃO PRÉVIA. Você não deve usar esse software em versão prévia em ambientes de produção.

Veja os Termos de Uso Complementares para Versões Prévias do Microsoft Azure para obter termos legais que se aplicam aos recursos do Azure que estão em versão beta, versão prévia ou que, de outra forma, ainda não foram lançados em disponibilidade geral.

As expressões jq oferecem uma maneira poderosa de executar cálculos e manipulações em mensagens de pipeline de dados. Este guia apresenta padrões e abordagens de linguagem para necessidades comuns de computação e processamento nos seus pipelines de dados.

Dica

Para experimentar os exemplos deste guia, você pode usar o jq playground e colar as entradas e expressões de exemplo no editor.

Conceitos básicos da linguagem

Se você não estiver familiarizado com o jq como linguagem, esta seção de conceitos básicos da linguagem fornecerá algumas informações gerais.

Programação funcional

A linguagem jq é uma linguagem de programação funcional. Cada operação usa uma entrada e produz uma saída. Várias operações são combinadas para executar uma lógica complexa. Por exemplo, considerando a seguinte entrada:

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

Aqui está uma expressão jq simples que especifica um caminho para recuperar:

.payload.temperature

Esse caminho é uma operação que usa um valor como entrada e gera outro valor como saída. Neste exemplo, o valor de saída é 25.

Existem algumas considerações importantes ao trabalhar com operações complexas e encadeadas no jq:

  • Todos os dados não retornados por uma operação não estão mais disponíveis no restante da expressão. Existem algumas maneiras de contornar essa restrição, mas em geral, você deve pensar sobre quais dados você precisa mais tarde na expressão e evitar que ela desabilite as operações anteriores.
  • As expressões são melhor compreendidas como uma série de transformações de dados em vez de um conjunto de computações a serem executadas. Mesmo operações como atribuições são apenas uma transformação do valor geral, onde um campo foi alterado.

Tudo é uma expressão

Na maioria das linguagens não funcionais, há uma distinção entre dois tipos de operação:

  • Expressões que produzem um valor que pode ser usado no contexto de outra expressão.
  • Instruções que criam algum tipo de efeito colateral em vez de manipular diretamente uma entrada e uma saída.

Com algumas exceções, tudo no jq é uma expressão. Loops, operações if/else e até mesmo atribuições são todas expressões que produzem um novo valor, em vez de criar um efeito colateral no sistema. Por exemplo, considerando a seguinte entrada:

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

Se eu quisesse alterar o campo humidity para 63, você pode usar uma expressão de atribuição:

.humidity = 63

Embora essa expressão pareça alterar o objeto de entrada, no jq ela está produzindo um novo objeto com um novo valor para humidity:

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

Essa diferença parece sutil, mas significa que você pode encadear o resultado dessa operação com outras operações usando |, conforme descrito mais adiante.

Encadeie operações com uma barra vertical (pipe): |

Realizar cálculos e manipulação de dados no jq frequentemente requer que você combine várias operações. Você encadeia as operações colocando uma | entre elas. Por exemplo, para calcular o comprimento de uma matriz de dados em uma mensagem:

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

Primeiro, isole a parte da mensagem que contém a matriz:

.data

Essa expressão fornece apenas a matriz:

[5, 2, 4, 1]

Em seguida, use a operação length para calcular o comprimento dessa matriz:

length

Essa expressão lhe dá a resposta:

4

Use o operador | como separador entre as etapas, de modo que, como uma única expressão jq, o cálculo torna-se:

.data | length

Se você estiver tentando executar uma transformação complexa e não encontrar um exemplo aqui que corresponda exatamente ao problema, é provável que você possa resolvê-lo encadeando várias soluções neste guia com o símbolo |.

Entradas e argumentos de função

Uma das principais operações no jq é chamar uma função. As funções no jq vêm de várias formas e podem receber diferentes números de entradas. As entradas de função vêm de duas formas:

  • Contexto de dados: os dados que são automaticamente alimentados na função pelo jq. Normalmente, os dados produzidos pela operação antes do símbolo | mais recente.
  • Argumentos de função: outras expressões e valores que você fornece para configurar o comportamento de uma função.

Muitas funções não têm argumentos e fazem todo o trabalho usando o contexto de dados fornecido pelo jq. A função length é um exemplo:

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

No exemplo anterior, a entrada para length é a matriz criada à esquerda do símbolo |. A função não precisa de nenhuma outra entrada para calcular o comprimento da matriz de entrada. Você chama funções sem argumentos usando apenas o nome delas. Em outras palavras, usa length, não length().

Algumas funções combinam o contexto de dados com um único argumento para definir seu comportamento. Por exemplo, a função map:

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

No exemplo anterior, a entrada para map é a matriz de números criada à esquerda do símbolo |. A função map executa uma expressão em cada elemento da matriz de entrada. Você fornece a expressão como um argumento para map, neste caso, . * 2, para multiplicar o valor de cada entrada na matriz por 2 e gerar a matriz [2, 4, 6] como saída. Você pode configurar qualquer comportamento interno desejado com a função map.

Algumas funções recebem mais de um argumento. Essas funções funcionam da mesma maneira que as funções de argumento único e usam o símbolo ; para separar os argumentos. Por exemplo, a função sub:

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

No exemplo anterior, a função sub recebe "Olá, Mundo" como seu contexto de dados de entrada e, em seguida, usa dois argumentos:

  • Uma expressão regular a ser pesquisada na cadeia de caracteres.
  • Uma cadeia de caracteres para substituir qualquer substring correspondente. Separe os argumentos com o símbolo ;. O mesmo padrão se aplica a funções com mais de dois argumentos.

Importante

Certifique-se de usar ; como separador de argumento e não ,.

Trabalhar com objetos

Existem muitas maneiras de extrair dados, manipular e construir objetos no jq. As seções a seguir descrevem alguns dos padrões mais comuns:

Extrair valores de um objeto

Para recuperar chaves, você normalmente usa uma expressão de caminho. Essa operação é frequentemente combinada com outras operações para obter resultados mais complexos.

É fácil recuperar dados de objetos. Quando você precisa recuperar muitos dados de estruturas que não são objetos, um padrão comum é converter estruturas que não são objetos em objetos. Dada a seguinte entrada:

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

Use a seguinte expressão para recuperar o valor da umidade:

.payload.values.humidity

Essa expressão gera a seguinte saída:

67

Alterar chaves em um objeto

Para renomear ou modificar chaves de objeto, você pode usar a função with_entries. Esta função recebe uma expressão que opera nos pares chave/valor de um objeto e retorna um novo objeto com os resultados da expressão.

O exemplo a seguir mostra como renomear o campo temp para temperature a fim de alinhá-lo com um esquema downstream. Dada a seguinte entrada:

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

Use a expressão a seguir para renomear o campo temp para temperature:

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

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • with_entries(<expression>) é uma abreviação para executar várias operações em conjunto. E faz as seguintes operações:
    • Usa um objeto como entrada e converte cada par chave/valor em uma entrada com estrutura {"key": <key>, "value": <value>}.
    • Executa <expression> para cada entrada gerada a partir do objeto, substituindo o valor de entrada pelo resultado da execução <expression>.
    • Converte o conjunto transformado de entradas de volta em um objeto, usando key como a chave no par chave/valor e value como o valor da chave.
  • if .key == "temp" then .key = "temperature" else . end executa a lógica condicional em relação à chave da entrada. Se a chave for temp, então ela será convertida para temperature, deixando o valor inalterado. Se a chave não for temp, a entrada ficará inalterada retornando . da expressão.

O JSON a seguir mostra a saída da expressão anterior:

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

Converter um objeto em uma matriz

Embora os objetos sejam úteis para acessar dados, as matrizes geralmente são mais úteis quando você deseja dividir mensagens ou combinar dinamicamente informações. Usar to_entries para converter um objeto em uma matriz.

O exemplo a seguir mostra como converter o campo payload em uma matriz. Dada a seguinte entrada:

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

Use a expressão a seguir para converter o campo de conteúdo em uma matriz:

.payload | to_entries

O JSON a segui é a saída da expressão jq anterior:

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

Dica

Esse exemplo simplesmente extrai a matriz e descarta qualquer outra informação na mensagem. Para preservar a mensagem geral, mas trocar a estrutura de .payload por uma matriz, use .payload |= to_entries em vez disso.

Criar objetos

Você constrói objetos usando sintaxe semelhante ao JSON, em que você pode fornecer uma mistura de informações estáticas e dinâmicas.

O exemplo a seguir mostra como reestruturar completamente um objeto, criando um novo objeto com campos renomeados e uma estrutura atualizada. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para criar um objeto com a nova estrutura:

{
  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"
}

Na expressão jq anterior:

  • {payload: {<fields>}} cria um objeto com um campo literal chamado payload que é em si um objeto literal contendo mais campos. Essa abordagem é a maneira mais básica de construir objetos.
  • humidity: .payload.Payload["dtmi:com:prod1:slicer3345:humidity"].Value, cria um nome de chave estático com um valor calculado dinamicamente. O contexto de dados para todas as expressões dentro da construção do objeto é a entrada completa para a expressão de construção do objeto, nesse caso, a mensagem completa.
  • (.payload.DataSetWriterName): "active" é um exemplo de uma chave de objeto dinâmica. Neste exemplo, o valor de .payload.DataSetWriterName é mapeado para um valor estático. Use chaves e valores estáticos e dinâmicos em qualquer combinação ao criar um objeto.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Adicionar campos a um objeto

Você pode aumentar um objeto adicionando campos para fornecer contexto adicional para os dados. Use uma atribuição para um campo que não existe.

O exemplo a seguir mostra como adicionar um campo averageVelocity ao conteúdo. Dada a seguinte entrada:

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

Use a seguinte expressão jq para adicionar um campo averageVelocity ao conteúdo:

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

Ao contrário de outros exemplos que usam o símbolo |=, este exemplo usa uma atribuição padrão, =. Portanto, não limite da expressão do lado direito ao campo do lado esquerdo. Essa abordagem é necessária para que você possa acessar outros campos do conteúdo.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Adicionar campos condicionalmente a um objeto

Combinar lógica condicional com a sintaxe para adicionar campos a um objeto permite cenários como adicionar valores padrão para campos que não estão presentes.

O exemplo a seguir mostra como adicionar uma unidade a qualquer medição de temperatura que não tenha uma. A unidade padrão é celsius. Dada a seguinte entrada:

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

Use a seguinte expressão jq para adicionar uma unidade a qualquer medição de temperatura que não tenha uma:

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

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • map(<expression>) executa <expression> em cada entrada na matriz e substitui o valor de entrada por qualquer coisa que <expression> produzir.
  • .unit //= "celsius" usa a atribuição especial //=. Essa atribuição combina (=) com o operador alternativo (//) para atribuir o valor existente de .unit a si mesmo se não for false ou null. Se .unit for falso ou nulo, a expressão atribuirá "celsius" como o valor de .unit, criando .unit, se necessário.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Remover campos de um objeto

Use a função del para remover campos desnecessários de um objeto.

O exemplo a seguir mostra como remover o campo timestamp porque não é relevante para o restante da computação. Dada a seguinte entrada:

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

Usar a seguinte expressão jq remove o campo timestamp:

del(.payload.timestamp)

O JSON a seguir mostra a saída da expressão jq anterior:

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

Trabalhar com matrizes

Matrizes são o bloco de compilação fundamental para iteração e divisão de mensagens no jq. Os exemplos a seguir mostram como manipular matrizes.

Extrair valores de uma matriz

Matrizes são mais difíceis de inspecionar do que objetos porque os dados podem estar localizados em índices diferentes da matriz em mensagens diferentes. Portanto, para extrair valores de uma matriz, muitas vezes é necessário procurar os dados que você precisa na matriz.

O exemplo a seguir mostra como extrair alguns valores de uma matriz para criar um novo objeto que contém os dados de seu interesse. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para extrair os valores timestamp, temperature, humidity e pressure da matriz para criar um novo 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,
}

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • {timestamp, <other-fields>} é uma abreviação para timestamp: .timestamp, que adiciona o carimbo de data/hora como um campo ao objeto usando o campo de mesmo nome do objeto original. <other-fields> adiciona mais campos ao objeto.
  • temperature: <expression>, humidity: <expression>, pressure: <expression> define a temperatura, umidade e pressão no objeto resultante com base nos resultados das três expressões.
  • .data | <expression> limita a computação de valor para a matriz data do conteúdo e executa <expression> na matriz.
  • map(<expression>)[0]?.value faz várias coisas:
    • map(<expression>) executa <expression> em cada elemento na matriz, retornando o resultado da execução dessa expressão em cada elemento.
    • [0] extrai o primeiro elemento da matriz resultante.
    • ? permite o encadeamento adicional de um segmento de caminho, mesmo que o valor anterior seja nulo. Quando o valor anterior é nulo, o caminho subsequente também retorna nulo em vez de falhar.
    • .value extrai o campo value do resultado.
  • select(.field == "dtmi:com:prod1:slicer3345:temperature") executa a expressão booliana dentro de select() em relação à entrada. Se o resultado for verdadeiro, a entrada será aprovada. Se o resultado for falso, a entrada será descartada. map(select(<expression>)) é uma combinação comum usada para filtrar elementos em uma matriz.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Alterar entradas de uma matriz

Modifique entradas em uma matriz com uma expressão map(). Use essas expressões para modificar cada elemento da matriz.

O exemplo a seguir mostra como converter o carimbo de data/hora de cada entrada na matriz de tempo Unix em milissegunds para uma cadeia de caracteres RFC3339. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para converter o carimbo de data e hora de cada entrada da matriz de um tempo Unix em milissegundos para uma cadeia de caracteres RFC3339:

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

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • map(<expression>) executa <expression> em cada elemento na matriz, substituindo cada um pela saída da execução de <expression>.
  • .timestamp |= <expression> define o carimbo de data/hora como um novo valor com base na execução de <expression>, em que o contexto de dados de <expression> é o valor de .timestamp.
  • (. / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ")) converte o tempo de milissegundos para segundos e usa um formatador de cadeia de caracteres de tempo para produzir um carimbo de data/hora ISO 8601.

O JSON a seguir mostra a saída da expressão 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
    }
  ]
}

Converter uma matriz em um objeto

Use from_entries para reestruturar uma matriz em um objeto, tornando mais fácil acessar ou estar em conformidade com um esquema desejado. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para converter a matriz em um objeto:

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

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • map({key: <expression>, value: <expression>}) converte cada elemento da matriz em um objeto da formato {"key": <data>, "value": <data>}, que é a estrutura necessária para from_entries.
  • {key: .field, value: {timestamp, value}} cria um objeto a partir da entrada de uma matriz, mapeando field para a chave e criando um valor que é um objeto contendo timestamp e value. {timestamp, value} é uma abreviação para {timestamp: .timestamp, value: .value}.
  • <expression> | from_entries converte uma matriz com valor <expression> em um objeto, mapeando o campo key de cada entrada da matriz para a chave de objeto e o campo value de cada entrada da matriz para correspondente a essa chave.

O JSON a seguir mostra a saída da expressão 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
    }
  }
}

Criar matrizes

Criar literais de matrizes é semelhante à criação de literais de objetos. A sintaxe jq para um literal de matriz é semelhante ao JSON e JavaScript.

O exemplo a seguir mostra como extrair alguns valores em uma matriz simples para processamento posterior.

Dada a seguinte entrada:

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

Use a expressão jq a seguir para cria uma matriz com base nos valores dos campos temperature, humidity e pressure:

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

O JSON a seguir mostra a saída da expressão jq anterior:

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

Adicionar entradas a uma matriz

Você pode adicionar entradas ao início ou ao final de uma matriz usando o operador + com a matriz e as novas entradas. O operador += simplifica essa operação atualizando automaticamente a matriz com as novas entradas no final. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para adicionar os valores 12 e 41 ao final da matriz de valor lineStatus:

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

O JSON a seguir mostra a saída da expressão 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
  }
}

Remover entradas de uma matriz

Use a função del para remover entradas de uma matriz da mesma forma que para um objeto. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para remover a segunda entrada da matriz de valor lineStatus:

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

O JSON a seguir mostra a saída da expressão 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
  }
}

Remover entradas duplicadas na matriz

Se os elementos de matriz se sobrepõem, é possível remover as entradas duplicadas. Na maioria das linguagens de programação, você pode remover duplicatas usando variáveis de pesquisa paralelas. No jq, a abordagem mais eficaz é organizar os dados conforme necessário para o processamento e, em seguida, executar quaisquer operações antes de convertê-los de volta para o formato desejado.

O exemplo a seguir mostra como obter uma mensagem com alguns valores e em seguida, filtrá-la para ter apenas a leitura mais recente de cada valor. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para filtrar a entrada de modo que você tenha apenas a leitura mais recente de cada valor:

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

Dica

Se não se importar em recuperar o valor mais recente de cada nome, você pode simplificar a expressão para .payload |= unique_by(.name)

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • group_by(.name), dada uma matriz como entrada, coloca elementos em subconjuntos com base no valor de .name em cada elemento. Cada submatriz contém todos os elementos da matriz original com o mesmo valor de .name.
  • map(<expression>) usa a matriz de matrizes produzidas por group_by e executa <expression> em cada uma das submatrizes.
  • sort_by(.timestamp)[-1] extrai o elemento que você se importa de cada submatriz:
    • sort_by(.timestamp) ordena os elementos aumentando o valor do campo deles .timestamp para a submatriz atual.
    • [-1] recupera o último elemento da submatriz classificada, que é a entrada com a hora mais recente para cada nome.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Computar valor entre elementos de matriz

Você pode combinar os valores dos elementos de uma matriz para calcular grandezas como médias entre esses elementos.

Este exemplo mostra como reduzir a matriz recuperando o carimbo de data/hora de valor mais alto e o valor médio das entradas que compartilham o mesmo nome. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para recuperar o carimbo de data/hora de valor mais alto e o valor médio das entradas que compartilham o mesmo nome:

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

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • group_by(.name), pega uma matriz como entrada, coloca elementos em subconjuntos com base no valor de .name em cada elemento. Cada submatriz contém todos os elementos da matriz original com o mesmo valor de .name.
  • map(<expression>) usa a matriz de matrizes produzidas por group_by e executa <expression> em cada uma das submatrizes.
  • {name: <expression>, value: <expression>, timestamp: <expression>} constrói um objeto a partir da submatriz de entrada com os campos name, value e timestamp. Cada <expression> produz o valor desejado para a chave associada.
  • .[0].name recupera o primeiro elemento da submatriz e extrai o campo name dele. Todos os elementos na submatriz têm o mesmo nome, portanto, você só precisa recuperar o primeiro.
  • map(.value) | (add / length) calcula a média value de cada submatriz:
    • map(.value) converte a submatriz em uma matriz do campo value em cada entrada, nesse caso, retornando uma matriz de números.
    • add é uma função interna do jq que calcula a soma de uma matriz de números.
    • length é uma função interna do jq que calcula o número ou o comprimento de uma matriz.
    • add / length divide a soma pelo número para determinar a média.
  • map(.timestamp) | max localiza o valor máximo timestamp de cada submatriz:
    • map(.timestamp) converte a submatriz em uma matriz dos campos timestamp em cada entrada, nesse caso, retornando uma matriz de números.
    • max é uma função interna do jq que localiza o valor máximo em uma matriz.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Trabalhar com cadeias de caracteres

o jq fornece vários utilitários para manipulação e construção de cadeias de caracteres. Os exemplos a seguir mostram alguns casos de uso comuns.

Dividir cadeias de caracteres

Se uma cadeia de caracteres contém várias informações separadas por um caractere comum, você poderá usar a função split() para extrair as partes individuais.

O exemplo a seguir mostra como dividir uma cadeia de caracteres de tópico e retornar um segmento específico do tópico. Geralmente, essa técnica é útil quando ao trabalhar com expressões de chave de partição. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para dividir a cadeia de caracteres do tópico, usando / como separador, e retornar um segmento específico do tópico:

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

Na expressão jq anterior:

  • .topic | <expression> seleciona a chave topic do objeto raiz e executa <expression> nos dados que ela contém.
  • split("/") divide a cadeia de caracteres de tópico em uma matriz, dividindo a cadeia de caracteres sempre que encontra o caractere /. Nesse caso, ela produz ["assets", "slicer-3345", "tags", "rpm"].
  • [1] recupera o elemento no índice 1 da matriz da etapa anterior, nesse caso slicer-3345.`

O JSON a seguir mostra a saída da expressão jq anterior:

"slicer-3345"

Construir cadeias de caracteres dinamicamente

O jq permite que você construa cadeias de caracteres usando modelos de cadeia de caracteres com a sintaxe \(<expression>) dentro de uma cadeia de caracteres. Use esses modelos para criar cadeias de caracteres dinamicamente.

O exemplo a seguir mostra como adicionar um prefixo a cada chave em um objeto usando modelos de cadeia de caracteres. Dada a seguinte entrada:

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

Use a seguinte expressão jq para adicionar um prefixo a cada chave no objeto:

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

Na expressão jq anterior:

  • with_entries(<expression>) converte o objeto em uma matriz de pares chave/valor com estrutura {key: <key>, value: <value>}, executa <expression> em cada par chave/valor e converte os pares de volta em um objeto.
  • .key |= <expression> atualiza o valor de .key no objeto de pares chave/valor para o resultado de <expression>. Usar |= em vez de = define o contexto de dados de <expression> para o valor de .key, em vez do objeto completo de pares chave/valor.
  • "current-\(.)" produz uma cadeia de caracteres que começa com "current-" e depois insere o valor do contexto de dados atual ., nesse caso, o valor da chave. A sintaxe \(<expression>) dentro da cadeia de caracteres indica que você deseja substituir essa parte da cadeia de caracteres pelo resultado da execução de <expression>.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Trabalhar com expressões regulares

O jq dá suporte a expressões regulares padrão. Você pode usar expressões regulares para extrair, substituir e verificar padrões dentro de cadeias de caracteres. Algumas funções comuns de expressões regulares no jq incluem test(), match(), split(), capture(), sub() e gsub().

Extrair valores usando expressões regulares

Se você não puder usar a divisão de cadeias de caracteres para extrair um valor de uma cadeia de caracteres, tente usar as expressões regulares para extrair os valores necessários.

O exemplo a seguir mostra como normalizar chaves de objeto testando uma expressão regular e substituindo-as por um formato diferente. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para normalizar as chaves de objeto:

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

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • with_entries(<expression>) converte o objeto em uma matriz de pares chave/valor com estrutura {key: <key>, value: <value>}, executa <expression> em cada par chave/valor e converte os pares de volta em um objeto.
  • .key |= <expression> atualiza o valor de .key no objeto de pares chave/valor para o resultado de <expression>. usar |= em vez de = define o contexto de dados de <expression> para o valor de .key, em vez do objeto completo de pares chave/valor.
  • if test("^dtmi:.*:(?<tag>[^:]+)$") then capture("^dtmi:.*:(?<tag>[^:]+)$").tag else . end verifica e atualiza a chave com base em uma expressão regular:
    • test("^dtmi:.*:(?<tag>[^:]+)$") verifica o contexto de dados de entrada, a chave nesse caso, em relação à expressão regular ^dtmi:.*:(?<tag>[^:]+)$. Se a expressão regular corresponder, ela retorna verdadeiro. Caso contrário, retorna falso.
    • capture("^dtmi:.*:(?<tag>[^:]+)$").tag executa a expressão regular ^dtmi:.*:(?<tag>[^:]+)$ no contexto de dados de entrada, a chave nesse caso, e coloca todos os grupos de captura da expressão regular, indicados por (?<tag>...), em um objeto como a saída. Em seguida, a expressão extrai .tag desse objeto para retornar as informações extraídas pela expressão regular.
    • . no branch else, a expressão aprova os dados sem alteração.

O JSON a seguir mostra a saída da expressão 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
  }
}

Dividir mensagens separadas

Um recurso útil da linguagem jq é sua capacidade de produzir várias saídas a partir de uma única entrada. Esse recurso permite que você divida mensagens em várias mensagens separadas para que o pipeline processe. A chave para essa técnica é .[], que divide as matrizes em valores separados. Os exemplos a seguir mostram alguns cenários que usam essa sintaxe.

Número dinâmico de saídas

Geralmente, quando você deseja dividir uma mensagem em várias saídas, o número de saídas desejado depende da estrutura da mensagem. A sintaxe [] permite que você faça esse tipo de divisão.

Por exemplo, suponha que você tenha mensagem com uma lista de tags que deseja colocar em mensagens separadas. Dada a seguinte entrada:

{
  "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 a seguinte expressão jq para dividir a mensagem em várias mensagens:

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

Na expressão jq anterior:

  • .payload.Payload = (.payload.Payload | to_entries[]) divide a mensagem em várias mensagens:
    • .payload.Payload = <expression> atribui o resultado da execução de <expression> a .payload.Payload. Normalmente, você usa |= nesse caso para restringir o contexto de <expression> para .payload.Payload, mas |= não dá suporte à divisão da mensagem separada, portanto, em vez disso, use =.
    • (.payload.Payload | <expression>) restringe o lado direito da expressão de atribuição para .payload.Payload, para que <expression> opere na parte correta da mensagem.
    • to_entries[] é combinação de duas operações e é uma abreviação de to_entries | .[]:
      • to_entries converte o objeto em uma matriz de pares chave/valor com o esquema {"key": <key>, "value": <value>}. Essas informações são o que você deseja separar em diferentes mensagens.
      • [] realiza a divisão das mensagens. Cada entrada na matriz torna-se um valor separado no jq. Quando a atribuição a .payload.Payload ocorre, cada valor separado resulta em uma cópia da mensagem geral sendo feita, com .payload.Payload definido para o valor correspondente produzido pelo lado direito da atribuição.
  • .payload |= <expression> substitui o valor de .payload pelo resultado da execução de <expression>. Neste ponto, a consulta está lidando com um fluxo de valores em vez de um único valor como resultado da divisão na operação anterior. Portanto, a atribuição é executada uma vez para cada mensagem que a operação anterior produz em vez de ser executada apenas uma vez no total.
  • {DataSetWriterName, SequenceNumber, ...} constrói um novo objeto que é o valor de .payload. DataSetWriterName e SequenceNumber não são alterados, para que você possa usar a sintaxe abreviada em vez de escrever DataSetWriterName: .DataSetWriterName e SequenceNumber: .SequenceNumber.
  • Tag: .Payload.key, extrai a chave original do objeto interno Payload e a eleva para o objeto pai. A operação to_entries anterior na consulta criou o campo key.
  • Value: .Payload.value.value e Timestamp: .Payload.value.sourceTimestamp executam uma extração semelhante de dados do conteúdo interno. Desta vez do valor do par chave/valor original. O resultado é um objeto de conteúdo simples que você pode usar em processamentos posteriores.

O JSON a seguir mostra as saídas da expressão jq anterior. Cada saída torna-de uma mensagem autônoma para estágios posteriores de processamento no pipeline:

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

Número fixo de saídas

Para dividir uma mensagem em um número fixo de saídas em vez de um número dinâmico de saídas com base na estrutura da mensagem, use o operador , em vez de [].

O exemplo a seguir mostra como dividir os dados em duas mensagens com base nos nomes de campo existentes. Dada a seguinte entrada:

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

Use a seguinte expressão jq para dividir a mensagem em duas mensagens:

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

Na expressão jq anterior:

  • .payload = ({<fields>},{<fields>}) atribui os dois literais de objeto ao .payload na mensagem. Os objetos separados por vírgula produzem dois valores separados e são atribuídos .payload, o que faz com que a mensagem inteira seja dividida em duas mensagens. Cada nova mensagem tem o .payload definido para um dos valores.
  • {field: "temperature", minimum: .payload.minTemperature, maximum: .payload.maxTemperature} é um construtor de objeto literal que preenche os campos de um objeto com uma cadeia de caracteres literal e outros dados obtidos do objeto.

O JSON a seguir mostra as saídas da expressão jq anterior. Cada saída torna-se uma mensagem autônoma para estágios de processamento adicionais:

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

Operações matemáticas

O jq dá suporte a operações matemáticas comuns. Algumas operações são operadores, como + e -. Outras operações são funções, como sin e exp.

Aritmético

O jq dá suporte a cinco operações aritméticas comuns: adição (+), subtração (-), multiplicação (*), divisão (/) e módulo (%). Ao contrário de muitos recursos do jq, essas operações são operações infixas que permitem que você escreva a expressão matemática completa em uma única expressão, sem separadores |.

O exemplo a seguir mostra como converter uma temperatura de Fahrenheit para Celsius e extrair leitura atual em segundos de um carimbo de data e hora Unix em milissegundos. Dada a seguinte entrada:

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

Use a seguinte expressão jq para converter a temperatura de Fahrenheit para Celsius e extrair a leitura atual em segundos de um carimbo de data e hora Unix em milissegundos:

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

Na expressão jq anterior:

  • .payload.temperatureC = (5/9) * (.payload.temperatureF - 32) cria um novo campo temperatureC no conteúdo que é definido como a conversão de temperatureF Fahrenheit para Celsius.
  • .payload.seconds = (.payload.timestamp / 1000) % 60 pega um tempo Unix em milissegundos e o converte para segundos e, em seguida, extrai o número de segundos no minuto atual usando um cálculo de modulo.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Funções matemáticas

O jq inclui várias funções que realizam operações matemáticas. Você pode encontrar a lista completa no manual do jq.

O exemplo a seguir mostra como calcular a energia cinética dos campos de massa e velocidade. Dada a seguinte entrada:

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

Use a seguinte expressão jq para calcular a energia cinética dos campos de massa e velocidade:

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

Na expressão jq anterior:

  • .payload.energy = <expression> cria um novo campo energy no conteúdo que é o resultado da execução de <expression>.
  • (0.5 * (.userProperties | from_entries).mass * pow(.payload.velocity; 2) | round) é a fórmula para a energia:
    • (.userProperties | from_entries).mass extrai a entrada mass da lista userProperties. Os dados já estão configurados como objetos com key e value, então from_entries pode convertê-los diretamente em um objeto. A expressão recupera a chave mass do objeto resultante e retorna seu valor.
    • pow(.payload.velocity; 2) extrai a velocidade do conteúdo e a eleva ao quadrado, ou seja, elevando-a à potência de 2.
    • <expression> | round arredonda o resultado para o número inteiro mais próximo para evitar uma precisão enganosamente alta no resultado.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Lógica booliana

Os pipelines de processamento de dados geralmente usam o jq para filtrar mensagens. Normalmente, a filtragem usa expressões e operadores boolianos. Além disso, a lógica booliana é útil para realizar o fluxo de controle em transformações e casos de uso de filtragem mais avançados.

Os exemplos a seguir ilustram algumas das funcionalidades mais comuns usadas em expressões boolianas no jq:

Operadores boolianos básicos e condicionais

O jq fornece os operadores lógicos boolianos básicos and, or e not. Os operadores and e or são operadores infixos. not é uma função que você invoca como um filtro, por exemplo, <expression> | not.

O jq possui os operadores condicionais >, <, ==, !=, >= e <=. Esses operadores são infixos.

O exemplo a seguir mostra como executar alguma lógica booliana básica usando condicionais. Dada a seguinte entrada:

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

Use a seguinte expressão jq para verificar se:

  • A temperatura está entre 30 graus e 60 graus, inclusive no limite superior.
  • A umidade é inferior 80, e o local é Redmond.
.payload
| ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond"
| not

Na expressão jq anterior:

  • .payload | <expression> limita <expression> ao conteúdo de .payload. Essa sintaxe torna o restante da expressão menos prolixa.
  • ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond" retorna verdadeiro se a temperatura estiver entre 30 graus e 60 graus (inclusive no limite superior) ou a umidade for inferior a 80, e só retornará verdadeiro se o local também for Redmond.
  • <expression> | not usa o resultado da expressão anterior e aplica um expressão NOT lógica a ela, neste exemplo, invertendo o resultado de true para false.

O JSON a seguir mostra a saída da expressão jq anterior:

false

Verificar a existência da chave de objeto

Você pode criar um filtro que verifica a estrutura de uma mensagem em vez de seu conteúdo. Por exemplo, você pode verificar se uma chave específica está presente em um objeto. Para fazer essa verificação, use a função has ou uma verificação em relação a nulo. O exemplo a seguir mostra as duas abordagens. Dada a seguinte entrada:

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

Use a seguinte expressão jq para verificar se o conteúdo tem um campo temperature, se o campo site não é nulo e outras verificações:

.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)
}

Na expressão jq anterior:

  • .payload | <expression> limita o contexto de dados de <expression> ao valor de .payload para tornar a <expression> menos prolixa.
  • hasTemperature: has("temperature"), e outras expressões semelhantes demonstram como a função has se comporta com um objeto de entrada. A função retorna verdadeiro somente se a chave estiver presente. hasSite é verdadeiro apesar do valor de site ser null.
  • temperatureNotNull: (.temperature != null), e outras expressões semelhantes demonstram como a verificação != null executa uma verificação semelhante a has. Uma chave inexistente em um objeto é null se acessada usando a sintaxe .<key>, ou a chave existe, mas tem um valor de null. Ambas expressões siteNotNull e missingNotNull são falsas, mesmo que uma chave esteja presente e a outra esteja ausente.
  • hasNested: (has("nested") and (.nested | has("inner"))) realiza uma verificação em um objeto aninhado com has, onde o objeto pai pode não existir. O resultado é uma cascata de verificações em cada nível para evitar erros.
  • nestedNotNull: (.nested?.inner != null) realiza a mesma verificação em um objeto aninhado usando != null e o ? para permitir o encadeamento de caminho em campos que talvez não existam. Essa abordagem produz uma sintaxe mais limpa para cadeias profundamente aninhadas que podem ou não existir, mas não pode diferenciar os valores-chave null dos que não existem.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Verificar a existência de entradas na matriz

Use a função any para verificar a existência de uma entrada em uma matriz. Dada a seguinte entrada:

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

Use a expressão jq a seguir para verificar se a matriz userProperties possui uma entrada com uma chave mass e nenhuma entrada com uma chave missing:

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

Na expressão jq anterior:

  • .userProperties | <expression> limita contexto de dados de <expression> ao valor de userProperties para tornar o restante da <expression> menos prolixa.
  • any(.key == "mass") executa a expressão .key == "mass" em cada elemento da matriz userProperties, retornando verdadeiro se a expressão for avaliada como verdadeira para pelo menos um elemento da matriz.
  • (any(.key == "missing") | not) executa .key == "missing" em cada elemento da matriz userProperties, retornando verdadeiro se algum elemento for avaliado como verdadeiro, e depois negar o resultado geral com | not.

O JSON a seguir mostra a saída da expressão jq anterior:

true

Fluxo de controle

O fluxo de controle no jq é diferente da maioria das linguagens, pois a maioria das formas de fluxo de controle são diretamente orientadas por dados. Ainda há suporte para expressões if/else com semântica tradicional de programação funcional, mas você pode alcançar a maioria das estruturas de loop usando combinações das funções map e reduce.

Os exemplos a seguir mostram alguns cenários comuns de fluxo de controle no jq.

Instruções if/else

O jq dá suporte a condições usando if <test-expression> then <true-expression> else <false-expression> end. Você pode inserir mais casos adicionando elif <test-expression> then <true-expression> no meio. Uma diferença fundamental entre o jq e muitas outras linguagens é que cada expressão then e else produz um resultado que é usado em operações subsequentes na expressão geral do jq.

O exemplo a seguir demonstra como usar instruções if para produzir informações condicionais. Dada a seguinte entrada:

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

Use a seguinte expressão jq para verificar se a temperatura é alta, baixa ou normal:

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

Na expressão jq anterior:

  • .payload.status = <expression> atribui o resultado da execução de <expression> a um novo campo status no conteúdo.
  • if ... end é a expressão if/elif/else principal:
    • if .payload.temperature > 80 then "high" verifica a temperatura em relação a um valor alto, retornando "high" se verdadeiro; caso contrário, continua.
    • elif .payload.temperature < 30 then "low" realizar uma segunda verificação em relação à temperatura para um valor baixo, definindo o resultado como "low" se verdadeiro; caso contrário, continua.
    • else "normal" end retorna "normal" se nenhuma das verificações anteriores for verdadeira e encerra a expressão com end.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Mapeamento

Em linguagens funcionais como jq, a maneira mais comum de executar a lógica iterativa é criar uma matriz e mapear os valores dessa matriz para novos valores. Essa técnica é alcançada no jq usando a função map, que aparece em muitos dos exemplos neste guia. Se você quiser executar alguma operação em relação a vários valores, map provavelmente é a resposta.

O exemplo a seguir mostra como usar map para remover um prefixo das chaves de um objeto. Essa solução pode ser escrita de forma mais sucinta usando with_entries, mas a versão mais detalhada mostrada aqui apresenta o mapeamento real acontecendo nos bastidores da abordagem simplificada. Dada a seguinte entrada:

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

Use a seguinte expressão jq para remover o prefixo rotor_ das chaves do conteúdo:

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

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • (to_entries | map(<expression) | from_entries) realiza a conversão de objetos-matriz e mapeia cada entrada para um novo valor com <expression>. Essa abordagem é semanticamente equivalente a with_entries(<expression>):
    • to_entries converte um objeto em uma matriz, com cada par chave/valor se tornando um objeto separado com a estrutura {"key": <key>, "value": <value>}.
    • map(<expression>) executa <expression> em cada elemento na matriz e produz uma matriz de saída com os resultados de cada expressão.
    • from_entries é o inverso de to_entries. A função converte uma matriz de objetos com a estrutura {"key": <key>, "value": <value>} em um objeto com os campos key e value mapeados em pares chave/valor.
  • .key |= ltrimstr("rotor_") atualiza o valor de .key em cada entrada com o resultado de ltrimstr("rotor_"). A sintaxe |= limita o contexto de dados do lado direito para o valor de .key. ltrimstr remove o prefixo especificado da cadeia de caracteres, se presente.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Reduzir

A redução é a principal maneira de executar operações de loop ou iterativas entre os elementos de uma matriz. A operação reduzir consiste de um acumuladore um operação que usa o acumulador e o elemento atual da matriz como entradas. Cada iteração do loop retorna o próximo valor do acumulador, e a saída final da operação de redução é o último valor acumulador. A redução é conhecida como fold em algumas outras linguagens de programação funcionais.

Use a operação reduce no jq para executar reduções. Na maioria dos casos de uso, não é necessário realizar essa manipulação de baixo nível e, em vez disso, é possível usar funções de nível mais alto, no entanto, reduce é uma ferramenta geral útil.

O exemplo a seguir mostra como calcular a mudança média no valor de uma métrica ao longo dos pontos de dados que você possui. Dada a seguinte entrada:

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

Use a seguinte expressão jq para calcular a mudança média de valor entre os pontos de dados:

.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
)

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • reduce .[] as $item (<init>; <expression>) é o scaffolding de uma operação de redução típica com as seguintes partes:
    • .[] as $item deve sempre ser <expression> as <variable> e é mais frequentemente .[] as $item. A <expression> produz um fluxo de valores, cada um dos quais é salvo em <variable> para uma iteração da operação de redução. Se você tiver uma matriz pela qual deseja iterar, .[] a divide em um fluxo. Essa sintaxe é a mesma usada para dividir mensagens, mas a operação reduce não usa o fluxo para gerar várias saídas. reduce não divide a mensagem.
    • <init>, nesse caso null, é o valor inicial do acumulador usado na operação de redução. Esse valor normalmente é definido como vazio ou zero. Esse valor torna-se o contexto de dados, ., neste loop <expression>, para a primeira iteração.
    • <expression> é a operação realizada em cada iteração da operação de redução. Ela tem acesso ao valor atual do acumulador, por meio de ., e o valor atual no fluxo por meio de <variable> declarado anteriormente, nesse caso $item.
  • if . == null then {totalChange: 0, previous: $item.value, count: 0} é uma condição para lidar com a primeira iteração de redução. Essa condição configura a estrutura do acumulador para a próxima iteração. Como a expressão computa diferenças entre entradas, a primeira entrada configura os dados usados para calcular uma diferença na segunda iteração de redução. Os campos totalChange, previous e count servem como variáveis de loop e atualizam a cada iteração.
  • .totalChange += (($item.value - .previous) | length) | .previous = $item.value | .count += 1 é a expressão no caso de else. Essa expressão define cada campo no objeto acumulador como um novo valor com base em uma computação. Para totalChange, ela encontra a diferença entre os valores atuais e anteriores e obtém o valor absoluto. De forma contra-intuitiva, usa a função length para obter o valor absoluto. previous é definido como o value atual do $item para a próxima iteração a ser usada e o count é incrementado.
  • .totalChange / .count computa a mudança média entre os pontos de dados após a conclusão da operação de redução e você tem o valor final do acumulador.

O JSON a seguir mostra a saída da expressão jq anterior:

{
  "payload": 5.25
}

Loops

Geralmente, os loops no jq são reservados para casos de uso avançados. Como cada operação no jq é uma expressão que produz um valor, a semântica de loop orientada por instruções na maioria das linguagens não se ajustam naturalmente no jq. Considere usar map ou reduce atender às suas necessidades.

Existem dois tipos principais de loop tradicionais no jq. Outros tipos de loop existem, mas servem para casos de uso mais especializados:

  • while aplica uma operação repetidamente em relação ao contexto de dados de entrada, atualizando o valor do contexto de dados para uso na próxima iteração e produzindo esse valor como saída. A saída de um loop while é uma matriz que contém os valores produzidos por cada iteração do loop.
  • until, como while, aplica uma operação repetidamente em relação ao contexto de dados de entrada, atualizando o valor do contexto de dados para uso na próxima iteração. Ao contrário dewhile, o loop until gera como saída o valor produzido pela última iteração do loop.

O exemplo a seguir mostra como usar um loop until para eliminar progressivamente pontos de dados discrepantes de uma lista de leituras até que o desvio padrão fique abaixo de um valor predefinido. Dada a seguinte entrada:

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

Use a expressão jq a seguir para eliminar progressivamente pontos de dados discrepantes de uma lista de leituras até que o desvio padrão fique abaixo 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
  )
)

Na expressão jq anterior:

  • def avg: add / length; define uma nova função chamada avg que é usada para calcular médias posteriormente na expressão. A expressão à direita de : é a expressão lógica usada sempre que você usa avg. A expressão <expression> | avg é equivalente a <expression> | add / length
  • def stdev: avg as $mean | (map(. - $mean | . * .) | add) / (length - 1) | sqrt; define uma nova função chamada stdev. A função calcula o desvio padrão amostral de uma matriz usando uma versão modificada da resposta da comunidade no StackOverflow.
  • .payload |= <expression> as duas primeiras def são apenas declarações e iniciam a expressão real. A expressão executa <expression> com um objeto de dados de entrada de .payload e atribui o resultado de volta a .payload.
  • sort_by(.value) classifica a matriz de entradas de matriz pelo campo value. Essa solução exige que você identifique e manipule os valores mais altos e mais baixos em uma matriz, portanto, classificar os dados com antecedência reduz o tempo de computação e simplifica o código.
  • until(<condition>; <expression>) executa <expression> em relação à entrada até que <condition> retorne verdadeiro. A entrada para cada execução de <expression> e <condition> é a saída da execução anterior de <expression>. O resultado da última execução de <expression> é retornado do loop.
  • (map(.value) | stdev) < 2 or length == 0 é a condição do loop:
    • map(.value) converte a matriz em uma lista de números puros para uso na próxima computação.
    • (<expression> | stdev) < 2 calcula o desvio padrão da matriz e retorna verdadeiro se o desvio padrão for menor que 2.
    • length == 0 obtém o comprimento da matriz de entrada e retorna verdadeiro se for 0. Para proteger contra o caso em que todas as entradas são eliminadas, o resultado é or-ed com a expressão geral.
  • (map(.value) | avg) as $avg converte a matriz em uma matriz de números e calcula a média delas e, em seguida, salva o resultado em uma variável $avg. Essa abordagem economiza custos de computação porque você reutiliza a média várias vezes na iteração de loop. As expressões de atribuição de variável não alteram o contexto de dados da próxima expressão depois de |, portanto, o restante da computação ainda tem acesso à matriz completa.
  • if <condition> then <expression> else <expression> end é a lógica central da iteração do loop. Usa <condition> para determinar a <expression> a ser executada e retornada.
  • ((.[0].value - $avg) | length) > ((.[-1].value - $avg) | length) é a condição if que compara os valores mais altos e mais baixos com o valor médio e compara essas diferenças:
    • (.[0].value - $avg) | length recupera o campo value da primeira entrada de matriz e obtém a diferença entre ela e a média geral. A primeira entrada de matriz é a mais baixa devido à classificação anterior. Esse valor pode ser negativo, então o resultado é canalizado para length, que retorna o valor absoluto quando fornecido com um número como entrada.
    • (.[-1].value - $avg) | length realiza a mesma operação na última entrada de matriz e também calcula o valor absoluto, por segurança. A última entrada de matriz é a mais alta devido à classificação anterior. Os valores absolutos são comparados na condição geral usando >.
  • del(.[0]) é a expressão then que é executada quando a primeira entrada de matriz foi a maior discrepância. A expressão remove o elemento em .[0] da matriz. A expressão retorna os dados restantes na matriz após a operação.
  • del(.[-1]) é a expressão else que é executada quando a última entrada de matriz foi a maior discrepância. A expressão remove o elemento em .[-1], que é a última entrada, da matriz. A expressão retorna os dados restantes na matriz após a operação.

O JSON a seguir mostra a saída da expressão jq anterior:

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

Remover mensagens

Ao escrever uma expressão de filtro, você pode instruir o sistema a remover as mensagens que não você não deseja retornando falso. Esse comportamento é o comportamento básico das expressões condicionais no jq. No entanto, há momentos em que você está transformando mensagens ou executando filtros mais avançados quando deseja que o sistema remova mensagens de forma explícita ou implícita. Os exemplos a seguir mostram como implementar esse comportamento.

Remoção explícita

Para remover explicitamente uma mensagem em uma expressão de filtro, retorne false da expressão.

Você também pode remover uma mensagem de dentro de uma transformação usando a função interna empty no jq.

O exemplo a seguir mostra como calcular uma média dos pontos de dados na mensagem e remover mensagens com uma média abaixo de um valor fixo. É possível e válido alcançar esse comportamento com a combinação de um estágio de transformação e um estágio de filtro. Use a abordagem que se adequar melhor à sua situação. Dadas as seguintes entradas:

Mensagem 1

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

Mensagem 2

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

Use a seguinte expressão jq para calcular a média dos pontos de dados e descartar mensagens com uma temperatura média inferior a 30 ou uma umidade média superior a 90:

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

Na expressão jq anterior:

  • .payload |= <expression> usa |= para atualizar o valor de .payload com o resultado da execução <expression>. Usar |= em vez de = define o contexto de dados de <expression> como .payload em vez ..
  • map_values(add / length) executa add / length para cada valor no subobjeto .payload. A expressão soma os elementos na matriz de valores e depois divide pelo comprimento da matriz para calcular a média.
  • if .payload.temperature > 30 and .payload.humidity < 90 then . else empty end verifica duas condições em relação à mensagem resultante. Se o filtro avaliar como verdadeiro, como na primeira entrada, a mensagem completa será produzida como uma saída. Se o filtro avaliar como falso, como na segunda entrada, retornará empty, o que resultará em um fluxo vazio com valores zero. Esse resultado faz com que a expressão solte a mensagem correspondente.

Saída 1

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

Saída 2

(sem saída)

Remoção implícita usando erros

Tanto as expressões de filtro quanto as de transformação podem remover mensagens implicitamente ao fazer com que o jq produza um erro. Embora essa abordagem não seja uma prática recomendada porque o pipeline não consegue diferenciar um erro que você causou intencionalmente e um provocado por uma entrada inesperada em sua expressão. No momento, o sistema lida com um erro de runtime no filtro ou transformação, descartando a mensagem e registrando o erro.

Um cenário comum que usa essa abordagem é quando uma entrada para um pipeline pode ter mensagens que são estruturalmente desconexas. O exemplo a seguir mostra como receber dois tipos de mensagens, um dos quais é avaliado com êxito em relação ao filtro e o outro estruturalmente incompatível com a expressão. Dadas as seguintes entradas:

Mensagem 1

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

Mensagem 2

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

Use a seguinte expressão jq para filtrar mensagens com uma temperatura inferior a 10 e uma umidade superior a 80:

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

No exemplo anterior, a expressão em si é uma expressão booliana composta simples. A expressão foi projetada para funcionar com a estrutura da primeira das mensagens de entrada mostrada anteriormente. Quando a expressão recebe a segunda mensagem, a estrutura da matriz de .payload é incompatível com o acesso ao objeto na expressão e resulta em um erro. Se desejar filtrar com base nos valores de temperatura/umidade e remover mensagens com estrutura incompatível, essa expressão funciona. Outra abordagem que não resulta em nenhum erro é adicionar o (.payload | type) == "object" and ao início da expressão.

Saída 1

true

Saída 2

(erro)

Utilitários de tempo

o jq não dá suporte ao tempo como um tipo nativo. No entanto, alguns formatos aceitos e emitidos pelo Processador de Dados dão suporte ao tempo como um tipo nativo. Esses tipos normalmente são representados usando o tipo de time.Time do Go.

Para permitir que você interaja com esses valores do jq, o Processador de Dados fornece um módulo com um conjunto de funções que permitem:

  • Converter entre tempo nativo, cadeias de caracteres ISO 8601 e carimbos de data/hora numéricos do Unix.
  • Executar várias operações específicas de tempo em todos esses tipos.

O módulo time

Todas as funções especiais específicas do tempo são especificadas em um módulo de time que você pode importar para uma consulta.

Importe o módulo no início da sua consulta de duas maneiras:

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

O primeiro método coloca todas as funções do módulo em um namespace, por exemplo time::totime. O segundo método simplesmente coloca todas as funções binárias no nível superior, por exemplo totime. Ambas as sintaxes são válidas e funcionalmente equivalentes.

Formatos e conversão

O módulo de hora funciona com três formatos de tempo:

  • time é um valor de tempo nativo. Você só pode usá-lo com as funções no módulo de tempo. Reconhecido como um tipo de dados time ao serializar.
  • unix é um carimbo de data/hora Unix numérico que representa o tempo como segundos desde a época do Unix. Pode ser um inteiro ou um número de ponto flutuante. Reconhecido como o tipo numérico correspondente ao serializar.
  • iso é uma representação de tempo em formato de cadeia de caracteres ISO 8601. Reconhecido como uma cadeia de caracteres ao serializar.

O módulo de tempo fornece as seguintes funções para verificar e manipular entre esses tipos:

  • time::totime converte qualquer um dos três tipos para time.
  • time::tounix converte qualquer um dos três tipos para unix.
  • time::toiso converte qualquer um dos três tipos para iso.
  • time::istime retorna verdadeiro se os dados estiverem no formato de time.

Operações de tempo

O módulo de tempo fornece várias operações específicas de tempo que operam em todos os tipos. As funções a seguir podem usar qualquer um dos tipos com suporte como entrada e retornar o mesmo tipo que a saída. Carimbos de data/hora inteiros poderão ser convertidos em carimbos de data/hora de ponto flutuante se mais precisão for necessária.

  • time::utcconverte a hora em UTC.
  • time::zone(zone) converte a hora na zona fornecida. zone é uma cadeia de caracteres de zona ISO 8601. Por exemplo, time::zone("-07").
  • time::localconverte a hora em hora local.
  • time::offset(duration) compensa o tempo pela duração fornecida. durationusa a sintaxe da cadeia de caracteres de duração do Go. Por exemplo, time::offset("1m2s").
  • time::offset(value;unit) compensa o tempo pela duração fornecida. Essa função usa um número e uma cadeia de caracteres de unidade. Por exemplo, time::offset(2;"s"). Essa função é útil quando a duração vem de outra propriedade.

Observação

As três funções de fuso horário não têm efeito significativo nos carimbos de data/hora Unix.

Utilitários diversos

O módulo util é uma coleção de utilitários que expande os recursos do runtime do jq.

O módulo util

Todas os utilitários diversos são especificadas em um módulo de util que você pode importar para uma consulta.

Importe o módulo no início da sua consulta de duas maneiras:

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

O primeiro método coloca todas as funções do módulo em um namespace, por exemplo util::uuid. O segundo método simplesmente coloca todas as funções diversas no nível superior, por exemplo uuid. Ambas as sintaxes são válidas e funcionalmente equivalentes.

O módulo util atualmente inclui a função uuid que retorna uma nova UUID aleatória no formato de cadeia de caracteres padrão.

Manipulação binária

o jq foi projetado para trabalhar com dados que podem ser representados como JSON. No entanto, os pipelines da versão prévia do Processador de Dados Internet das Coisas do Azure também dão suporte a um formato de dados brutos que contém dados binários não analisados. Para trabalhar com dados binários, a versão do jq que acompanha o Processador de Dados contém um pacote desenvolvido para ajudá-lo a processar dados binários. Com ele, você pode:

  • Converta entre formatos binários e outros, como matrizes de base64 e de números inteiros.
  • Use funções internas para ler valores numéricos e de cadeia de caracteres de uma mensagem binária.
  • Faça edições pontuais de dados binários, preservando seu formato.

Importante

Você não pode usar nenhuma função interna jq ou operadores que modifiquem um valor binário. Isso significa que não há concatenação com +, nenhuma map operando em relação aos bytes e nenhuma atribuição misturada com valores binários como |=, +=, //=. Você pode usar a atribuição padrão (==). Se você tentar usar dados binários com uma operação sem suporte, o sistema gerará um erro jqImproperBinaryUsage. Caso precise manipular seus dados binários de maneiras personalizadas, considere usar uma das funções a seguir para convertê-los em base64 ou uma matriz de números inteiros para realizar a computação e convertê-los de volta para o formato binário quando necessário.

As seções a seguir descrevem o suporte binário no mecanismo jq do Processador de Dados.

O módulo binary

Todo o suporte binário no mecanismo jq do Processador de Dados é especificado em um módulo binary que você pode importar.

Importe o módulo no início da sua consulta de duas maneiras:

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

O primeiro método coloca todas as funções do módulo em um namespace, por exemplo binary::tobase64. O segundo método simplesmente coloca todas as funções binárias no nível superior, por exemplo tobase64. Ambas as sintaxes são válidas e funcionalmente equivalentes.

Formatos e conversão

O módulo binário funciona com três tipos:

  • binário: um valor binário, que só pode ser usado diretamente com as funções no módulo binário. Reconhecido por um pipeline como um tipo de dados binário ao serializar. Use este tipo para serialização bruta.
  • matriz: um formato que converte o binário em uma matriz de números, permitindo que você faça seu próprio processamento. Reconhecido por um pipeline como uma matriz de números inteiros ao serializar.
  • base64: uma representação em formato de cadeia de caracteres de binários. Principalmente útil se você quiser converter entre binários e cadeias de caracteres. Reconhecido por um pipeline como uma cadeia de caracteres ao serializar.

Você pode converter entre os três tipos em suas consultas jq, dependendo das necessidades específicas. Por exemplo, é possível converter de binário em uma matriz, realizar manipulações personalizadas e, em seguida, converter de volta para binário a fim de preservar as informações do tipo.

Funções

As seguintes funções são fornecidas para verificar e manipular entre esses tipos:

  • binary::tobinary converte qualquer um dos três tipos para binário.
  • binary::toarray converte qualquer um dos três tipos para matriz.
  • binary::tobase64 converte qualquer um dos três tipos para base64.
  • binary::isbinary retorna verdadeiro se os dados estiverem no formato binário.
  • binary::isarray retorna verdadeiro se os dados estiverem no formato de matriz.
  • binary::isbase64 retorna verdadeiro se os dados estiverem no formato base64.

O módulo também fornece a função binary::edit(f) para edições rápidas de dados binários. A função converte a entrada para o formato de matriz, aplica a função desejada e, em seguida, converte o resultado de volta para binário.

Extrair dados de binários

O módulo binário permite que você extraia valores dos dados binários, facilitando o desempacotamento de cargas binárias personalizadas. Em geral, essa funcionalidade segue padrões semelhantes a outras bibliotecas de desempacotamento binário, utilizando uma nomenclatura semelhante. Os seguintes tipos podem ser descompactados:

  • Números inteiros (int8, int16, int32, int64, uint8, uint16, uint32, uint64)
  • Floats (float, double)
  • Cadeias de caracteres (utf8)

O módulo também permite especificar deslocamentos e endianness (extremidade), quando aplicável.

Funções para ler dados binários

O módulo binário fornece as seguintes funções para extrair dados de valores binários. Você pode usar todas as funções com qualquer um dos três tipos que o pacote pode converter entre si.

Todos os parâmetros de função são opcionais, sendo que offset tem o padrão 0 e length tem o padrão para o restante dos dados.

  • binary::read_int8(offset) lê um int8 de um valor binário.
  • binary::read_int16_be(offset) lê um int16 de um valor binário em ordem big-endian.
  • binary::read_int16_le(offset) lê um int16 de um valor binário em ordem little-endian.
  • binary::read_int32_be(offset) lê um int32 de um valor binário em ordem big-endian.
  • binary::read_int32_le(offset) lê um int32 de um valor binário em ordem little-endian.
  • binary::read_int64_be(offset) lê um int64 de um valor binário em ordem big-endian.
  • binary::read_int64_le(offset) lê um int64 de um valor binário em ordem little-endian.
  • binary::read_uint8(offset) lê um uint8 de um valor binário.
  • binary::read_uint16_be(offset) lê um uint16 de um valor binário em ordem big-endian.
  • binary::read_uint16_le(offset) lê um uint16 de um valor binário em ordem little-endian.
  • binary::read_uint32_be(offset) lê um uint32 de um valor binário em ordem big-endian.
  • binary::read_uint32_le(offset) lê um uint32 de um valor binário em ordem little-endian.
  • binary::read_uint64_be(offset) lê um uint64 de um valor binário em ordem big-endian.
  • binary::read_uint64_le(offset) lê um uint64 de um valor binário em ordem little-endian.
  • binary::read_float_be(offset) lê um float de um valor binário em ordem big-endian.
  • binary::read_float_le(offset) lê um float de um valor binário em ordem little-endian.
  • binary::read_double_be(offset) lê um double de um valor binário em ordem big-endian.
  • binary::read_double_le(offset) lê um double de um valor binário em ordem little-endian.
  • binary::read_bool(offset; bit) lê um booliano de um valor binário, verificando o bit fornecido para o valor.
  • binary::read_bit(offset; bit) lê um pouco de um valor binário, usando o índice de bits fornecido.
  • binary::read_utf8(offset; length) lê uma cadeia de caracteres UTF-8 de um valor binário.

Gravar dados binários

O módulo binário permite que você codifique e grave valores binários. Essa funcionalidade permite que você construa ou faça edições em conteúdos binários diretamente no jq. A gravação de dados dá suporte ao mesmo conjunto de tipos de dados que a extração de dados e também permite que você especifique a endianness (extremidade) a ser usada.

A gravação de dados ocorre em duas formas:

  • Funções write_* atualizam dados no local em um valor binário, sendo utilizadas para atualizar ou manipular valores existentes.
  • Funções append_* adicionam dados ao final de um valor binário, sendo empregadas para adicionar ou construir novos valores binários.

Funções para gravar dados binários

O módulo binário fornece as seguintes funções para gravar dados em valores binários. Todas as funções podem ser executadas em qualquer um dos três tipos válidos que este pacote pode converter entre si.

O parâmetro value é obrigatório para todas as funções, enquanto offset é opcional, sendo válido e tendo o padrão 0.

Funções write:

  • binary::write_int8(value; offset) grava um int8 em um valor binário.
  • binary::write_int16_be(value; offset) grava um int16 em um valor binário em ordem big-endian.
  • binary::write_int16_le(value; offset) grava um int16 em um valor binário em ordem little-endian.
  • binary::write_int32_be(value; offset) grava um int32 em um valor binário em ordem big-endian.
  • binary::write_int32_le(value; offset) grava um int32 em um valor binário em ordem little-endian.
  • binary::write_int64_be(value; offset) grava um int64 em um valor binário em ordem big-endian.
  • binary::write_int64_le(value; offset) grava um int64 em um valor binário em ordem little-endian.
  • binary::write_uint8(value; offset) grava um uint8 em um valor binário.
  • binary::write_uint16_be(value; offset) grava um uint16 em um valor binário em ordem big-endian.
  • binary::write_uint16_le(value; offset) grava um uint16 em um valor binário em ordem little-endian.
  • binary::write_uint32_be(value; offset) grava um uint32 em um valor binário em ordem big-endian.
  • binary::write_uint32_le(value; offset) grava um uint32 em um valor binário em ordem little-endian.
  • binary::write_uint64_be(value; offset) grava um uint64 em um valor binário em ordem big-endian.
  • binary::write_uint64_le(value; offset) grava um uint64 em um valor binário em ordem little-endian.
  • binary::write_float_be(value; offset) grava um float em um valor binário em ordem big-endian.
  • binary::write_float_le(value; offset) grava um float em um valor binário em ordem little-endian.
  • binary::write_double_be(value; offset) grava um double em um valor binário em ordem big-endian.
  • binary::write_double_le(value; offset) grava um double em um valor binário em ordem little-endian.
  • binary::write_bool(value; offset; bit) grava um booliano em um único byte em um valor binário, definindo o bit fornecido como o valor booliano.
  • binary::write_bit(value; offset; bit) grava um único bit em um valor binário, deixando outros bits no byte como está.
  • binary::write_utf8(value; offset) grava uma cadeia de caracteres UTF-8 em um valor binário.

Funções append:

  • binary::append_int8(value) acrescenta um int8 a um valor binário.
  • binary::append_int16_be(value) acrescenta um int16 a um valor binário em ordem big-endian.
  • binary::append_int16_le(value) acrescenta um int16 a um valor binário em ordem little-endian.
  • binary::append_int32_be(value) acrescenta um int32 a um valor binário em ordem big-endian.
  • binary::append_int32_le(value) acrescenta um int32 a um valor binário em ordem little-endian.
  • binary::append_int64_be(value) acrescenta um int64 a um valor binário em ordem big-endian.
  • binary::append_int64_le(value) acrescenta um int64 a um valor binário em ordem little-endian.
  • binary::append_uint8(value) acrescenta um uint8 a um valor binário.
  • binary::append_uint16_be(value) acrescenta um uint16 a um valor binário em ordem big-endian.
  • binary::append_uint16_le(value) acrescenta um uint16 a um valor binário em ordem little-endian.
  • binary::append_uint32_be(value) acrescenta um uint32 a um valor binário em ordem big-endian.
  • binary::append_uint32_le(value) acrescenta um uint32 a um valor binário em ordem little-endian.
  • binary::append_uint64_be(value) acrescenta um uint64 a um valor binário em ordem big-endian.
  • binary::append_uint64_le(value) acrescenta um uint64 a um valor binário em ordem little-endian.
  • binary::append_float_be(value) acrescenta um float a um valor binário em ordem big-endian.
  • binary::append_float_le(value) acrescenta um float a um valor binário em ordem little-endian.
  • binary::append_double_be(value) acrescenta um double a um valor binário em ordem big-endian.
  • binary::append_double_le(value) acrescenta um double em um valor binário em ordem little-endian.
  • binary::append_bool(value; bit) acrescenta um booliano em um único byte em um valor binário, definindo o bit fornecido como o valor booliano.
  • binary::append_utf8(value) acrescenta uma cadeia de caracteres UTF-8 em um valor binário.

Exemplos binários

Esta seção apresenta alguns casos de uso comuns para trabalhar com dados binários. Os exemplos usam uma mensagem de entrada comum.

Suponha que você tenha uma mensagem com um conteúdo que é um formato binário personalizado que contém várias seções. Cada seção contém os seguintes dados em ordem de bytes big-endian:

  • Um uint32 que contém o comprimento do nome do campo em bytes.
  • Uma cadeia de caracteres utf-8 que contém o nome do campo cujo comprimento é especificado pelo uint32 anterior.
  • Um double que armazena o valor do campo.

Para este exemplo, você tem três destas seções, contendo:

  • (uint32) 11

  • (utf-8) temperatura

  • (double) 86,0

  • (uint32) 8

  • (utf-8) umidade

  • (double) 51,290

  • (uint32) 8

  • (utf-8) pressão

  • (double) 346,23

Esses dados se parecem com isso quando impressos dentro da seção payload de uma mensagem:

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

Observação

A representação base64::<string> dos dados binários é apenas para facilitar a diferenciação de outros tipos e não representa o formato físico dos dados durante o processamento.

Extrair valores diretamente

Se você conhece a estrutura exata da mensagem, poderá recuperar os valores dela usando os deslocamentos apropriados.

Use a seguinte expressão jq para extrair os valores:

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

O JSON a seguir mostra a saída da expressão jq anterior:

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

Extrair valores dinamicamente

Se a mensagem puder conter quaisquer campos em qualquer ordem, você poderá extrair dinamicamente a mensagem completa:

Use a seguinte expressão jq para extrair os 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

O JSON a seguir mostra a saída da expressão jq anterior:

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

Editar valores diretamente

Este exemplo mostra como editar um dos valores. De modo similar à extração, é mais fácil se você souber onde está o valor que deseja editar nos dados binários. Este exemplo mostra como converter a temperatura de Fahrenheit para Celsius.

Use a seguinte expressão jq para converter a temperatura de Fahrenheit para Celsius na mensagem binária:

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

O JSON a seguir mostra a saída da expressão jq anterior:

"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"

Se você aplicar a lógica de extração mostrada anteriormente, obterá a seguinte saída:

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

Editar valores dinamicamente

Este exemplo mostra como obter o mesmo resultado do exemplo anterior, localizando dinamicamente o valor desejado na consulta.

Use a seguinte expressão jq para converter a temperatura de Fahrenheit para Celsius na mensagem binária, localizando dinamicamente os dados a serem editados:

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
)

O JSON a seguir mostra a saída da expressão jq anterior:

"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"

Inserir novos valores

Adicione novos valores usando as funções append (acrescentar) do pacote. Por exemplo, para adicionar um campo windSpeed com um valor 31.678 à entrada, preservando o formato binário de entrada, use a seguinte expressão 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)

O JSON a seguir mostra a saída da expressão jq anterior:

"base64:AAAAC3RlbXBlcmF0dXJlQFWAAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFIAAAACXdpbmRTcGVlZEA/rZFocrAh"