データ プロセッサの jq 式とは

重要

Azure Arc によって有効にされる Azure IoT Operations Preview は、 現在プレビュー段階です。 運用環境ではこのプレビュー ソフトウェアを使わないでください。

一般公開リリースが利用可能になった場合は、新しい Azure IoT Operations インストールをデプロイする必要があります。プレビュー インストールをアップグレードすることはできません。

ベータ版、プレビュー版、または一般提供としてまだリリースされていない Azure の機能に適用される法律条項については、「Microsoft Azure プレビューの追加使用条件」を参照してください。

"jq 式" は、データ パイプライン メッセージの計算と操作を実行するための強力な手段です。 このガイドでは、データ パイプラインでの一般的な計算と処理のニーズに対する言語パターンとアプローチについて説明します。

ヒント

jq プレイグラウンドを使い、入力と式の例をエディターに貼り付けることで、このガイドの例を試すことができます。

言語の基礎

言語としての jq に慣れていない場合は、この言語の基礎セクションで背景情報を提供します。

関数型プログラミング

jq 言語は関数型プログラミング言語です。 すべての操作は入力を受け取り、出力を生成します。 複数の操作を組み合わせて複雑なロジックを実行します。 たとえば、次のような入力を考えてください。

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

次に示すのは、取得するパスを指定する簡単な jq 式です。

.payload.temperature

このパスは、入力として値を受け取り、別の値を出力する操作です。 この例では、出力値は 25 です。

jq で複雑な連結された操作を使うときは、重要な考慮事項がいくつかあります。

  • 操作から返されないデータは、式の残りの部分で使用できなくなります。 この制約を回避する方法はいくつかありますが、一般に、後続の式で必要になるデータについて考え、それを前の操作で捨て去らないようにする必要があります。
  • 式は、実行する一連の計算ではなく、一連のデータ変換と考えるのが最も適しています。 代入のような操作であっても、1 つのフィールドが変更される全体的な値の変換にすぎません。

すべてが式である

ほとんどの非関数型言語では、次の 2 種類の操作が区別されます。

  • "式" は、別の式のコンテキストで使用できる値を生成します。
  • "ステートメント" は、入力と出力を直接操作するのではなく、何らかの形の副作用を作ります。

いくつか例外はありますが、jq ではすべてのものが式です。 ループ、if/else 操作、さらには代入もすべて、システムで副作用を作り出すのではなく、新しい値を生成する式です。 たとえば、次のような入力を考えてください。

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

humidity フィールドを 63 に変更したい場合は、代入式を使用できます。

.humidity = 63

この式は入力オブジェクトを変更するように見えますが、jq では、新しい humidity 値を持つオブジェクトが新しく生成されます。

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

この違いは些細なことのように思えますが、後で説明するように、| を使うことで、この操作の結果をさらに他の操作と連結できることを意味します。

パイプを使用して操作を連結する: |

jq で計算やデータ操作を行うときは、複数の操作の組み合わせが必要になることがよくあります。 操作を連結するには、それらの間に | を配置します。 たとえば、メッセージ内のデータ配列の長さを計算するには、次のようにします。

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

最初に、配列を保持しているメッセージの部分を分離します。

.data

この式によって、配列だけが得られます。

[5, 2, 4, 1]

次に、length 操作を使って、その配列の長さを計算します。

length

この式からは、次のような答えが得られます。

4

ステップの間の区切り記号として | 演算子を使い、単一の jq 式のようにすると、計算は次のようになります。

.data | length

複雑な変換を実行しようとして、問題にぴったり一致する例がここにない場合は、| 記号に関するこのガイドのように複数の解法を連結することで、問題を解決できる可能性があります。

関数の入力と引数

jq での主要な操作の 1 つは、関数の呼び出しです。 jq には多くの形式の関数があり、受け取ることができる入力の数が異なります。 関数の入力には、次の 2 つの形式があります。

  • データ コンテキスト - jq によって関数に自動的にフィードされるデータ。 通常、最も新しい | 記号の前の操作によって生成されたデータ。
  • 関数の引数 - 関数の動作を構成するためにユーザーが指定するその他の式と値。

多くの関数には引数がなく、jq が提供するデータ コンテキストを使ってすべての処理が行われます。 length 関数の例を次に示します。

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

前の例で、length への入力は、| 記号の左側で作成された配列です。 この関数で入力配列の長さを計算するために、他の入力は必要ありません。 引数を持たない関数は、その名前だけを使って呼び出します。 つまり、length() ではなく、length を使います。

一部の関数は、データ コンテキストと 1 つの引数を組み合わせて、動作が定義されます。 たとえば、map 関数は次のようになります。

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

前の例で、map への入力は、| 記号の左辺で作成された数値の配列です。 map 関数は、入力配列の各要素に対して式を実行します。 map への引数として式を指定します。この例の場合は . * 2 で、配列の各エントリの値に 2 を乗算して、配列 [2, 4, 6] を出力します。 map 関数で必要な任意の内部動作を構成できます。

関数の中には、複数の引数を受け取るものがあります。 これらの関数は、単一引数の関数と同じように動作し、; 記号を使って引数を区切ります。 たとえば、sub 関数は次のようになります。

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

前の例の sub 関数は、入力データ コンテキストとしての "Hello World" と、2 つの引数を受け取ります。

  • 文字列内で検索する正規表現。
  • 一致した部分文字列を置き換える文字列。 複数の引数を区切るには ; 記号を使います。 引数が 3 つ以上ある関数にも、同じパターンが適用されます。

重要

, ではなく ; を引数の区切り記号として使うことに注意してください。

オブジェクトを操作する

jq には、オブジェクトからのデータの抽出、オブジェクトの操作や作成のための方法が多くあります。 以下のセクションでは、最も一般的なパターンについて説明します。

オブジェクトから値を抽出する

キーを取得するには、通常、パス式を使います。 さらに複雑な結果を得るため、この操作を他の操作と組み合わせることがよくあります。

簡単にオブジェクトからデータを取得できます。 非オブジェクト構造から多数のデータを取得する必要がある場合の一般的なパターンは、非オブジェクト構造をオブジェクトに変換することです。 次のような入力があるものとします。

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

湿度の値を取得するには、次の式を使います。

.payload.values.humidity

この式からは、次のような出力が生成されます。

67

オブジェクト内のキーを変更する

オブジェクトのキーの名前やキー自体を変更するには、with_entries 関数を使用できます。 この関数は、オブジェクトのキーと値のペアを操作する式を受け取り、式の結果を含む新しいオブジェクトを返します。

次に示すのは、ダウンストリームのスキーマに合わせて temp フィールドの名前を temperature に変更する方法の例です。 次のような入力があるものとします。

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

temp フィールドの名前を temperature に変更するには、次の式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • with_entries(<expression>) は、複数の操作をまとめて実行するための短縮形です。 それによって、次の操作が行われます。
    • 入力としてオブジェクトを受け取り、キーと値の各ペアを、{"key": <key>, "value": <value>} という構造のエントリに変換します。
    • オブジェクトから生成された各エントリに対して <expression> を実行し、そのエントリの入力値を、<expression> の実行結果に置き換えます。
    • キーと値のペアのキーとして key を使い、キーの値として value を使って、変換されたエントリのセットをオブジェクトに戻します。
  • if .key == "temp" then .key = "temperature" else . end は、エントリのキーに対して条件付きロジックを実行します。 キーが temp の場合は、それを temperature に変換し、値は変更しないでそのままにします。 キーが temp ではない場合は、式から . を返すことで、エントリを変更せずそのままにします。

次の JSON は、前の式からの出力を示したものです。

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

オブジェクトを配列に変換する

オブジェクトはデータにアクセスするのに便利ですが、メッセージを分割したり、情報を動的に結合したりするときは、多くの場合、配列の方が便利です。 オブジェクトを配列に変換するには、to_entries を使います。

次に示すのは、payload フィールドを配列に変換する方法の例です。 次のような入力があるものとします。

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

payload フィールドを配列に変換するには、次の式を使います。

.payload | to_entries

次の JSON は、前の jq 式からの出力です。

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

ヒント

この例は、配列を抽出し、メッセージ内の他の情報を破棄しているだけです。 メッセージ全体を保持しつつ、.payload の構造を配列に変換するには、代わりに .payload |= to_entries を使います。

オブジェクトを作成する

オブジェクトを構築するには、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
  }
}

次の jq 式を使うと、新しい構造を持つオブジェクトが作成されます。

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

