2026年7月27日
2026年7月27日
jqでJSONデータを操作する高度な方法:正規表現・日付・構造の組み替えとJSONの組み立て
jq で値を取り出せるようになったら、次に役立つのは「文字列を正規表現で加工する」「日付で絞り込む」「配列とオブジェクトを組み替える」「シェルの値から安全に JSON を作る」の4つです。この記事では、WordPress の REST API と WP-CLI の出力を題材に、実務でそのまま使える書き方と、エラーが出たときの読み方を説明します。
変数の受け渡し(--arg)、reduce による集計、walk による再帰的な書き換え、--stream による巨大ファイルの処理は、同じテーマの別記事で扱っています。この記事はそれ以外の、文字列・日付・構造変換・JSON 生成に絞っています。
前提:jq の版と題材のデータ
使える関数は jq の版で少しずつ違います。jq --version で確認してください。2026年10月現在、多くのディストリビューションでは 1.7 系以降が入っていますが、古い LTS 版では 1.6 のこともあります。以下の例は、特に断りがない限り 1.6 でも動く書き方です。
題材として、REST API から投稿とカテゴリーを取得しておきます。_fields で必要な項目だけを返させると、余計なデータを扱わずに済みます。
curl -sS 'https://example.com/wp-json/wp/v2/posts?per_page=100&_fields=id,slug,date_gmt,modified_gmt,title,link,categories,content' > posts.json
curl -sS 'https://example.com/wp-json/wp/v2/categories?per_page=100&_fields=id,name,slug' > categories.json
wp plugin list --fields=name,status,version --format=json > plugins.json
REST API は1回に最大100件までしか返しません。全件が必要なら page パラメーターを増やしながら取得し、レスポンスヘッダーの X-WP-TotalPages で終わりを判断します。
正規表現で文字列を加工する
jq の正規表現は Oniguruma というライブラリによるもので、test・capture・scan・sub・gsub などから使えます。jq の文字列の中に書くので、\d は \\d のようにバックスラッシュを二重にする点に注意してください。
一致するかを調べる:test
# スラッグに数字だけの部分を含む投稿(旧URLの名残など)を探す
jq -r '.[] | select(.slug | test("-\\d+$")) | .slug' posts.json
# 第2引数でフラグを指定。"i" は大文字小文字を区別しない
jq -r '.[] | select(.title.rendered | test("wordpress"; "i")) | .id' posts.json
contains("wp") でも部分一致は調べられますが、配列やオブジェクトに対しては「部分集合かどうか」という別の意味になります。文字列の判定には test か startswith・endswith を使うほうが意図がはっきりします。
名前付きで取り出す:capture
capture は名前付きグループ (?<名前>...) に一致した部分をオブジェクトで返します。プラグインのバージョン番号をメジャー・マイナーに分ける例です。
jq -c '.[] | {name} + (.version | capture("^(?<major>\\d+)\\.(?<minor>\\d+)"))' plugins.json
# {"name":"akismet","major":"5","minor":"3"}
取り出した値は文字列です。大小を比べるなら | tonumber で数値にしてから比較してください。文字列のままだと "10" < "9" が真になります。また、一致しなかった要素は何も出力されずに消えるので、件数が合わないときはこの点を疑います。
すべての一致を集める・置換する:scan / gsub
本文中の http:// で始まるリンクを洗い出す例です。
jq -r '.[] | .id as $id
| .content.rendered | [scan("http://[^\"<> ]+")] | unique[]
| "\($id)\t\(.)"' posts.json
gsub は全置換、sub は最初の1か所だけを置換します。置換後の文字列の中では、名前付きグループを \(.名前) で参照できます。
# タイトルから HTML タグを取り除く(簡易的な処理)
jq -r '.[] | .title.rendered | gsub("<[^>]+>"; "")' posts.json
# 2025-03-01 形式を 2025年3月1日 形式にする
jq -r '.[] | .date_gmt[0:10]
| sub("(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})"; "\(.y)年\(.m | tonumber)月\(.d | tonumber)日")' posts.json
タグの除去は正規表現による簡易処理なので、’ のような文字参照はそのまま残ります。HTML を正確に扱う必要があるなら、HTML パーサーを持つ言語に任せたほうが安全です。
日付で絞り込む
jq は日時を「1970年1月1日からの秒数」に変換して比較します。fromdateiso8601 は 2025-03-01T09:00:00Z のように末尾に Z が付いた形式を受け付けます。WordPress の REST API の date_gmt や modified_gmt は Z が付かないため、文字列を足してから変換します。
# 1年以上更新していない投稿を、古い順に一覧にする
jq -r --argjson days 365 '
(now - $days * 86400) as $limit
| map(. + {mtime: (.modified_gmt + "Z" | fromdateiso8601)})
| map(select(.mtime < $limit))
| sort_by(.mtime)[]
| [.id, .modified_gmt[0:10], .title.rendered] | @tsv' posts.json
date や modified(_gmt が付かないもの)はサイトのタイムゾーンでの時刻で、オフセットの情報を持っていません。比較には _gmt 付きの項目を使うと、タイムゾーン設定の違いに惑わされずに済みます。
別の書式の日時は strptime で解析し、mktime で秒数にします。秒数を文字列に戻すときは todate(ISO 8601 形式)か strftime、サーバーのローカル時刻で表示したいなら strflocaltime を使います。
jq -n '"2025/03/01 09:00" | strptime("%Y/%m/%d %H:%M") | mktime | todate'
# "2025-03-01T09:00:00Z"
配列とオブジェクトを組み替える
オブジェクトを回す:to_entries / from_entries
オブジェクトの「キーと値」を順に処理したいときは、to_entries で {key, value} の配列にします。composer.json の依存パッケージを一覧にする例です。
jq -r '.require | to_entries[] | "\(.key)\t\(.value)"' composer.json
逆に、配列からオブジェクトを作るのが from_entries です。WP-CLI のカスタムフィールド一覧を「キー: 値」のオブジェクトに変換してみます。
wp post meta list 123 --format=json \
| jq 'map({key: .meta_key, value: .meta_value}) | from_entries'
ここでつまずきやすいのが、同じキーが複数ある場合です。カスタムフィールドは1つのキーに複数の値を持てるため、from_entries では後の値で上書きされ、前の値が消えます。値をすべて残したいときは、先にキーでまとめます。
wp post meta list 123 --format=json \
| jq 'group_by(.meta_key) | map({key: .[0].meta_key, value: map(.meta_value)}) | from_entries'
値の書き換えだけなら map_values(...)、キー名の変更を含むなら with_entries(...) が短く書けます。
IDで引き当てる:INDEX
投稿の categories は ID の配列です。カテゴリー名に置き換えるには、INDEX で「ID をキーにしたオブジェクト」を作り、引き当てます。
jq --slurpfile cats categories.json '
INDEX($cats[0][]; .id) as $c
| map({id, slug, categories: [.categories[] | $c[tostring].name // "(不明)"]})' posts.json
INDEX が作るオブジェクトのキーは文字列なので、数値の ID は tostring で変換してから引きます。数値のまま $c[12] と書くと、配列の添字とみなされてエラーになります。複数の値のどれかに一致するかを調べる IN(例: select(.status | IN("publish", "draft")))も、INDEX と同じく 1.6 から使えます。
必要な項目だけ残す・消す
REST API のレスポンスには、リンク情報の _links など、使わない項目が付いてきます。del で消すか、必要な項目だけで作り直します。
jq 'map(del(._links))' posts.json
jq 'map({id, slug, title: .title.rendered})' posts.json
# jq 1.7 以降なら、入れ子の構造を保ったまま選べる pick が使える
jq 'map(pick(.id, .title.rendered))' posts.json
シェルの値から安全にJSONを作る
API に送る JSON を、シェルの文字列連結で組み立てるのは避けてください。タイトルに " や改行が含まれると、壊れた JSON になったり、意図しない項目が混ざったりします。jq -n を使えば、エスケープは jq が行います。
次は、本文を HTML ファイルから読み、下書き投稿を作る例です。認証にはアプリケーションパスワード(WordPress 5.6 以降のコア機能)を使い、値はスクリプトに書かず環境変数から読みます。
jq -n --arg title "$TITLE" --rawfile content body.html \
'{title: $title, content: $content, status: "draft"}' \
| curl -sS -X POST 'https://example.com/wp-json/wp/v2/posts' \
-H 'Content-Type: application/json' \
--user "editor:${WP_APP_PASSWORD}" \
--data-binary @-
--rawfile はファイルの中身を1つの文字列として変数に入れます。アプリケーションパスワードは HTTPS のサイトで使うのが前提です。また、--user に渡した値はサーバー上の他のユーザーから ps で見えることがあるため、共有サーバーでは curl の --netrc-file などで渡す方法も検討してください。権限は、下書きの作成に必要な範囲のユーザー(投稿者や編集者)にとどめます。
テキストを JSON に変換する:-R
1行1件のテキストや TSV を JSON に変えるには、-R(入力を生の文字列として読む)を使います。
# 1行1スラッグのリストを配列に
jq -Rn '[inputs | select(length > 0)]' slugs.txt
# TSV(スラッグ<TAB>タイトル)をオブジェクトの配列に
jq -Rn '[inputs | split("\t") | {slug: .[0], title: .[1]}]' list.tsv
カンマ区切りの CSV は、値の中にカンマや引用符が入ることがあるため、split(",") では正しく分けられない場合があります。CSV を読む処理は、mlr などの CSV 対応のツールや Python に任せるほうが安全です。
文字列に埋め込まれたJSON:fromjson / tojson
ログやプラグインの設定値では、JSON が「文字列として」入っていることがあります。fromjson で中身を展開すれば、普通の JSON として扱えます。逆に、JSON を文字列にして埋め込むときは tojson を使います。
jq '.payload |= fromjson' event.json
出力を整える・差分を取る
-Sはキーを並べ替えて出力します。2つの JSON を比べるときは、diff <(jq -S . before.json) <(jq -S . after.json)のように順序をそろえてから比べると、意味のある差分だけが見えます。--tabや--indent 4で字下げを変えられます。-cは1件1行の出力です。-aは ASCII 以外の文字を\uXXXXの形にします。文字コードの扱いが不安な環境に渡すときに使います。-jは-rと同じく文字列を引用符なしで出し、改行を付けません。
エラーの読み方
jq のエラーは、何の型に対して何をしようとしたかを示しています。
Cannot index string with "rendered":.title.renderedの.titleがオブジェクトではなく文字列だったという意味です。WP-CLI の出力と REST API の出力では構造が違うので、まず.[0] | keysや.[0].title | typeで実際の形を確認します。Cannot iterate over null: 存在しないキーに[]を使っています。項目が無い要素がある場合は.categories[]?のように?を付けるか、(.categories // [])[]で空配列として扱います。jq: error: syntax error, unexpected ...: 多くはシェルの引用符の問題です。プログラムは単一引用符で囲み、長くなったら-f filter.jqでファイルに分けます。ファイルなら#でコメントも書けます。
パイプラインの途中で値を確かめたいときは、debug を挟むと、その時点の値が標準エラー出力に表示されます。
jq '.[] | debug | select(.id > 100)' posts.json > /dev/null
まとめ
- 正規表現は
test(判定)、capture(名前付きで取り出す)、scan(すべて集める)、sub・gsub(置換)を使い分ける。バックスラッシュは二重に書く。 - REST API の
_gmt付きの日時は+ "Z" | fromdateiso8601で秒数にしてから比較する。 - オブジェクトの処理は
to_entries・from_entries、ID の引き当てはINDEXを使う。重複するキーの上書きに注意する。 - 送信用の JSON は
jq -n --argで作り、文字列連結で組み立てない。認証情報はスクリプトに書かない。 - エラーは「どの型に何をしたか」を読み、
keys・type・debugで実際の構造を確かめる。