2026年5月27日
2026年5月27日
jqでJSONデータを操作する高度な方法:変数・集計・再帰処理・巨大ファイルの扱い
jq を .foo や .[] で値を取り出すだけでなく、シェル変数の安全な受け渡し、group_by や reduce による集計、walk や paths による再帰的な書き換え、--stream による巨大ファイルの処理まで使えるようになると、WordPress の REST API や WP-CLI の出力をシェルだけで十分に加工できます。この記事では、基本のフィルタは知っている人向けに、実務で効く書き方とつまずきやすい点をまとめます。
前提:バージョンと題材
jq は版によって使える組み込み関数が異なります。jq --version で確認してください(2026年10月現在、主要なディストリビューションでは 1.7 系以降が入っていることが多いものの、古い LTS では 1.6 のままの場合があります)。この記事の例は、特記しない限り 1.6 でも動く書き方にしています。
題材には、WP-CLI と WordPress REST API の JSON を使います。
wp plugin list --fields=name,status,update,version --format=json > plugins.json
curl -sS 'https://example.com/wp-json/wp/v2/posts?per_page=100&_fields=id,author,date,slug,title' > posts.json
REST API の _fields パラメーターで必要な項目だけを返させると、転送量も jq 側の処理も軽くなります。
シェル変数は --arg / --argjson で渡す
jq のプログラムに "$var" を文字列として埋め込むと、値に " や \ が含まれたときに構文が壊れ、意図しないフィルタが実行されることもあります。値は引数として渡します。
slug='hello-world'
jq --arg slug "$slug" '.[] | select(.slug == $slug) | .id' posts.json
author_id=3
jq --argjson author "$author_id" '[.[] | select(.author == $author)] | length' posts.json
# 環境変数は $ENV から読める
SITE_URL='https://example.com' jq -n '$ENV.SITE_URL'
--arg は常に文字列として渡すため、数値の author と比べると一致しません。数値や真偽値、配列として扱いたいときは --argjson を使うか、($id | tonumber) で変換します。「条件は合っているはずなのに何も出ない」ときは、まずこの型の違いを疑ってください。
集計:group_by・reduce・自作関数
投稿者ごとの投稿数を、多い順に並べる例です。
jq 'group_by(.author)
| map({author: .[0].author, count: length})
| sort_by(-.count)' posts.json
group_by は内部でソートしてからまとめるため、件数が多いとメモリを使います。単純な件数集計なら reduce でオブジェクトに足し込む方が軽く済みます。
jq 'reduce .[] as $p ({}; .[$p.author | tostring] += 1)' posts.json
オブジェクトのキーは文字列でなければならないので tostring で変換しています。null + 1 は 1 になるため、初期値の用意は不要です。
同じ集計を何度も書くなら、def で関数にできます。~/.jq というファイルを置くと、その中の定義が毎回自動で読み込まれます。
# ~/.jq
def tally(f): reduce .[] as $x ({}; .[$x | f | tostring] += 1);
jq 'tally(.author)' posts.json
jq 'tally(.date[0:7])' posts.json # 月別の投稿数
再帰的な検索と書き換え:paths・walk
構造の深い JSON から「どこにその値があるか」を探すには paths を使います。次の例は、http:// で始まる文字列がどのパスにあるかを一覧にします。HTTPS 移行後の取り残し確認などに便利です。
jq -r 'paths(strings | test("^http://")) | map(tostring) | join(".")' export.json
見つけたパスは getpath・setpath・delpaths で読み書きできます。構造全体を一括で書き換えるなら walk が使えます。次は、すべての階層から値が null の項目を取り除く例です。
jq 'walk(if type == "object" then with_entries(select(.value != null)) else . end)' data.json
with_entries はオブジェクトを {key, value} の配列に分解し、処理後に組み立て直す関数です。キー名の一括変更(.key |= ascii_downcase など)にも使えます。
欠けた値とエラーの扱い
API のレスポンスは、項目が欠けていたり型が想定と違ったりすることがあります。
.meta.views // 0は、左辺がnullかfalseのときに右辺を使います。falseも置き換わる点に注意してください。.foo?やtry ... catch ...で、型が合わずにエラーになる要素を飛ばせます。try (.title.rendered | ascii_downcase) catch "N/A"のように使います。-eを付けると、最後の出力がfalseかnullのときに終了コードが 0 以外になります。シェルのifと組み合わせられます。
if curl -sS "https://example.com/wp-json/" | jq -e '.namespaces | index("wp/v2")' > /dev/null; then
echo "REST API is available"
fi
複数ファイルの突き合わせ
アップデート前後の wp plugin list を比べて、バージョンが変わったプラグインだけを出す例です。--slurpfile はファイルの中身を配列に包んで変数に入れるので、$before[0] のように取り出します。
jq -rn --slurpfile before before.json --slurpfile after after.json '
($before[0] | map({(.name): .version}) | add) as $old
| $after[0][]
| select($old[.name] != .version)
| "\(.name): \($old[.name] // "new") -> \(.version)"'
-n で入力を読まずに始め、必要なデータはすべて変数から取る形です。標準入力から複数の JSON を順に読むときは、-n と inputs を組み合わせます。
巨大なJSONを少ないメモリで処理する
jq は通常、入力全体をメモリに読み込みます。数百MB以上の配列を処理するとメモリ不足になることがあるため、--stream で要素ごとに読みます。
jq -cn --stream 'fromstream(1 | truncate_stream(inputs))' huge.json \
| jq -c 'select(.status == "publish") | {id, slug}'
1つ目の jq がトップレベル配列の要素を1行ずつ出力し、2つ目が通常のフィルタで処理します。処理は遅くなりますが、メモリ使用量はほぼ1要素分で済みます。
また、jq 1.6 以前は大きな整数を浮動小数点数として扱うため、2の53乗を超える ID が丸められることがあります。1.7 以降は変更しない数値の元の表記を保つように改善されていますが、演算すると丸めの対象になるので、巨大な ID は文字列のまま扱う方が安全です。
シェルに戻す・ファイルを書き換える
結果をシェルのループで使うなら、@tsv で区切ってから read で受けます。
while IFS=$'\t' read -r id slug; do
printf 'post %s: %s\n' "$id" "$slug"
done < <(jq -r '.[] | [.id, .slug] | @tsv' posts.json)
@csv・@tsv・@sh・@uri・@base64 は、それぞれの形式に合わせたエスケープを jq 側で行ってくれるので、自分で引用符を組み立てるより安全です。
jq には元ファイルを直接書き換えるオプションがありません。jq ... file > file とすると、読み込む前にファイルが空になります。一時ファイルに書いてから置き換えます。
tmp="$(mktemp)"
jq '.version = "1.2.3"' composer.json > "$tmp" && cat "$tmp" > composer.json
rm -f "$tmp"
mv で置き換えると mktemp が作った権限(多くは 0600)が引き継がれてしまうため、ここでは cat で中身だけを書き戻しています。
まとめ
- シェル変数は
--arg(文字列)と--argjson(JSON値)で渡し、型の違いに注意する。 - 集計は
group_byよりもreduceが軽い場面が多く、繰り返す処理はdefや~/.jqで関数にする。 - 深い構造の検索は
paths、一括の書き換えはwalkとwith_entriesが便利。 //・try・-eで欠けた値や失敗をシェルから扱えるようにする。- 巨大ファイルは
--stream、ファイルの書き換えは一時ファイル経由で行う。