前の jq 式では、次のことが行われています。

  • {payload: {<fields>}} によって作成されるオブジェクトに含まれる payload という名前のリテラル フィールドは、それ自体がリテラル オブジェクトであり、さらに多くのフィールドを含みます。 この方法は、オブジェクトを構築するための最も基本的な方法です。
  • humidity: .payload.Payload["dtmi:com:prod1:slicer3345:humidity"].Value, では、動的に計算された値を含む静的なキー名が作成されます。 オブジェクト構築内のすべての式のデータ コンテキストは、オブジェクト構築式への完全な入力です (この例の場合は完全なメッセージ)。
  • (.payload.DataSetWriterName): "active" は、動的なオブジェクト キーの例です。 この例では、.payload.DataSetWriterName の値は静的な値にマップされています。 オブジェクトを作成するときは、静的と動的なキーと値を任意の組み合わせで使います。

次の JSON は、前の jq 式からの出力を示したものです。

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

オブジェクトにフィールドを追加する

フィールドを追加してデータに関するさらに多くのコンテキストを提供することで、オブジェクトを拡張できます。 存在しないフィールドへの代入を使います。

次に示すのは、ペイロードに averageVelocity フィールドを追加する方法の例です。 次のような入力があるものとします。

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

次の jq 式を使うと、ペイロードに averageVelocity フィールドが追加されます。

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

|= 記号を使う他の例とは異なり、この例では標準的な代入である = を使います。 したがって、右辺の式の範囲は左辺のフィールドには適用されません。 ペイロードの他のフィールドにアクセスできるように、この方法が必要です。

次の JSON は、前の jq 式からの出力を示したものです。

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

オブジェクトにフィールドを条件付きで追加する

条件付きロジックと、オブジェクトにフィールドを追加する構文を組み合わせると、存在しないフィールドへの既定値の追加といったシナリオが可能になります。

次に示すのは、単位がない温度測定値に単位を追加する方法の例です。 既定の単位は摂氏です。 次のような入力があるものとします。

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

次の jq 式を使うと、単位がない温度測定値に単位が追加されます。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • map(<expression>) は、配列内の各エントリに対して <expression> を実行し、入力値を <expression> によって生成されたものに置き換えます。
  • .unit //= "celsius" では、特別な代入 //= が使われています。 この代入では、(=) と代替演算子 (//) を組み合わせて、.unit の既存の値が false または null でない場合は、既存の値をそれ自体に代入します。 .unit が false または null の場合は、.unit の値として "celsius" が代入され、必要な場合は .unit が作成されます。

次の JSON は、前の jq 式からの出力を示したものです。

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

オブジェクトからフィールドを削除する

オブジェクトから不要なフィールドを削除するには、del 関数を使います。

次に示す例では、計算の残りの部分には関係がないため、timestamp フィールドを削除しています。 次のような入力があるものとします。

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

次の jq 式を使うと、timestamp フィールドが削除されます。

del(.payload.timestamp)

次の JSON は、前の jq 式からの出力を示したものです。

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

配列の操作

配列は、jq での反復とメッセージ分割の中核となる構成要素です。 次の例では、配列を操作する方法を示します。

配列から値を抽出する

メッセージが異なると、データが配置されている配列内のインデックスが異なる場合があるため、配列の検査はオブジェクトより困難です。 したがって、配列から値を抽出するには、多くの場合、配列内で必要なデータを検索する必要があります。

次の例では、配列からいくつかの値を抽出して、関心のあるデータを保持する新しいオブジェクトを作成する方法を示します。 次のような入力があるものとします。

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

次の jq 式を使うと、配列から timestamptemperaturehumiditypressure の各値が抽出されて、新しいオブジェクトが作成されます。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • {timestamp, <other-fields>}timestamp: .timestamp の短縮形であり、元のオブジェクトの同じ名前のフィールドを使って、オブジェクトにタイムスタンプをフィールドとして追加します。 <other-fields> は、オブジェクトにさらにフィールドを追加します。
  • temperature: <expression>, humidity: <expression>, pressure: <expression> は、3 つの式の結果に基づいて、結果のオブジェクトに温度、湿度、圧力を設定します。
  • .data | <expression> は、値の計算のスコープをペイロードの data 配列に設定し、配列に対して <expression> を実行します。
  • map(<expression>)[0]?.value は、いくつかのことを行います。
    • map(<expression>) は、配列内の各要素に対して <expression> を実行し、各要素に対してその式を実行した結果を返します。
    • [0] は、結果の配列の最初の要素を抽出します。
    • ? は、前の値が null の場合でも、パス セグメントをさらに連結できるようにします。 前の値が null の場合、後続のパスは失敗するのではなくやはり null を返します。
    • .value は、結果から value フィールドを抽出します。
  • select(.field == "dtmi:com:prod1:slicer3345:temperature") は、入力に対して select() の内部のブール式を実行します。 結果が true の場合、入力は渡されます。 結果が false の場合、入力は破棄されます。 map(select(<expression>)) は、配列内の要素をフィルター処理するために使われる一般的な組み合わせです。

次の JSON は、前の jq 式からの出力を示したものです。

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

配列のエントリを変更する

map() 式を使って配列内のエントリを変更します。 これらの式を使って、配列の各要素を変更します。

次に示すのは、配列内の各エントリのタイムスタンプを UNIX のミリ秒の時刻から RFC3339 文字列に変換する方法の例です。 次のような入力があるものとします。

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

