O que são expressões jq no processador de dados?
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.
Você precisará implantar uma nova instalação do Azure IoT Operations quando uma versão geralmente disponível for disponibilizada, você não poderá atualizar uma instalação de visualizaçã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 evalue
como o valor da chave.
- Usa um objeto como entrada e converte cada par chave/valor em uma entrada com estrutura
if .key == "temp" then .key = "temperature" else . end
executa a lógica condicional em relação à chave da entrada. Se a chave fortemp
, então ela será convertida paratemperature
, deixando o valor inalterado. Se a chave não fortemp
, 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 chamadopayload
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 forfalse
ounull
. 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 paratimestamp: .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 matrizdata
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 campovalue
do resultado.
select(.field == "dtmi:com:prod1:slicer3345:temperature")
executa a expressão booliana dentro deselect()
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 parafrom_entries
.{key: .field, value: {timestamp, value}}
cria um objeto a partir da entrada de uma matriz, mapeandofield
para a chave e criando um valor que é um objeto contendotimestamp
evalue
.{timestamp, value}
é uma abreviação para{timestamp: .timestamp, value: .value}
.<expression> | from_entries
converte uma matriz com valor<expression>
em um objeto, mapeando o campokey
de cada entrada da matriz para a chave de objeto e o campovalue
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 porgroup_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 porgroup_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 camposname
,value
etimestamp
. Cada<expression>
produz o valor desejado para a chave associada..[0].name
recupera o primeiro elemento da submatriz e extrai o camponame
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édiavalue
de cada submatriz:map(.value)
converte a submatriz em uma matriz do campovalue
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áximotimestamp
de cada submatriz:map(.timestamp)
converte a submatriz em uma matriz dos campostimestamp
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 chavetopic
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 casoslicer-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 branchelse
, 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 deto_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
eSequenceNumber
não são alterados, para que você possa usar a sintaxe abreviada em vez de escreverDataSetWriterName: .DataSetWriterName
eSequenceNumber: .SequenceNumber
.Tag: .Payload.key,
extrai a chave original do objeto internoPayload
e a eleva para o objeto pai. A operaçãoto_entries
anterior na consulta criou o campokey
.Value: .Payload.value.value
eTimestamp: .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 campotemperatureC
no conteúdo que é definido como a conversão detemperatureF
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 campoenergy
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 entradamass
da listauserProperties
. Os dados já estão configurados como objetos comkey
evalue
, entãofrom_entries
pode convertê-los diretamente em um objeto. A expressão recupera a chavemass
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 detrue
parafalse
.
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çãohas
se comporta com um objeto de entrada. A função retorna verdadeiro somente se a chave estiver presente.hasSite
é verdadeiro apesar do valor desite
sernull
.temperatureNotNull: (.temperature != null),
e outras expressões semelhantes demonstram como a verificação!= null
executa uma verificação semelhante ahas
. Uma chave inexistente em um objeto énull
se acessada usando a sintaxe.<key>
, ou a chave existe, mas tem um valor denull
. Ambas expressõessiteNotNull
emissingNotNull
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 comhas
, 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-chavenull
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 deuserProperties
para tornar o restante da<expression>
menos prolixa.any(.key == "mass")
executa a expressão.key == "mass"
em cada elemento da matrizuserProperties
, 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 matrizuserProperties
, 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 campostatus
no conteúdo.if ... end
é a expressãoif/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 comend
.
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 awith_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 deto_entries
. A função converte uma matriz de objetos com a estrutura{"key": <key>, "value": <value>}
em um objeto com os camposkey
evalue
mapeados em pares chave/valor.
.key |= ltrimstr("rotor_")
atualiza o valor de.key
em cada entrada com o resultado deltrimstr("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çãoreduce
não usa o fluxo para gerar várias saídas.reduce
não divide a mensagem.<init>
, nesse casonull
, é 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 campostotalChange
,previous
ecount
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 deelse
. Essa expressão define cada campo no objeto acumulador como um novo valor com base em uma computação. ParatotalChange
, ela encontra a diferença entre os valores atuais e anteriores e obtém o valor absoluto. De forma contra-intuitiva, usa a funçãolength
para obter o valor absoluto.previous
é definido como ovalue
atual do$item
para a próxima iteração a ser usada e ocount
é 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 loopwhile
é uma matriz que contém os valores produzidos por cada iteração do loop.until
, comowhile
, 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 loopuntil
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 chamadaavg
que é usada para calcular médias posteriormente na expressão. A expressão à direita de:
é a expressão lógica usada sempre que você usaavg
. 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 chamadastdev
. 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 primeirasdef
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 campovalue
. 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çãoif
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 campovalue
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 paralength
, 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ãothen
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ãoelse
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)
executaadd / 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 de jq, o processador de dados fornece ao módulo 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 dadostime
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 paratime
.time::tounix
converte qualquer um dos três tipos paraunix
.time::toiso
converte qualquer um dos três tipos paraiso
.time::istime
retorna verdadeiro se os dados estiverem no formato detime
.
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::utc
converte 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::local
converte a hora em hora local.time::offset(duration)
compensa o tempo pela duração fornecida.duration
usa 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 do processador de dados 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 projetado 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 binary
módulo que pode ser importado.
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"