JQ: 実践的JSON処理ガイド

はじめに

JQ(JSON Query Language)は、コマンドラインで動作するJSONプロセッサであり、JSONデータの操作、変換、フィルタリングのための強力なツールです。シェルスクリプトのsedawkがテキスト処理に使われるように、JQはJSON処理のための専用ツールとして設計されています。大規模なデータセットやAPIレスポンス、構成ファイルなど、JSONデータを扱う場面で非常に役立ちます。

この記事では、JQの基本から高度な機能まで、実践的な例を交えて解説します。APIレスポンスの解析からデータ変換、複雑なデータ構造の操作まで、JQを使いこなすための包括的なガイドを提供します。

なぜJQを使うのか

以下のような場面でJQは特に威力を発揮します:

  1. APIレスポンスの解析: REST APIから返されるJSONデータを素早く解析し、必要な情報を抽出
  2. 大規模JSONデータの処理: 何百MBものJSONファイルを効率的に処理
  3. データ変換: JSONからCSVや他の形式へのデータ変換
  4. 構造化データのフィルタリング: 複雑な条件に基づくデータのフィルタリング
  5. データの正規化と変形: ネストされた構造の平坦化や再構成

JQの主な利点は以下の通りです:

  • 高速処理: 大規模なJSONファイルでも効率的に処理
  • 軽量: 依存関係が少なく、ほとんどの環境に容易にインストール可能
  • 強力な表現力: 複雑なデータ変換も簡潔に表現可能
  • ストリーム処理: メモリ効率の良いストリーム処理をサポート
  • パイプライン互換性: Unix哲学に沿ったデザインで、他のコマンドラインツールとシームレスに連携

JQの基本

インストールと基本構文

多くのOSでパッケージマネージャを使用してJQをインストールできます:

# Debian/Ubuntu
apt-get install jq
 
# macOS
brew install jq
 
# Windows (Chocolatey)
choco install jq

JQの基本的な使用方法は以下の通りです:

jq [オプション] 'フィルター式' [JSONファイル]

例えば、簡単なJSONファイルからデータを抽出する場合:

echo '{"name": "田中太郎", "age": 30, "email": "tanaka@example.com"}' | jq '.name'

これは以下を出力します:

"田中太郎"

基本フィルター

JQのフィルター式は、JSONデータを入力として受け取り、変換されたJSONデータを出力します。最も基本的なフィルターには以下のものがあります:

  • . - 入力そのもの
  • .フィールド名 - オブジェクトからフィールドを抽出
  • .[] - 配列のすべての要素を展開
  • .[インデックス] - 配列から特定の要素を抽出

例えば:

echo '[{"id": 1, "name": "田中"}, {"id": 2, "name": "佐藤"}]' | jq '.[0]'

結果:

{
  "id": 1,
  "name": "田中"
}

複数のフィールドを抽出する場合は、オブジェクトリテラルを使用します:

echo '{"name": "田中太郎", "age": 30, "email": "tanaka@example.com"}' | jq '{name: .name, email: .email}'

結果:

{
  "name": "田中太郎",
  "email": "tanaka@example.com"
}

パイプラインと関数

JQではパイプライン(|)を使用して、複数のフィルターを連鎖させることができます:

echo '{"users": [{"name": "田中", "active": true}, {"name": "佐藤", "active": false}]}' | jq '.users[] | select(.active == true) | .name'

結果:

"田中"

主要な組み込み関数には以下のものがあります:

  • length - 文字列の長さや配列/オブジェクトの要素数を返す
  • keys - オブジェクトのキーを配列として返す
  • has(key) - オブジェクトが指定されたキーを持つか確認
  • map(filter) - 配列の各要素にフィルターを適用
  • select(condition) - 条件に一致する要素のみを選択

例えば、配列内の各オブジェクトに対して変換を適用する場合:

echo '[{"id": 1, "raw": "A"}, {"id": 2, "raw": "B"}]' | jq 'map({id: .id, value: (.raw | ascii_downcase)})'

結果:

[
  {
    "id": 1,
    "value": "a"
  },
  {
    "id": 2,
    "value": "b"
  }
]

高度なJQ技術

複雑なフィルタリングと変換

複雑なJSONデータを扱う場合、条件付きフィルタリングと高度な変換が必要になります:

条件付きフィルタリング

select関数と論理演算子を使用して、複雑な条件でフィルタリングできます:

echo '[{"name": "田中", "age": 25, "role": "開発者"}, {"name": "佐藤", "age": 40, "role": "マネージャー"}, {"name": "鈴木", "age": 22, "role": "開発者"}]' | jq '.[] | select(.age > 23 and .role == "開発者")'

結果:

{
  "name": "田中",
  "age": 25,
  "role": "開発者"
}

再帰的探索

JQの..演算子を使用して、ネストされた構造を再帰的に探索できます:

echo '{"a": {"b": {"c": 1, "d": 2}}, "e": {"f": 3}}' | jq '.. | objects | select(has("c"))'

結果:

{
  "c": 1,
  "d": 2
}

複雑なデータ変換

複数のステップを組み合わせた複雑な変換の例:

echo '{"orders": [{"id": "A1", "items": [{"product": "鉛筆", "quantity": 2}, {"product": "ノート", "quantity": 1}]}, {"id": "B2", "items": [{"product": "ペン", "quantity": 5}]}]}' | jq '.orders[] | {order_id: .id, total_items: (.items | map(.quantity) | add)}'

結果:

{
  "order_id": "A1",
  "total_items": 3
}
{
  "order_id": "B2",
  "total_items": 5
}

カスタム関数の定義

JQでは、再利用可能なカスタム関数を定義できます:

jq '
  def add_tax($rate):
    . * (1 + $rate);
  
  def format_price:
    . | floor | tostring + "円";
  
  .prices[] | add_tax(0.10) | format_price
' prices.json

この例では、税率を追加するadd_tax関数と金額をフォーマットするformat_price関数を定義しています。

変数の使用

JQでは、計算結果を変数に保存して後で参照できます:

echo '{"first": {"a": 1, "b": 2}, "second": {"c": 3}}' | jq 'def sum(obj): reduce obj[] as $item (0; . + $item); .first as $f | {first_sum: sum($f), second: .second}'

結果:

{
  "first_sum": 3,
  "second": {
    "c": 3
  }
}

代替データの処理

データが存在しない場合の処理も重要です:

echo '{"name": "田中", "details": null}' | jq '.details // "詳細情報なし"'

結果:

"詳細情報なし"

実践的なJQの使用例

APIレスポンスの解析

GitHub APIからリポジトリ情報を取得し、特定の情報を抽出する例:

curl -s 'https://api.github.com/repos/stedolan/jq' | jq '{name: .name, stars: .stargazers_count, owner: .owner.login, description: .description}'

JSON to CSV変換

JQを使用してJSONデータをCSV形式に変換できます:

echo '[{"name": "田中", "age": 30}, {"name": "佐藤", "age": 25}]' | jq -r '.[] | [.name, .age] | @csv'

結果:

"田中",30
"佐藤",25

ヘッダー付きのCSVを生成する場合:

echo '[{"name": "田中", "age": 30}, {"name": "佐藤", "age": 25}]' | jq -r '(["名前", "年齢"] | @csv), (.[] | [.name, .age] | @csv)'

複雑なJSONの平坦化

ネストされたJSONを平坦な構造に変換する例:

echo '{"user": {"name": "田中", "contact": {"email": "tanaka@example.com", "phone": "090-1234-5678"}}}' | jq '{name: .user.name, email: .user.contact.email, phone: .user.contact.phone}'

結果:

{
  "name": "田中",
  "email": "tanaka@example.com",
  "phone": "090-1234-5678"
}

より一般的なアプローチでは、flatten_obj関数を定義できます:

jq '
  def flatten_obj:
    reduce (tostream | select(length == 2) | .[0] |= [join(".")]) as [$path, $value] ({}; . + {($path[0]): $value});
  
  flatten_obj