次の jq 式を使うと、配列内の各エントリのタイムスタンプが UNIX のミリ秒の時刻から RFC3339 文字列に変換されます。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • map(<expression>) は、配列内の各要素に対して <expression> を実行し、<expression> を実行した出力でそれぞれを置き換えます。
  • .timestamp |= <expression> は、<expression> の実行に基づく新しい値にタイムスタンプを設定します。ここで、<expression> のデータ コンテキストは .timestamp の値です。
  • (. / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ")) は、ミリ秒単位の時間を秒単位に変換し、時間文字列フォーマッタを使って ISO 8601 のタイムスタンプを生成します。

次の JSON は、前の jq 式からの出力を示したものです。

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

配列をオブジェクトに変換する

目的のスキーマへのアクセスまたは準拠が容易になるように、配列をオブジェクトに再構築するには、from_entries を使います。 次のような入力があるものとします。

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

配列をオブジェクトに変換するには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • map({key: <expression>, value: <expression>}) は、配列の各要素を、{"key": <data>, "value": <data>} の形式のオブジェクトに変換します。これは、from_entries で必要な構造です。
  • {key: .field, value: {timestamp, value}} は、配列エントリからオブジェクトを作成し、field をキーにマッピングして、timestampvalue を保持するオブジェクトである値を作成します。 {timestamp, value}{timestamp: .timestamp, value: .value} の短縮形です。
  • <expression> | from_entries は、配列の値になっている <expression> をオブジェクトに変換し、各配列エントリの key フィールドをオブジェクトのキーにマッピングして、各配列エントリの value フィールドをそのキーの値にマッピングします。

次の JSON は、前の jq 式からの出力を示したものです。

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

配列の作成

配列リテラルの作成は、オブジェクト リテラルの作成と似ています。 配列リテラルの jq 構文は、JSON と JavaScript に似ています。

次の例では、後で処理するためにいくつかの値を単純な配列に抽出する方法を示します。

次のような入力があるものとします。

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

次の jq 式を使うと、temperaturehumiditypressure フィールドの値から配列が作成されます。

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

次の JSON は、前の jq 式からの出力を示したものです。

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

配列にエントリを追加する

+ 演算子と配列およびその新しいエントリを使うことで、配列の先頭または末尾にエントリを追加できます。 += 演算子を使うと、末尾に新しいエントリが追加されて配列が自動的に更新されるので、この操作が簡単になります。 次のような入力があるものとします。

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

次の jq 式を使うと、lineStatus 値配列の末尾に値 1241 が追加されます。

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

次の JSON は、前の jq 式からの出力を示したものです。

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

配列からエントリを削除する

オブジェクトの場合と同じ方法で配列からエントリを削除するには、del 関数を使います。 次のような入力があるものとします。

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

次の jq 式を使うと、lineStatus 値配列から 2 番目のエントリが削除されます。

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

次の JSON は、前の jq 式からの出力を示したものです。

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

重複する配列エントリを削除する

配列の要素が重複している場合は、重複するエントリを削除できます。 ほとんどのプログラミング言語では、サイド ルックアップ変数を使って重複を削除できます。 jq での最善の方法は、必要な処理方法に合わせてデータを整理してから、操作を実行した後、目的の形式に変換して戻すことです。

次に示すのは、いくつかの値を含むメッセージを取得してから、各値の最新の測定値のみになるようにフィルター処理する方法の例です。 次のような入力があるものとします。

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

次の jq 式を使うと、入力がフィルター処理されて、各値の最新の測定値のみになります。

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

ヒント

各名前の最新の値を取得しなくてもよい場合は、.payload |= unique_by(.name) のように式を簡略化できます

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • 入力として配列を与えられた group_by(.name) は、各要素の .name の値に基づいて要素を部分配列に配置する。 各サブ配列には、.name の値が同じである元の配列のすべての要素が含まれます。
  • map(<expression>) は、group_by によって生成された配列の配列を受け取り、各サブ配列に対して <expression> を実行します。
  • sort_by(.timestamp)[-1] は、各サブ配列から、関心のある要素を抽出します。
    • sort_by(.timestamp) は、現在のサブ配列の .timestamp フィールドの値の昇順に要素を並べ替えます。
    • [-1] は、並べ替えられたサブ配列から最後の要素を取得します。これは、各名前で時刻が最新のエントリです。

次の JSON は、前の jq 式からの出力を示したものです。

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

配列の要素間で値を計算する

配列の複数の要素の値を組み合わせて、要素全体の平均などの値を計算できます。

この例では、同じ名前を共有するエントリの最大のタイムスタンプと平均値を取得することで、配列を減らす方法を示します。 次のような入力があるものとします。

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

同じ名前を共有するエントリの最大のタイムスタンプと平均値を取得するには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • group_by(.name) は、入力として配列を受け取り、各要素の .name の値に基づいて要素をサブ配列に配置します。 各サブ配列には、.name の値が同じである元の配列のすべての要素が含まれます。
  • map(<expression>) は、group_by によって生成された配列の配列を受け取り、各サブ配列に対して <expression> を実行します。
  • {name: <expression>, value: <expression>, timestamp: <expression>} は、namevaluetimestamp フィールドを使って、入力サブ配列からオブジェクトを構築します。 各 <expression> は、関連付けられたキーに必要な値を生成します。
  • .[0].name は、サブ配列から最初の要素を取得して、そこから name フィールドを抽出します。 サブ配列内の要素はすべて同じ名前であるため、取得する必要があるのは最初の要素のみです。
  • map(.value) | (add / length) は、各サブ配列の value の平均を計算します:
    • map(.value) は、サブ配列を各エントリの value フィールドの配列に変換し、この場合は数値の配列を返します。
    • add は、数値の配列の合計を計算する jq の組み込み関数です。
    • length は、配列の数または長さを計算する jq の組み込み関数です。
    • add / length は、合計をカウントで除算して平均を決定します。
  • map(.timestamp) | max は、各サブ配列の timestamp の最大値を検索します:
    • map(.timestamp) は、サブ配列を各エントリの timestamp フィールドの配列に変換し、この場合は数値の配列を返します。
    • max は、配列内の最大値を検索する jq の組み込み関数です。

次の JSON は、前の jq 式からの出力を示したものです。

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

文字列の処理

jq には、文字列の操作と構築のためのユーティリティがいくつかあります。 次に、一般的なユース ケースの例をいくつか示します。

文字列を分割する

文字列に共通の文字で区切られた複数の情報が含まれている場合は、split() 関数を使って個々の部分を抽出できます。

次に示すのは、トピックの文字列を分割し、トピックの特定のセグメントを返す方法の例です。 パーティション キーの式を使う場合、この手法が役に立つことがよくあります。 次のような入力があるものとします。

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

次の jq 式を使うと、区切り記号として / を使ってトピックの文字列が分割されて、トピックの特定のセグメントが返されます。

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

前の jq 式では、次のことが行われています。

  • .topic | <expression> は、ルート オブジェクトから topic キーを選択し、含まれるデータに対して <expression> を実行します。
  • split("/") は、文字列で / 文字が見つかるたびに文字列を分割することで、トピック文字列を配列に分割します。 この場合は、["assets", "slicer-3345", "tags", "rpm"] が生成されます。
  • [1] は、前のステップから渡された配列のインデックス 1 にある要素を取得します (この場合は slicer-3345)。

次の JSON は、前の jq 式からの出力を示したものです。

"slicer-3345"

文字列を動的に構築する

jq では、文字列内で \(<expression>) という構文で文字列テンプレートを使って文字列を構築できます。 文字列を動的に構築するには、これらのテンプレートを使います。

次の例では、文字列テンプレートを使って、オブジェクト内の各キーにプレフィックスを追加する方法を示します。 次のような入力があるものとします。

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

次の jq 式を使うと、オブジェクト内の各キーにプレフィックスが追加されます。

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

前の jq 式では、次のことが行われています。

  • with_entries(<expression>) は、{key: <key>, value: <value>} という構造を持つキーと値のペアの配列にオブジェクトを変換し、キーと値の各ペアに対して <expression> を実行して、ペアをオブジェクトに戻します。
  • .key |= <expression> は、キーと値のペア オブジェクトの .key の値を、<expression> の結果に更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、完全なキーと値のペア オブジェクトではなく、.key の値に設定されます。
  • "current-\(.)" は、"current-" で始まる文字列を生成した後、現在のデータ コンテキスト . の値 (この場合はキーの値) を挿入します。 文字列内の \(<expression>) 構文は、文字列のその部分を <expression> の実行結果に置き換えることを示します。

次の JSON は、前の jq 式からの出力を示したものです。

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

正規表現を操作する

jq では標準の正規表現がサポートされています。 正規表現を使って、文字列内のパターンを抽出、置換、チェックできます。 jq の一般的な正規表現関数としては、test()match()split()capture()sub()gsub() があります。

正規表現を使用して値を抽出する

文字列の分割を使って文字列から値を抽出できない場合は、正規表現を使って必要な値を抽出してみてください。

次に示すのは、正規表現について検査してから、別の形式に置き換えることで、オブジェクトのキーを正規化する方法の例です。 次のような入力があるものとします。

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

オブジェクトのキーを正規化するには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • with_entries(<expression>) は、{key: <key>, value: <value>} という構造を持つキーと値のペアの配列にオブジェクトを変換し、キーと値の各ペアに対して <expression> を実行して、ペアをオブジェクトに戻します。
  • .key |= <expression> は、キーと値のペア オブジェクトの .key の値を、<expression> の結果に更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、完全なキーと値のペア オブジェクトではなく、.key の値に設定されます。
  • if test("^dtmi:.*:(?<tag>[^:]+)$") then capture("^dtmi:.*:(?<tag>[^:]+)$").tag else . end は、正規表現に基づいてキーをチェックして更新します。
    • test("^dtmi:.*:(?<tag>[^:]+)$") は、入力データ コンテキスト (この場合はキー) を正規表現 ^dtmi:.*:(?<tag>[^:]+)$ に対してチェックします。 正規表現が一致する場合は true を返します。 そうでない場合は false を返します。
    • capture("^dtmi:.*:(?<tag>[^:]+)$").tag は、入力データ コンテキスト (この場合はキー) に対して正規表現 ^dtmi:.*:(?<tag>[^:]+)$ を実行し、出力としてのオブジェクトに、(?<tag>...) で示される正規表現からのキャプチャ グループを配置します。 その後、この式はそのオブジェクトから .tag を抽出し、正規表現によって抽出された情報を返します。
    • else 分岐の . では、式はデータを変更せずに渡します。

次の JSON は、前の jq 式からの出力を示したものです。

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

メッセージを分割する

jq 言語の便利な機能は、1 つの入力から複数の出力を生成できることです。 この機能を使うと、メッセージを複数の個別のメッセージに分割してパイプラインで処理できます。 この手法の鍵となるのは .[] であり、これは配列を個別の値に分割します。 次の例では、この構文を使うシナリオをいくつか示します。

動的な出力の数

通常、メッセージを複数の出力に分割する場合、必要な出力の数はメッセージの構造に依存します。 [] 構文を使うと、このタイプの分割を実行できます。

たとえば、タグのリストを含む 1 つのメッセージがあり、タグを個別のメッセージに配置したいものとします。 次のような入力があるものとします。

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

1 つのメッセージを複数のメッセージに分割するには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload.Payload = (.payload.Payload | to_entries[]) は、1 つのメッセージを複数のメッセージに分割します。
    • .payload.Payload = <expression> は、<expression> の実行結果を .payload.Payload に代入します。 通常、このケースでは |= を使って <expression> のコンテキストのスコープを .payload.Payload まで下げますが、|= はメッセージの分割をサポートしていないため、代わりに = を使います。
    • (.payload.Payload | <expression>) は、代入式の右辺のスコープを .payload.Payload まで下げて、<expression> がメッセージの正しい部分に対して実行されるようにします。
    • to_entries[] は 2 つの操作であり、to_entries | .[] の短縮形です。
      • to_entries は、スキーマ {"key": <key>, "value": <value>} を使って、オブジェクトをキーと値のペアの配列に変換します。 この情報は、異なるメッセージに分けたいものです。
      • [] は、メッセージの分割を実行します。 配列内の各エントリは、jq の個別の値になります。 .payload.Payload への代入が行われると、各個別値ではメッセージ全体のコピーが行われ、.payload.Payload には代入の右辺で生成された対応する値が設定されます。
  • .payload |= <expression> は、.payload の値を <expression> の実行結果に置き換えます。 この時点のクエリでは、前の操作での分割の結果として、単一の値ではなく、値の "ストリーム" が処理されています。 したがって、代入は、全体について 1 回だけ実行されるのではなく、前の操作によって生成されたメッセージごとに 1 回実行されます。
  • {DataSetWriterName, SequenceNumber, ...} は、.payload の値である新しいオブジェクトを作成します。 DataSetWriterNameSequenceNumber は変更されないため、DataSetWriterName: .DataSetWriterNameSequenceNumber: .SequenceNumber を記述するのではなく、短縮構文を使用できます。
  • Tag: .Payload.key, は、内部の Payload から元のオブジェクト キーを抽出して、親オブジェクトまでレベルを上げます。 クエリで前に行われた to_entries 操作によって、key フィールドが作成されました。
  • Value: .Payload.value.valueTimestamp: .Payload.value.sourceTimestamp は、内部ペイロードから同様のデータ抽出を実行します。 今回は、元のキーと値のペアの値からです。 その結果であるフラットなペイロード オブジェクトを使って、さらに処理を行うことができます。

次に示す JSON は、前の jq 式からの出力です。 各出力は、パイプラインの後続の処理ステージのためのスタンドアロン メッセージになります。

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

一定の数の出力

メッセージの構造に基づく動的な数の出力ではなく、一定の数の出力にメッセージを分割するには、[] の代わりに , 演算子を使います。

次の例では、既存のフィールド名に基づいてデータを 2 つのメッセージに分割する方法を示します。 次のような入力があるものとします。

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

1 つのメッセージを 2 つのメッセージに分割するには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload = ({<fields>},{<fields>}) は、メッセージ内の .payload に 2 つのオブジェクト リテラルを代入します。 コンマで区切られたオブジェクトにより、2 つの個別の値が生成されて、.payload に代入されます。これにより、メッセージ全体が 2 つのメッセージに分割されます。 新しい各メッセージでは、.payload にいずれかの値が設定されます。
  • {field: "temperature", minimum: .payload.minTemperature, maximum: .payload.maxTemperature} は、オブジェクトのフィールドに、リテラル文字列と、オブジェクトからフェッチされた他のデータを設定する、リテラル オブジェクト コンストラクターです。

次に示す JSON は、前の jq 式からの出力です。 各出力は、さらに処理を行うステージのためのスタンドアロン メッセージになります。

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

数学的操作

jq では、一般的な数学的操作がサポートされています。 一部の操作は、+- などの演算子で行われます。 その他の操作は、sinexp などの関数です。

算術

jq では、加算 (+)、減算 (-)、乗算 (*)、除算 (/)、剰余 (%) の 5 つの一般的な算術演算がサポートされています。 jq の多くの機能とは異なり、これらの操作は、| 区切り記号を使わないで完全な数式を単一の式に記述できる中置操作です。

次に示すのは、温度を華氏から摂氏に変換し、UNIX のミリ秒のタイムスタンプから現在の秒の値を抽出する方法の例です。 次のような入力があるものとします。

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

次の jq 式を使うと、温度が華氏から摂氏に変換されて、UNIX のミリ秒のタイムスタンプから現在の秒の値が抽出されます。

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

前の jq 式では、次のことが行われています。

  • .payload.temperatureC = (5/9) * (.payload.temperatureF - 32) は、temperatureF を華氏から摂氏に変換するように設定された新しい temperatureC フィールドをペイロードに作成します。
  • .payload.seconds = (.payload.timestamp / 1000) % 60 は、UNIX のミリ秒の時間を受け取って秒に変換した後、剰余計算を使って現在の分の秒数を抽出します。

次の JSON は、前の jq 式からの出力を示したものです。

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

数学関数

jq には、数学的操作を実行するいくつかの関数が含まれています。 詳細な一覧については、jq のマニュアルをご覧ください。

次の例では、質量と速度のフィールドから運動エネルギーを計算する方法を示します。 次のような入力があるものとします。

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

質量と速度のフィールドから運動エネルギーを計算するには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload.energy = <expression> は、<expression> の実行結果である新しい energy フィールドをペイロードに作成します。
  • (0.5 * (.userProperties | from_entries).mass * pow(.payload.velocity; 2) | round) はエネルギーの数式です。
    • (.userProperties | from_entries).mass は、userProperties リストから mass エントリを抽出します。 データは keyvalue を含むオブジェクトとして既に設定されているため、from_entries はそれをオブジェクトに直接変換できます。 この式は、結果のオブジェクトから mass キーを取得して、その値を返します。
    • pow(.payload.velocity; 2) は、ペイロードから速度を抽出して、それを 2 乗します。
    • <expression> | round は、結果の精度が誤って高くなるのを防ぐため、結果を最も近い整数に丸めます。

次の JSON は、前の jq 式からの出力を示したものです。

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

ブール論理

データ処理パイプラインでは、jq を使ってメッセージをフィルター処理することがよくあります。 通常、フィルター処理ではブール式と演算子が使われます。 さらに、ブール ロジックは、変換やいっそう高度なフィルター処理のユース ケースで制御フローを実行するのに役立ちます。

次に示すのは、jq のブール式で使われる最も一般的ないくつかの機能の例です:

基本的なブール演算子と条件演算子

jq には、基本的なブール論理演算子 andornot が用意されています。 andor 演算子は、中置演算子です。 not は、フィルターとして呼び出す関数です (例: <expression> | not)。

jq には、条件演算子 ><==!=>=<= があります。 これらの演算子は中置演算子です。

次の例は、条件を使って基本的なブール ロジックを実行する方法を示したものです。 次のような入力があるものとします。

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

次のような条件を調べるには、次の jq 式を使います。

  • 温度が、30 度より高く、60 度以下である。
  • 湿度が 80 未満であり、サイトが Redmond である。
.payload
| ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond"
| not

前の jq 式では、次のことが行われています。

  • .payload | <expression> は、<expression> のスコープを .payload の内容に設定します。 この構文により、式の残りの部分では詳細さが低下します。
  • ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond" は、温度が 30 度より高く 60 度以下または湿度が 80 未満で、かつ、サイトが Redmond である場合にのみ、true を返します。
  • <expression> | not は、前の式の結果を受け取り、論理 NOT を適用します。この例では、true の結果が false に反転されます。

次の JSON は、前の jq 式からの出力を示したものです。

false

オブジェクト キーの存在を調べる

メッセージの内容ではなく、メッセージの構造を調べるフィルターを作成できます。 たとえば、特定のキーがオブジェクトに存在するかどうかを確認できます。 このチェックを行うには、has 関数または null に対するチェックを使います。 次の例では、両方の方法が示されています。 次のような入力があるものとします。

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

次の jq 式を使うと、ペイロードに temperature フィールドがあるかどうか、site フィールドが null ではないかどうか、およびその他のチェックが行われます。

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

前の jq 式では、次のことが行われています。

  • .payload | <expression> は、<expression> のデータ コンテキストのスコープを .payload の値に設定して、<expression> の詳細さを低下させます。
  • hasTemperature: has("temperature"), や他の同様の式は、入力オブジェクトで has 関数がどのように動作するかを示します。 この関数は、キーが存在する場合にのみ true を返します。 site の値が null であるにもかかわらず、hasSite は true です。
  • temperatureNotNull: (.temperature != null), や他の同様の式は、!= null のチェックで has と同様のチェックが行われる方法を示します。 .<key> 構文を使ってアクセスされた場合、またはキーは存在するが値が null である場合は、オブジェクトに存在しないキーは null です。 一方のキーが存在し、もう一方のキーが存在しない場合でも、siteNotNullmissingNotNull はどちらも false になります。
  • hasNested: (has("nested") and (.nested | has("inner"))) は、親オブジェクトが存在しない可能性がある has を含む入れ子になったオブジェクトに対してチェックを実行します。 結果は、エラーを回避するために、各レベルでのチェックのカスケードです。
  • nestedNotNull: (.nested?.inner != null) は、!= null? を使って入れ子になったオブジェクトに対して同じチェックを実行し、存在しない可能性があるフィールドでパスを連結できるようにします。 この方法では、存在するかどうかわからないほど深く入れ子になった連結に対して、よりわかりやすい構文が生成されますが、null キーの値を存在しない値と区別することはできません。

次の JSON は、前の jq 式からの出力を示したものです。

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

配列エントリの存在を調べる

配列内にエントリが存在するかどうかを調べるには、any 関数を使います。 次のような入力があるものとします。

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

userProperties 配列に、キーが mass のエントリがあり、キーが missing のエントリがないかどうかを調べるには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .userProperties | <expression> は、<expression> のデータ コンテキストのスコープを userProperties の値に設定して、<expression> の残りの部分の詳細さを低下させます。
  • any(.key == "mass")userProperties 配列の各要素に対して .key == "mass" 式を実行し、配列の少なくとも 1 つの要素に対して式が true に評価された場合は true を返します。
  • (any(.key == "missing") | not)userProperties 配列の各要素に対して .key == "missing" を実行して、いずれかの要素が true と評価された場合は true を返してから、結果全体を | not で否定します。

次の JSON は、前の jq 式からの出力を示したものです。

true

制御フロー

jq の制御フローは、ほとんどの形式の制御フローが直接データドリブンであるため、ほとんどの言語とは異なります。 従来の関数型プログラミング セマンティクスでの if/else 式はサポートされていますが、map 関数と reduce 関数の組み合わせを使うことで、ほとんどのループ構造を実現できます。

次に示すのは、jq での一般的な制御フロー シナリオの例です。

If-else ステートメント

jq は、if <test-expression> then <true-expression> else <false-expression> end を使って条件をサポートします。 途中に elif <test-expression> then <true-expression> を追加することで、さらに多くのケースを挿入できます。 jq と他の多くの言語の主な違いは、各 then および else 式によって jq 式全体の後続の演算で使用される結果を生成する点です。

次に示すのは、if ステートメントを使って条件付き情報を生成する方法の例です。 次のような入力があるものとします。

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

温度が高、低、または正常かどうかを調べるには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload.status = <expression> は、<expression> の実行結果をペイロードの新しい status フィールドに代入します。
  • if ... end は、中核となる if/elif/else 式です。
    • if .payload.temperature > 80 then "high" は温度を高い値に対してチェックして、true の場合は "high" を返し、それ以外の場合は続行します。
    • elif .payload.temperature < 30 then "low" は低い値に対する温度の 2 回目のチェックを実行して、true の場合は結果を "low" に設定し、それ以外の場合は続行します。
    • else "normal" end は、前のどのチェックも true でなかった場合は "normal" を返し、end で式を閉じます。

次の JSON は、前の jq 式からの出力を示したものです。

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

マップ

jq のような関数型言語で、反復ロジックを実行する最も一般的な方法は、配列を作成し、その配列の値を新しい値にマップすることです。 この手法は、jq では map 関数を使って実現され、このガイドの多くの例に出てきます。 複数の値に対して何らかの操作を実行したい場合は、おそらく map が答えです。

次の例では、map を使って、オブジェクトのキーからプレフィックスを削除する方法を示します。 この解決策は、with_entries を使ってさらに簡潔に記述できますが、ここで示すより詳細なバージョンを見ると、短縮形のアプローチの内部で実際にマッピングが行われている様子がわかります。 次のような入力があるものとします。

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

次の jq 式を使うと、ペイロードのキーから rotor_ プレフィックスが削除されます。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • (to_entries | map(<expression) | from_entries) は、オブジェクト配列変換を実行して、各エントリを <expression> で新しい値にマップします。 このアプローチは、セマンティック的には with_entries(<expression>) と同等です。
    • to_entries は、オブジェクトを配列に変換します。キーと値の各ペアは、{"key": <key>, "value": <value>} という構造の個別のオブジェクトになります。
    • map(<expression>) は、配列内の各要素に対して <expression> を実行し、各式の結果で出力配列を生成します。
    • from_entries は、to_entries の逆です。 この関数は、{"key": <key>, "value": <value>} という構造のオブジェクトの配列を、キーと値のペアにマップされた keyvalue フィールドを含むオブジェクトに変換します。
  • .key |= ltrimstr("rotor_") は、各エントリの .key の値を、ltrimstr("rotor_") の結果で更新します。 |= 構文は、右辺のデータ コンテキストのスコープを .key の値に設定します。 ltrimstr は、文字列に指定されたプレフィックスが存在する場合は、それを削除します。

次の JSON は、前の jq 式からの出力を示したものです。

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

削減

還元は、配列の要素間でループまたは反復操作を実行する主な方法です。 リデュース演算は、アキュムレータと、入力としてアキュムレータと配列の現在の要素を使用する演算で構成されます。 ループの各反復からはアキュムレータの次の値が返され、還元操作の最終出力はアキュムレータの最後の値です。 還元は、他のいくつかの関数型プログラミング言語では "折りたたみ" と呼ばれます。

jq では、reduce 操作を使って還元を実行します。 ほとんどのユース ケースでは、この低レベルの操作は必要なく、代わりに高レベルの関数を使用できますが、reduce は便利な一般的ツールです。

次の例では、データ ポイントに対するメトリックの値の平均変化を計算する方法を示します。 次のような入力があるものとします。

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

データ ポイント全体の値の平均変化を計算するには、次の jq 式を使います。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • reduce .[] as $item (<init>; <expression>) は、次の部分を含む一般的な還元操作のスキャフォールディングです。
    • .[] as $item は常に <expression> as <variable> である必要があり、ほとんどの場合は .[] as $item です。 <expression> は値のストリームを生成し、それぞれが還元操作の反復のために <variable> に保存されます。 反復処理する配列がある場合は、.[] によってそれをストリームに分割します。 この構文は、メッセージの分割に使われる構文と同じですが、reduce 操作ではストリームを使って複数の出力は生成されません。 reduce ではメッセージは分割されません。
    • <init> (このケースでは null) は、リデュース演算で使われるアキュムレータの初期値です。 多くの場合、この値は空またはゼロに設定されます。 この値は最初の反復に対するデータ コンテキストになります (このループ <expression> では .)。
    • <expression> は、還元操作の各反復で実行される操作です。 . を通して現在のアキュムレータ値にアクセスでき、前に宣言した <variable> を通してストリーム内の現在の値にアクセスできます (このケースでは $item)。
  • if . == null then {totalChange: 0, previous: $item.value, count: 0} は、還元の最初の反復を処理するための条件付きです。 それによって、次の反復のためにアキュムレータの構造が設定されます。 この式はエントリ間の差を計算するので、最初のエントリでは、2 番目のリデュースの反復で差を計算するために使用されるデータが設定されます。 totalChangepreviouscount フィールドはループ変数として機能し、各反復で更新されます。
  • .totalChange += (($item.value - .previous) | length) | .previous = $item.value | .count += 1 は、else ケースでの式です。 この式は、アキュムレータ オブジェクト内の各フィールドを、計算に基づいて新しい値に設定します。 totalChange の場合は、現在の値と前の値の差を計算して、その絶対値を取得します。 わかりにくいですが、絶対値を取得するには length 関数を使います。 次の反復で使うため、previous には現在の $itemvalue が設定され、count がインクリメントされます。
  • 還元操作が完了した後、.totalChange / .count によってデータ ポイント全体の平均変化が計算されて、最終的なアキュムレータ値が得られます。

次の JSON は、前の jq 式からの出力を示したものです。

{
  "payload": 5.25
}

ループ

jq でのループは、通常、高度なユース ケースのためのものです。 jq では、すべての操作が値を生成する式であるため、ほとんどの言語でのループのステートメント駆動型セマンティクスは、jq に自然には適合しません。 map または reduce を使ってニーズに対応することを検討してください。

jq で使われる従来のループの種類は主に 2 つです。 他のループの種類も存在しますが、より特殊なユース ケースのためのものです。

  • while は、入力データ コンテキストに対して操作を繰り返し適用し、次の反復で使うためにデータ コンテキストの値を更新して、その値を出力として生成します。 while ループの出力は、ループの各反復によって生成される値を保持する配列です。
  • until は、while と同様に、入力データ コンテキストに対して操作を繰り返し適用し、次の反復で使うためにデータ コンテキストの値を更新します。 while とは異なり、until ループは、ループの最後の反復によって生成された値を出力します。

次の例では、until ループを使って、標準偏差が定義済みの値を下回るまで、測定値のリストから外れ値のデータ ポイントを徐々に取り除く方法を示します。 次のような入力があるものとします。

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

次の jq 式を使うと、標準偏差が 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
  )
)

