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

タグの除去は正規表現による簡易処理なので、&#8217; のような文字参照はそのまま残ります。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 で実際の構造を確かめる。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