' nested.json

複数ファイルの処理

複数のJSONファイルを結合して処理する例:

jq -s '.[0] * .[1]' file1.json file2.json

統計計算

配列内の数値に対する統計計算の例:

echo '[{"value": 10}, {"value": 20}, {"value": 30}, {"value": 15}, {"value": 25}]' | jq '{count: length, sum: map(.value) | add, average: (map(.value) | add) / length, min: map(.value) | min, max: map(.value) | max}'

結果:

{
  "count": 5,
  "sum": 100,
  "average": 20,
  "min": 10,
  "max": 30
}

パフォーマンス最適化とベストプラクティス

メモリ効率の良い処理

大規模なJSONファイルを処理する場合、メモリ使用量を最小限に抑えるテクニックがあります:

  1. ストリーム処理の使用: .[]を使用して大きな配列を一度に1要素ずつ処理
  2. 必要なデータのみを抽出: 最初に必要なフィールドのみを選択し、不要なデータを早期に破棄
  3. --streamモードの活用: 非常に大きなファイルには--streamオプションを使用

例えば、ギガバイト単位のJSONファイルから特定のフィールドのみを抽出する場合:

jq --stream 'fromstream(1|truncate_stream(inputs))' huge.json | jq 'select(.type == "important") | {id, name}'

複雑なクエリの分割

複雑なクエリは、より小さく管理しやすい部分に分割するとデバッグや保守が容易になります:

# 悪い例:1つの長く複雑な式
jq '.items[] | select(.category == "A" and .price > 100) | {id: .id, name: .name, discount_price: (.price * 0.9)}'
 
# 良い例:複数のステップに分割
jq '
  # 特定のカテゴリと価格で商品をフィルタリング
  def filter_premium_items:
    select(.category == "A" and .price > 100);
  
  # 割引価格を計算
  def with_discount:
    {id: .id, name: .name, discount_price: (.price * 0.9)};
  
  # メインのパイプライン
  .items[] | filter_premium_items | with_discount
'

エラー処理

JQでのエラー処理は、オプションや代替演算子を使用して実装できます:

# null安全な処理
jq '.user.contacts[]?.email // "メールなし"'
 
# 型チェック
jq 'if type == "array" then .[] else . end'
 
# try-catch風の処理
jq 'try .non_existent catch "存在しないフィールドです"'

モジュール化と再利用

複雑なJQスクリプトを作成する場合、モジュール化して再利用できるようにすることが重要です:

# jqスクリプトファイル(utils.jq)の作成
echo '
# null安全なフィールドアクセス
def get($field):
  .[$field] // null;
 
# 人間が読みやすい日付形式に変換
def format_date:
  split("T")[0];
 
# 小数点以下2桁に丸める
def round_price:
  . * 100 | floor | . / 100;
' > utils.jq
 
# スクリプトを使用
jq -L . 'include "utils"; .transactions[] | {date: (.timestamp | format_date), amount: (.price | round_price)}' data.json

実世界のJQユースケース

ログ解析

JSON形式のログファイルを解析する例:

# エラーレベルのログエントリのみを抽出
jq 'select(.level == "ERROR") | {timestamp, message, service}' logs.json
 
# サービス別にエラー数をカウント
jq 'select(.level == "ERROR") | .service' logs.json | sort | uniq -c
 
# 時間帯別のエラー発生数を集計
jq 'select(.level == "ERROR") | .timestamp | split("T")[1] | split(":")[0]' logs.json | sort | uniq -c

構成管理

複数の設定ファイルをマージする例:

# ベース設定とオーバーライド設定をマージ
jq -s '.[0] * .[1]' base_config.json override_config.json > final_config.json
 
# 環境ごとの設定を生成
jq --arg env "production" '.environments[$env] * .default' config_template.json > config.json

データ検証

JQを使用してJSONデータの検証を行う例:

# 必須フィールドの存在確認
jq 'map(select(has("id") and has("name") and has("email"))) | length == length' users.json
 