前の jq 式では、次のことが行われています。

  • def avg: add / length; によって、式で後に平均を計算するために使われる avg という新しい関数が定義されます。 : の右側の式は、avg を使うたびに使われる論理式です。 式 <expression> | avg<expression> | add / length と同じです
  • def stdev: avg as $mean | (map(. - $mean | . * .) | add) / (length - 1) | sqrt; では、stdev という新しい関数が定義されます。 この関数は、StackOverflow でのコミュニティの応答の修正バージョンを使って、配列のサンプル標準偏差を計算します。
  • .payload |= <expression> の最初の 2 つの def は単なる宣言であり、実際の式を開始します。 式は、.payload の入力データ オブジェクトを使って <expression> を実行し、結果を元の .payload に代入します。
  • sort_by(.value) は、配列エントリの配列を value フィールドで並べ替えます。 この解法では、配列内の最大値と最小値を識別して操作する必要があるため、前もってデータを並べ替えておくと、計算が減り、コードが簡単になります。
  • until(<condition>; <expression>) は、<condition> が true を返すまで、入力に対して <expression> を実行します。 <expression><condition> の各実行への入力は、<expression> の前回の実行からの出力です。 <expression> の最後の実行の結果がループから返されます。
  • (map(.value) | stdev) < 2 or length == 0 はループの条件です。
    • map(.value) は、後の計算で使うために、配列を純粋な数値のリストに変換します。
    • (<expression> | stdev) < 2 は、配列の標準偏差を計算し、標準偏差が 2 未満の場合は true を返します。
    • length == 0 は、入力配列の長さを受け取り、それが 0 の場合は true を返します。 すべてのエントリが除去された場合に対する保護として、結果と式全体の or が計算されます。
  • (map(.value) | avg) as $avg は、配列を数値の配列に変換し、その平均を計算してから、結果を $avg 変数に保存します。 ループの反復で平均を何回も再利用するため、このようにすると計算コストが減ります。 変数代入式によって | の後にある次の式のデータ コンテキストが変わることはないので、計算の残りの部分でも完全な配列に引き続きアクセスできます。
  • if <condition> then <expression> else <expression> end は、ループの反復の中核となるロジックです。 <condition> を使って、実行して返す <expression> を決定します。
  • ((.[0].value - $avg) | length) > ((.[-1].value - $avg) | length) は、最大値と最小値を平均値と比較してから、それらの差を比較する if 条件です。
    • (.[0].value - $avg) | length は、最初の配列エントリの value フィールドを取得して、それと全体の平均との差を計算します。 前に並べ替えたので、最初の配列エントリは最も小さい値です。 この値は負である可能性があるため、結果は length にパイプされます。この関数は、入力として数値を指定されると絶対値を返します。
    • (.[-1].value - $avg) | length は、最後の配列エントリに対して同じ操作を実行し、安全性のために絶対値も計算します。 前に並べ替えたので、最後の配列エントリは最も大きい値です。 その後、絶対値は > を使って全体的な条件で比較されます。
  • del(.[0]) は、最初の配列エントリが最大の外れ値であったときに実行される then 式です。 この式により、配列から .[0] の要素が削除されます。 この式は、操作の後で配列に残ったデータを返します。
  • del(.[-1]) は、最後の配列エントリが最大の外れ値であったときに実行される else 式です。 この式は、最後のエントリである .[-1] の要素を配列から削除します。 この式は、操作の後で配列に残ったデータを返します。

次の JSON は、前の jq 式からの出力を示したものです。

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

メッセージを破棄する

フィルター式を記述するときは、false を返すことによって、不要なメッセージを破棄するようシステムに指示できます。 この動作は、jq での条件式の基本的な動作です。 ただし、システムでメッセージを明示的または暗黙的に破棄するときに、メッセージを変換したり、より高度なフィルターを実行したりする場合があります。 以下の例では、この動作を実装する方法を示します。

明示的な破棄

フィルター式でメッセージを明示的に破棄するには、式から false を返します。

jq の組み込み関数 empty を使って、変換内からメッセージを破棄することもできます。

次の例では、メッセージ内のデータ ポイントの平均を計算し、平均が固定値を下回るメッセージを破棄する方法を示します。 変換ステージとフィルター ステージの組み合わせでこの動作を実現することは可能であり、有効です。 状況に最も適したアプローチを使ってください。 次のような入力について考えます。

メッセージ 1

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

メッセージ 2

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

次の jq 式を使うと、データ ポイントの平均が計算されて、平均温度が 30 より低いメッセージ、または平均湿度が 90 より高いメッセージは破棄されます。

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

前の jq 式では、次のことが行われています。

  • .payload |= <expression> では、|= を使って、<expression> の実行結果で .payload の値を更新します。 = ではなく |= を使うと、<expression> のデータ コンテキストが、. ではなく .payload に設定されます。
  • map_values(add / length) は、.payload サブオブジェクト内の各値に対して add / length を実行します。 この式は、値の配列内の要素を合計したものを配列の長さで除算して、平均を計算します。
  • if .payload.temperature > 30 and .payload.humidity < 90 then . else empty end は、結果のメッセージに対して 2 つの条件をチェックします。 最初の入力のように、フィルターが true と評価された場合は、完全なメッセージが出力として生成されます。 2 番目の入力のように、フィルターが false と評価された場合は、empty を返し、結果は値が 0 の空のストリームになります。 この結果により、対応するメッセージは式によって破棄されます。