# 値の形式検証
jq 'map(select(.email | test("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"))) | length == length' users.json

ETLプロセス

JQはETL(抽出・変換・ロード)プロセスで強力なツールとして活用できます:

# データの抽出と変換
jq '
  # 正規化関数
  def normalize_user:
    {
      user_id: .id,
      full_name: (.first_name + " " + .last_name),
      email: .email | ascii_downcase,
      is_active: (.status == "active"),
      joined_date: (.created_at | split("T")[0])
    };
  
  # メインのETLパイプライン
  .users[] | normalize_user
' raw_data.json > transformed_data.json

JQとその他のツールの比較

JQ vs. Python

JQ:

  • 利点: 軽量、高速、専用構文でJSON操作に最適化
  • 欠点: 複雑なロジックは表現が難しい場合がある

Python:

  • 利点: 汎用性が高く、より複雑なロジックを実装可能
  • 欠点: 実行環境の依存関係、単純なタスクでもオーバーヘッドがある

例えば、同じデータ処理をJQとPythonで比較すると:

# JQによる処理
echo '[{"name": "田中", "age": 30}, {"name": "佐藤", "age": 25}]' | jq 'map(select(.age > 27))'
 
# Pythonによる同等の処理
echo '[{"name": "田中", "age": 30}, {"name": "佐藤", "age": 25}]' | python -c 'import sys, json; data = json.load(sys.stdin); print(json.dumps([user for user in data if user["age"] > 27]))'

JQ vs. jc

JQ:

  • 利点: JSONの処理に特化、豊富な変換機能
  • 欠点: 非JSON形式のデータ取り込みには追加のステップが必要

jc:

  • 利点: 多様なコマンド出力をJSONに変換する機能
  • 欠点: JQ自体の機能はないため、JQと組み合わせて使用することが多い

組み合わせの例:

# コマンド出力をJSONに変換し、JQで処理
ls -l | jc --ls | jq '.[] | select(.size > 1000000) | {name: .filename, size_mb: (.size / 1048576)}'

JQ vs. gron

JQ:

  • 利点: 強力な変換機能、複雑なデータ構造の操作に優れる
  • 欠点: 構文が複雑になりうる

gron:

  • 利点: JSONを行指向のテキストに変換し、grepなどの標準ツールで処理しやすくする
  • 欠点: 高度なデータ変換機能はJQほど充実していない

使用例の比較:

# JQでの特定フィールドの抽出
curl -s https://api.github.com/repos/stedolan/jq | jq '.owner.login'
 
# gronとgrepを使用した同等の操作
curl -s https://api.github.com/repos/stedolan/jq | gron | grep 'owner.login' | gron --ungron

JQの高度なトピック

条件付き式と制御構造

JQは条件付き式や制御構造もサポートしています:

# if-then-else
jq 'if .score > 80 then "優" elif .score > 60 then "良" else "可" end'
 
# try-catch
jq 'try .non_existent catch "エラー: フィールドが存在しません"'
 
# 再帰的な処理
jq '
  def factorial(n):
    if n <= 1 then 1 else n * factorial(n-1) end;
  
  .number | factorial
'

生成器とストリーミング

JQはデータストリームの処理に適しています:

# range生成器を使用した数列の生成
jq -n 'range(1; 6) | . * .'
 
# foreachを使用した入力ストリームの処理
jq 'foreach inputs as $item ([]; . + [$item.value]; .)' file1.json file2.json

動的キーアクセス

変数を使用して動的にオブジェクトのキーにアクセスする例:

echo '{"user": {"name": "田中", "email": "tanaka@example.com"}}' | jq --arg field "email" '.user[$field]'

結果:

"tanaka@example.com"

JSONスキーマ検証

JQを使用して、JSONスキーマに基づいたデータ検証を実装できます:

jq '
  def validate_user:
    # IDが有効な形式か検証
    (has("id") and (.id | type == "string") and (.id | test("^U[0-9]{6}$"))) and
    # 名前が存在し、空でないか検証
    (has("name") and (.name | type == "string") and (.name | length > 0)) and
    # 年齢が有効な範囲か検証
    (has("age") and (.age | type == "number") and (.age >= 18 and .age < 120)) and
    # メールが有効な形式か検証
    (has("email") and (.email | type == "string") and (.email | test("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")));
  
  map(select(validate_user) | .id) as $valid_ids |
  {
    valid_users: $valid_ids,
    valid_count: ($valid_ids | length),
    total_count: length,
    invalid_count: (length - ($valid_ids | length))
  }
' users.json

JQのトラブルシューティング

一般的なエラーとその解決法

「無効なJSONテキスト」エラー

parse error: Invalid numeric literal at line 1, column 4

解決策:

  • 入力が有効なJSONであることを確認
  • --raw-input(-R)オプションを使用して、プレーンテキストをJSONとして解釈

「オブジェクトが必要」エラー

jq: error (at <stdin>:1): Cannot index array with string "name"

解決策:

  • データ構造を.jq -n inputで調査
  • 正しいデータ型に対して適切な操作を使用

「期待しないトークン」エラー

jq: error: syntax error, unexpected INVALID_CHARACTER, expecting $end (Unix shell quoting issues?) at <top-level>

解決策:

  • シェルの引用符を正しく使用(特に'"
  • 複雑なフィルターは別ファイルに保存して-fで読み込む

デバッグテクニック

段階的なパイプラインの検証

複雑なパイプラインは、各ステップの出力を確認することでデバッグできます:

# 問題のあるパイプライン
echo '{"users": [{"name": "田中", "age": 30}, {"name": "佐藤", "age": 25}]}' | jq '.users[] | select(.age > 27) | .nonexistent'
 
# 各ステップの出力を確認
echo '{"users": [{"name": "田中", "age": 30}, {"name": "佐藤", "age": 25}]}' | jq '.users[]'
echo '{"users": [{"name": "田中", "age": 30}, {"name": "佐藤", "age": 25}]}' | jq '.users[] | select(.age > 27)'

デバッグ用の中間出力

debug関数を定義して、パイプラインの途中経過を表示できます:

jq '
  # デバッグ用のヘルパー関数
  def debug(label):
    debug | label_values(label);
  
  .users[] | debug("After users[]") | select(.age > 27) | debug("After select")
' data.json

JQのエコシステムと拡張

有用なJQ関連ツール

JQを補完するツールやエコシステムについて:

  1. gojq: GoによるJQ互換の実装で、パフォーマンスが向上
  2. jid: インタラクティブなJQフィルター開発ツール
  3. fx: JSONをターミナルで対話的に探索するツール
  4. jless: JSONファイルのターミナルベースビューア

実用的なJQモジュール

RosettaCodeなどのソースから提供されるJQモジュールの紹介:

  • assert.jq: アサーションをサポートするモジュール
  • bitwise.jq: ビット操作を行うモジュール
  • date.jq: 日付処理のためのモジュール
  • math.jq: 数学関数を提供するモジュール

まとめ

JQは、JSON処理のための強力で柔軟なツールです。基本的なフィルタリングから複雑なデータ変換まで、多様なJSONデータ処理ニーズに対応できます。この記事で紹介した技術を活用することで、コマンドラインでのJSON処理作業を効率化し、生産性を向上させることができるでしょう。

JQのさらなる学習には、以下のリソースが役立ちます:

参考文献

  1. Stedolan. (2023). “jq Manual.” https://stedolan.github.io/jq/manual/
  2. Fiatjaf. (2022). “Awesome JQ.” https://github.com/fiatjaf/awesome-jq
  3. Wader. (2023). “fq - Tool, language and decoders for working with binary data.” https://github.com/wader/fq
  4. Kellyjonbrazil. (2023). “jc - JSON CLI output utility.” https://github.com/kellyjonbrazil/jc
  5. Tomnomnom. (2022). “gron - Make JSON greppable.” https://github.com/tomnomnom/gron