出力 1

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

出力 2

(出力なし)

エラーを使用した暗黙的な破棄

フィルターと変換のどちらの式でも、jq でエラーを生成させることによってメッセージを暗黙的に破棄できます。 ユーザーが意図的に発生させたエラーと、式への予期しない入力によって発生したエラーを、パイプラインでは区別できないため、このアプローチはベスト プラクティスではありません。 現在、システムは、フィルターまたは変換での実行時エラーを、メッセージを破棄してエラーを記録することで処理します。

このアプローチを使用する一般的なシナリオは、パイプラインへの入力に、構造的に不整合なメッセージが含まれる可能性がある場合です。 次の例では、2 種類のメッセージを受信する方法を示します。1 つはフィルターに対して正常に評価されるもので、もう 1 つは式と構造的に互換性がないものです。 次のような入力について考えます。

メッセージ 1

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

メッセージ 2

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

次の jq 式を使うと、温度が 10 より低く、湿度が 80 より高いメッセージが除外されます。

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

前の例では、式自体は単純な複合ブール式です。 この式は、前に示した最初の入力メッセージの構造で動作するように設計されています。 式が 2 番目のメッセージを受け取ると、.payload の配列構造は式でのオブジェクト アクセスと互換性がなく、エラーが発生します。 温度と湿度の値に基づくフィルターで除外し、互換性のない構造のメッセージを除去したい場合、この式は機能します。 エラーが発生しないもう 1 つの方法は、式の先頭に (.payload | type) == "object" and を追加することです。

出力 1

true

出力 2

(エラー)

時間ユーティリティ

jq では、ネイティブ型としての時間はサポートされていません。 ただし、データ プロセッサによって受け入れおよび出力される一部の形式では、ネイティブ型として時間がサポートされます。 通常、これらの型は Go の time.Time 型を使用して表されます。

jq からこれらの値を操作できるようにするために、データ プロセッサはモジュールに次のような一連の関数を提供します。

  • ネイティブ時刻、ISO 8601 文字列、および Unix の数値タイムスタンプを変換します。
  • これらのすべての種類に対して、さまざまな時間固有の操作を実行します。

time モジュール

特殊な時刻固有の関数はすべて、クエリにインポートできる time モジュールで指定されます。

クエリの先頭で、次の 2 つの方法のいずれかを使ってモジュールをインポートします。

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

1 番目の方法では、モジュール内のすべての関数を 1 つの名前空間の下に配置します (例: time::totime)。 2 番目の方法では、すべてのバイナリ関数を最上位レベルに単純に配置します (例: totime)。 どちらの構文も有効であり、機能的に同等です。

形式と変換

time モジュールは、次の 3 つの時刻形式で動作します:

  • time はネイティブ時刻値です。 これは time モジュールの関数でのみ使用できます。 シリアル化時に time データ型として認識されます。
  • unix は Unix エポックからの秒数を表す Unix の数値タイムスタンプです。 整数または浮動小数点数を指定できます。 シリアル化時に対応する数値型として認識されます。
  • iso は、ISO 8601 文字列形式の時刻表現です。 シリアル化時に文字列として認識されます。

time モジュールには、これらの型を確認および操作するための次の関数が用意されています:

  • time::totime は、3 つの型のいずれかを time に変換します。
  • time::tounix は、3 つの型のいずれかを unix に変換します。
  • time::toiso は、3 つの型のいずれかを iso に変換します。
  • time::istime は、データが time 形式の場合に true を返します。

時間操作

time モジュールは、すべての種類で動作するさまざまな時間固有の操作を提供します。 次の関数は、サポートされている型のいずれかを入力として受け取り、出力と同じ型を返すことができます。 より有効桁数が必要な場合は、整数タイムスタンプが浮動小数点タイムスタンプに変換される可能性があります。

  • time::utc は時刻を UTC に変換します。
  • time::zone(zone) は、指定されたゾーンに時刻を変換します。 zone は ISO 8601 ゾーン文字列です。 たとえば、time::zone("-07") のようにします。
  • time::local は、時刻を現地時刻に変換します。
  • time::offset(duration) は、指定された期間で時間をオフセットします。 duration は Go の期間文字列構文を使用します。 たとえば、time::offset("1m2s") のようにします。
  • time::offset(value;unit) は、指定された期間で時間をオフセットします。 この関数は、数値と単位文字列を使用します。 たとえば、time::offset(2;"s") のようにします。 この関数は、期間が別のプロパティから取得される場合に便利です。

Note

3 つのタイムゾーン関数は、Unix タイムスタンプには意味がありません。

その他のユーティリティ

util モジュールは、jq ランタイムの機能を拡張するユーティリティのコレクションです。

util モジュール

その他のユーティリティはすべて、クエリにインポートできる util モジュールで指定されます。

クエリの先頭で、次の 2 つの方法のいずれかを使ってモジュールをインポートします。

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

1 番目の方法では、モジュール内のすべての関数を 1 つの名前空間の下に配置します (例: util::uuid)。 2 番目の方法では、すべての補助関数を最上位レベルに単純に配置します (例: uuid)。 どちらの構文も有効であり、機能的に同等です。

util モジュールには現在、標準文字列形式で新しいランダム UUID を返す uuid 関数が含まれています。

バイナリ操作

jq は、JSON として表すことができるデータを操作するように設計されています。 ただし、データ プロセッサ パイプラインでは、未解析のバイナリ データを保持する生データ形式もサポートされます。 バイナリ データを操作するために、データ プロセッサに付属するバージョンの jq には、バイナリ データの処理に役立つパッケージが含まれています。 以下を実行できます。

  • バイナリ形式と、base64 や整数配列などの他の形式との間で、双方向に変換します。
  • 組み込み関数を使って、バイナリ メッセージから数値と文字列値を読み取ります。
  • 形式を維持しながら、バイナリ データのポイント編集を実行します。

重要

バイナリ値を変更する jq の組み込み関数や演算子は使用できません。 つまり、+ での連結、バイトに対する map 操作、|=+=//= などのバイナリ値が混在する代入は、行うことができません。 標準的な代入 (==) は使用できます。 サポートされていない操作でバイナリ データを使おうとすると、システムは jqImproperBinaryUsage エラーをスローします。 独自の方法でバイナリ データを操作する必要がある場合は、次のいずれかの関数を使って base64 または整数配列に変換して計算を行った後、バイナリに変換して戻すことを検討してください。

次のセクションでは、データ プロセッサ jq エンジンでのバイナリ のサポートについて説明します。

binary モジュール

データ プロセッサ jq エンジンのすべてのバイナリ サポートは、インポートできる binary モジュールで指定されています。

クエリの先頭で、次の 2 つの方法のいずれかを使ってモジュールをインポートします。

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

1 番目の方法では、モジュール内のすべての関数を 1 つの名前空間の下に配置します (例: binary::tobase64)。 2 番目の方法では、すべてのバイナリ関数を最上位レベルに単純に配置します (例: tobase64)。 どちらの構文も有効であり、機能的に同等です。

形式と変換

binary モジュールでは 3 つの型が使われます。

  • バイナリ - binary モジュール内の関数でのみ直接使用できるバイナリ値。 シリアル化のときは、パイプラインによってバイナリ データ型として認識されます。 生のシリアル化には、この型を使います。
  • 配列 - ユーザーが独自の処理を実行できるように、バイナリを数値の配列に変換する形式。 シリアル化のときは、パイプラインによって整数の配列として認識されます。
  • base64 - バイナリの文字列形式表現。 バイナリと文字列の間で変換を行う場合に最も便利です。 シリアル化のときは、パイプラインによって文字列として認識されます。

jq クエリでは、必要に応じて、3 つの型すべての間で変換を行うことができます。 たとえば、バイナリから配列に変換し、カスタム操作を行った後、最後にバイナリに変換して戻し、型の情報を保持することができます。

関数

これらの型の間のチェックと操作用に、次の関数が用意されています。

  • binary::tobinary は、3 つの型のいずれかをバイナリに変換します。
  • binary::toarray は、3 つの型のいずれかを配列に変換します。
  • binary::tobase64 は、3 つの型のいずれかを base64 に変換します。
  • binary::isbinary は、データがバイナリ形式の場合に true を返します。
  • binary::isarray は、データが配列形式の場合に true を返します。
  • binary::isbase64 は、データが base64 形式の場合に true を返します。

また、モジュールには、バイナリ データをすばやく編集するための binary::edit(f) 関数も用意されています。 この関数は、入力を配列形式に変換し、それに関数を適用した後、結果をバイナリに変換して戻します。

バイナリからデータを抽出する

binary モジュールを使うと、カスタム バイナリ ペイロードのアンパックに使用する値をバイナリ データから抽出できます。 一般に、この機能は、他のバイナリ アンパック ライブラリの機能および同じ名前付けに従います。 次の型をアンパックできます。

  • 整数 (int8、int16、int32、int64、uint8、uint16、uint32、uint64)
  • 浮動小数点数 (float、double)
  • 文字列 (utf8)

このモジュールでは、必要に応じてオフセットとエンディアンを指定することもできます。

バイナリ データを読み取る関数

binary モジュールでは、バイナリ値からデータを抽出するための次の関数が提供されています。 パッケージが変換できる 3 つの型のどれでも、すべての関数を使用できます。

すべての関数パラメーターは省略可能で、offset の既定値は 0length の既定置は残りのデータです。

  • binary::read_int8(offset) は、バイナリ値から int8 を読み取ります。
  • binary::read_int16_be(offset) は、ビッグエンディアン順のバイナリ値から int16 を読み取ります。
  • binary::read_int16_le(offset) は、リトルエンディアン順のバイナリ値から int16 を読み取ります。
  • binary::read_int32_be(offset) は、ビッグエンディアン順のバイナリ値から int32 を読み取ります。
  • binary::read_int32_le(offset) は、リトルエンディアン順のバイナリ値から int32 を読み取ります。
  • binary::read_int64_be(offset) は、ビッグエンディアン順のバイナリ値から int64 を読み取ります。
  • binary::read_int64_le(offset) は、リトルエンディアン順のバイナリ値から int64 を読み取ります。
  • binary::read_uint8(offset) は、バイナリ値から uint8 を読み取ります。
  • binary::read_uint16_be(offset) は、ビッグエンディアン順のバイナリ値から uint16 を読み取ります。
  • binary::read_uint16_le(offset) は、リトルエンディアン順のバイナリ値から uint16 を読み取ります。
  • binary::read_uint32_be(offset) は、ビッグエンディアン順のバイナリ値から uint32 を読み取ります。
  • binary::read_uint32_le(offset) は、リトルエンディアン順のバイナリ値から uint32 を読み取ります。
  • binary::read_uint64_be(offset) は、ビッグエンディアン順のバイナリ値から uint64 を読み取ります。
  • binary::read_uint64_le(offset) は、リトルエンディアン順のバイナリ値から uint64 を読み取ります。
  • binary::read_float_be(offset) は、ビッグエンディアン順のバイナリ値から float を読み取ります。
  • binary::read_float_le(offset) は、リトルエンディアン順のバイナリ値から float を読み取ります。
  • binary::read_double_be(offset) は、ビッグエンディアン順のバイナリ値から double を読み取ります。
  • binary::read_double_le(offset) は、リトルエンディアン順のバイナリ値から double を読み取ります。
  • binary::read_bool(offset; bit) は、バイナリ値から bool を読み取り、指定されたビットの値をチェックします。
  • binary::read_bit(offset; bit) は、指定されたビット インデックスを使って、バイナリ値からビットを読み取ります。
  • binary::read_utf8(offset; length) は、バイナリ値から UTF-8 文字列を読み取ります。

バイナリ データを書き込む

binary モジュールを使うと、バイナリ値をエンコードして書き込むことができます。 この機能を使うと、jq で直接バイナリ ペイロードの作成または編集を行うことができます。 データの書き込みでは、データの抽出と同じデータ型のセットがサポートされ、使用するエンディアンを指定することもできます。

データの書き込みには 2 つの形式があります。

  • write_* 関数は、バイナリ値内のデータをその場で更新し、既存の値の更新または操作に使われます。
  • append_* 関数は、バイナリ値の末尾にデータを追加し、バイナリ値への追加または新しいバイナリ値の作成に使われます。

バイナリ データを書き込む関数

binary モジュールでは、バイナリ値にデータを書き込むための次の関数が提供されています。 このパッケージが変換できる 3 つの有効な型のいずれに対しても、すべての関数を実行できます。

value パラメーターはすべての関数で必須ですが、offset は有効な場所では省略可能であり、既定値は 0 です。

書き込み関数:

  • binary::write_int8(value; offset) は、バイナリ値に int8 を書き込みます。
  • binary::write_int16_be(value; offset) は、ビッグエンディアン順のバイナリ値に int16 を書き込みます。
  • binary::write_int16_le(value; offset) は、リトルエンディアン順のバイナリ値に int16 を書き込みます。
  • binary::write_int32_be(value; offset) は、ビッグエンディアン順のバイナリ値に int32 を書き込みます。
  • binary::write_int32_le(value; offset) は、リトルエンディアン順のバイナリ値に int32 を書き込みます。
  • binary::write_int64_be(value; offset) は、ビッグエンディアン順のバイナリ値に int64 を書き込みます。
  • binary::write_int64_le(value; offset) は、リトルエンディアン順のバイナリ値に int64 を書き込みます。
  • binary::write_uint8(value; offset) は、バイナリ値に uint8 を書き込みます。
  • binary::write_uint16_be(value; offset) は、ビッグエンディアン順のバイナリ値に uint16 を書き込みます。
  • binary::write_uint16_le(value; offset) は、リトルエンディアン順のバイナリ値に uint16 を書き込みます。
  • binary::write_uint32_be(value; offset) は、ビッグエンディアン順のバイナリ値に uint32 を書き込みます。
  • binary::write_uint32_le(value; offset) は、リトルエンディアン順のバイナリ値に uint32 を書き込みます。
  • binary::write_uint64_be(value; offset) は、ビッグエンディアン順のバイナリ値に uint64 を書き込みます。
  • binary::write_uint64_le(value; offset) は、リトルエンディアン順のバイナリ値に uint64 を書き込みます。
  • binary::write_float_be(value; offset) は、ビッグエンディアン順のバイナリ値に float を書き込みます。
  • binary::write_float_le(value; offset) は、リトルエンディアン順のバイナリ値に float を書き込みます。
  • binary::write_double_be(value; offset) は、ビッグエンディアン順のバイナリ値に double を書き込みます。
  • binary::write_double_le(value; offset) は、リトルエンディアン順のバイナリ値に double を書き込みます。
  • binary::write_bool(value; offset; bit) は、バイナリ値の 1 バイトに bool を書き込み、指定されたビットをブール値に設定します。
  • binary::write_bit(value; offset; bit) は、バイナリ値に 1 ビットを書き込み、バイト内の他のビットはそのままにします。
  • binary::write_utf8(value; offset) は、UTF-8 文字列をバイナリ値に書き込みます。

追加関数:

  • binary::append_int8(value) は、バイナリ値に int8 を追加します。
  • binary::append_int16_be(value) は、ビッグエンディアン順のバイナリ値に int16 を追加します。
  • binary::append_int16_le(value) は、リトルエンディアン順のバイナリ値に int16 を追加します。
  • binary::append_int32_be(value) は、ビッグエンディアン順のバイナリ値に int32 を追加します。
  • binary::append_int32_le(value) は、リトルエンディアン順のバイナリ値に int32 を追加します。
  • binary::append_int64_be(value) は、ビッグエンディアン順のバイナリ値に int64 を追加します。
  • binary::append_int64_le(value) は、リトルエンディアン順のバイナリ値に int64 を追加します。
  • binary::append_uint8(value) は、バイナリ値に uint8 を追加します。
  • binary::append_uint16_be(value) は、ビッグエンディアン順のバイナリ値に uint16 を追加します。
  • binary::append_uint16_le(value) は、リトルエンディアン順のバイナリ値に uint16 を追加します。
  • binary::append_uint32_be(value) は、ビッグエンディアン順のバイナリ値に uint32 を追加します。
  • binary::append_uint32_le(value) は、リトルエンディアン順のバイナリ値に uint32 を追加します。
  • binary::append_uint64_be(value) は、ビッグエンディアン順のバイナリ値に uint64 を追加します。
  • binary::append_uint64_le(value) は、リトルエンディアン順のバイナリ値に uint64 を追加します。
  • binary::append_float_be(value) は、ビッグエンディアン順のバイナリ値に float を追加します。
  • binary::append_float_le(value) は、リトルエンディアン順のバイナリ値に float を追加します。
  • binary::append_double_be(value) は、ビッグエンディアン順のバイナリ値に double を追加します。
  • binary::append_double_le(value) は、リトルエンディアン順のバイナリ値に double を追加します。
  • binary::append_bool(value; bit) は、バイナリ値の 1 バイトに bool を追加し、指定されたビットをブール値に設定します。
  • binary::append_utf8(value) は、UTF-8 文字列をバイナリ値に追加します。

バイナリの例

このセクションでは、バイナリ データを使用する一般的なユース ケースをいくつか紹介します。 これらの例では、一般的な入力メッセージを使います。

複数のセクションを含むカスタム バイナリ形式のペイロードが含まれるメッセージがあるとします。 各セクションには、ビッグエンディアン バイト順で次のデータが含まれています。

  • フィールド名の長さをバイト単位で保持する uint32。
  • 前の uint32 で指定された長さのフィールド名を含む utf-8 文字列。
  • フィールドの値を保持する double。

この例では、次のデータを保持する 3 つのセクションがあります。

  • (uint32) 11

  • (utf-8) temperature

  • (double) 86.0

  • (uint32) 8

  • (utf-8) humidity

  • (double) 51.290

  • (uint32) 8

  • (utf-8) pressure

  • (double) 346.23

このデータは、メッセージの payload セクション内に設定されるときは次のようになります。

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

Note

バイナリ データの base64::<string> 表現は、他の型と簡単に区別できるようにするためであり、処理中の物理データ形式を表すわけではありません。

値を直接抽出する

メッセージの正確な構造がわかっている場合は、適切なオフセットを使って、そこから値を取得できます。

値を抽出するには、次の jq 式を使います。

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

次の JSON は、前の jq 式からの出力を示したものです。

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

値を動的に抽出する

メッセージが任意のフィールドを任意の順序で含んでいる可能性がある場合は、メッセージ全体を動的に抽出できます。

値を抽出するには、次の jq 式を使います。

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

次の JSON は、前の jq 式からの出力を示したものです。

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

値を直接編集する

この例では、値の 1 つを編集する方法を示します。 抽出の場合と同様に、編集する値がバイナリ データ内のどこにあるかわかっている場合は、処理が簡単になります。 この例では、温度を華氏から摂氏に変換する方法を示します。

次の jq 式を使うと、バイナリ メッセージの温度が華氏から摂氏に変換されます。

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

次の JSON は、前の jq 式からの出力を示したものです。

"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"

前に示した抽出ロジックを適用すると、次の出力が得られます。

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

値を動的に編集する

この例では、クエリで目的の値の場所を動的に調べることで、前の例と同じ結果を得る方法を示します。

バイナリ メッセージで、編集するデータの場所を動的に見つけて、温度を華氏から摂氏に変換するには、次の jq 式を使います。

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
)

次の JSON は、前の jq 式からの出力を示したものです。

"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"

新しい値を挿入する

パッケージの追加関数を使って新しい値を追加します。 たとえば、受け取ったバイナリ形式を維持しながら、値が 31.678 である windSpeed フィールドを入力に追加するには、次の 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)

次の JSON は、前の jq 式からの出力を示したものです。

"base64:AAAAC3RlbXBlcmF0dXJlQFWAAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFIAAAACXdpbmRTcGVlZEA/rZFocrAh"