現場で使えるシェルスクリプト入門16:jqコマンドによるJSONパースとデータ抽出の基本

シェルスクリプト IT

前回の記事(入門15)では、スペースやカンマで区切られたデータから特定の列を抜き出したり、計算・集計を行ったりできる便利な「awk」コマンドの使い方について解説しました。

今回は、現代のシステム開発やWeb API連携において避けて通ることができない「JSONフォーマット」のデータを、シェルスクリプトで安全かつ簡単に処理するための必須コマンド「jq」の基本的な使い方を解説します。

JSONは改行の位置が自由で、データの階層(ネスト)が深くなるため、これまで学んだ sedawk などの行指向ツールだけで処理しようとすると、パースエラーなどの深刻なバグを引き起こす原因になります。jqコマンドをマスターして、API連携やクラウドツールの自動化をワンランク上に引き上げましょう!

jqコマンドとは?

jq(ジェイキュー)は、コマンドラインでJSONデータを整形して表示したり、特定のキーの値を取り出したり、データのフィルタリングや変換を行うことができる、非常に軽量で強力なJSONプロセッサです。

ほとんどの環境(Linux, macOS)でパッケージ管理コマンドを使って導入できます。

  • macOS(Homebrew)の場合brew install jq
  • Ubuntu/Debianの場合sudo apt-get install jq
  • RedHat/CentOSの場合sudo yum install jq

jqコマンドを使用したJSON解析 of サンプルスクリプト

今回は、疑似的な気象データ(JSON形式)を記述したファイルを作成し、そのファイルから jq を使って「都市名」と「現在の気温」を安全に抜き出して変数に格納・表示するシェルスクリプト(read_weather.sh.sh)を作成しました。

#!/bin/bash

# スクリプトを安全に実行するための基本設定
set -euo pipefail

# 1. 読み込むJSONファイルのパスを定義
JSON_FILE="./weather_data.json"

# 2. JSONファイルが存在することを確認
if [ ! -f "$JSON_FILE" ]; then
    echo "エラー: $JSON_FILE が存在しません。" >&2
    exit 1
fi

echo "--- jqによるデータの抽出を開始します ---"

# 3. jqコマンドを使って「都市名(city)」を取り出し、変数に格納する
# 💡 -r オプションで「生の文字列(ダブルクォート無し)」として出力します
CITY_NAME=$(jq -r '.location.city' "$JSON_FILE")

# 4. jqコマンドを使って「現在の気温(temperature)」を取り出し、変数に格納する
TEMP_VALUE=$(jq -r '.current.temperature' "$JSON_FILE")

# 5. 抽出したデータを画面に出力する
echo "都市名: $CITY_NAME"
echo "現在の気温: $TEMP_VALUE ℃"

echo "処理が完了しました。"

weather_data.jsonファイル:

{
  "location": {
    "city": "Tokyo",
    "country": "Japan"
  },
  "current": {
    "weather": "Sunny",
    "temperature": 26.5
  }
}

実行結果:

# ./read_weather.sh.sh 
--- jqによるデータの抽出を開始します ---
都市名: Tokyo
現在の気温: 26.5 ℃
処理が完了しました。

プログラムコードの詳細解説

上記のスクリプトの各ブロックの動きを順に解説します。

  • jqによる都市名の抽出
    jq -r '.location.city' "$JSON_FILE" を実行しています。.location.city というフィルターは、「JSONのルート(一番外側の階層)にある location オブジェクトの中の、city キーの値を取り出す」という指定です。
  • jqによる気温の抽出と変数代入
    同様に .current.temperature フィルターで気温の値を取得し, シェル変数 TEMP_VALUE に代入しています。この時、$( ... ) (コマンド置換)を使用することで、コマンドの出力結果を直接変数へ格納しています。
  • 結果の出力
    変数に正しく格納されたデータを取り出し、分かりやすくフォーマットして画面に表示(echo)して処理を正常終了させています。

jqコマンドの基本的な使い方

シェルスクリプト内で使用する前に、jq コマンド単体での基本的なフィルター指定とデータ抽出のパターンを整理しておきましょう。

JSONデータの整形(ピリオド「.」フィルター)

最もシンプルな使い方は、ルートを意味する . を指定することです。改行やスペースがない1行のJSONデータを整形(インデント表示・カラー化)して見やすく表示します。

$ jq '.' weather_data.json

weather_data.json:

{"location":{"city":"Tokyo","country":"Japan"},"current":{"weather":"Sunny","temperature":26.5}}

実行結果:

{
  "location": {
    "city": "Tokyo",
    "country": "Japan"
  },
  "current": {
    "weather": "Sunny",
    "temperature": 26.5
  }
}

特定キーの値の抽出(「.key」フィルター)

ピリオドの後にキー名を指定することで、オブジェクト内の特定の値にアクセスします。オブジェクトがネスト(入れ子)されている場合は、ピリオドで繋ぎます。

# locationオブジェクトの中のcityキーを抽出
$ jq '.location.city' weather_data.json
"Tokyo"

複数キーの同時抽出と文字列結合

特定のオブジェクト配下にある複数のキー(例: citycountry)を同時に抽出して出力したい場合、いくつかの出力形式に合わせた指定方法があります。

① カンマ(,)で並べて抽出する(改行区切りで別々に出力)

フィルターをカンマで区切って並べることで、それぞれの値を順番に出力できます。-r オプションと合わせると、改行で区切られた生のテキストになります。

# city と country を同時に抽出し、改行区切りで出力する
$ jq -r '.location.city, .location.country' weather_data.json

実行結果:

Tokyo
Japan

② 新しいオブジェクトとして再構築する

{key: value} の形式で指定することで、必要なデータだけを抽出した新しいJSONオブジェクトを再構築できます。キー名が元の名前と同じでよい場合は、{city, country} のように簡略化して書くことも可能です。

# 必要な値だけを持った新しいオブジェクトを作る
$ jq '.location | {city, country}' weather_data.json

実行結果:

{
  "city": "Tokyo",
  "country": "Japan"
}

③ 文字列結合(\( … ))で1行のテキストにする

ダブルクォーテーションで囲んだ文字列の中で \( フィルター ) と記述することで、JSONの値を変数のように文字列内に埋め込むことができます。シェルスクリプト側で読みやすい1行のテキストに整形したい場合に非常に便利です。

# 1行のカスタム文字列にフォーマットして出力する
$ jq -r '.location | "都市: \(.city), 国: \(.country)"' weather_data.json

実行結果:

都市: Tokyo, 国: Japan

④ CSV形式(カンマ区切り)に変換して出力する

jqには、データからCSV形式を自動生成する @csv フォーマッタ関数が用意されています。値を一度 [列1, 列2, ...] のように配列にまとめてから @csv にパイプで渡すことで、適切なダブルクォーテーションで保護されたCSVデータが出力されます。

# locationオブジェクトの city と country をCSV形式で出力する
$ jq -r '.location | [.city, .country] | @csv' weather_data.json

実行結果:

"Tokyo","Japan"

※同様に、タブ区切りのTSVを出力したい場合は @tsv を使用します。

配列データの展開と抽出(「.[]」フィルター)

データが配列(リスト)形式の場合、.[] を指定することで配列の要素を1つずつ展開して処理することができます。特定のインデックス(番目)を指定して取り出すことも可能です。

# 配列の最初の要素を取り出す
$ jq '.[0]' array_data.json

array_data.json:

[
  { "name": "Alice Smith", "age": 30 },
  { "name": "Bob",         "age": 25 },
  { "name": "Carol",       "age": 28 }
]

実行結果:

$ jq '.[0]' array_data.json
{
  "name": "Alice Smith",
  "age": 30
}

配列データ全体をCSV形式で一括出力する応用例

配列を展開(.[])した上で、各オブジェクトから必要な複数項目を取り出しCSVに流すことで、JSONファイル全体を1行ずつのCSVテーブルに変換できます。

# 配列全体から name と age をCSV形式で出力する
$ jq -r '.[] | [.name, .age] | @csv' array_data.json

実行結果:

"Alice Smith",30
"Bob",25
"Carol",28

パイプ処理(「|」)によるフィルターの連結

コマンドラインのパイプ(|)と同様に, jqの内部でも左側のフィルターで処理した出力を、右側のフィルターの入力へ渡すことができます。配列を展開して特定のキーを抽出する際によく使われます。

# 配列を展開し、それぞれの「name」キーを一括で抽出する
$ jq '.[] | .name' array_data.json

実行結果:

"Alice Smith"
"Bob"
"Carol"

条件に合致するデータのみを抽出する(「select」関数)

select() 関数を使用すると、特定の条件式(比較演算子など)を満たす要素のみを取り出すことができます。パイプ処理と組み合わせることで、「条件で絞り込んでから特定のキーの値だけを出力する」といった高度な操作が可能です。

# 1. 年齢(age)が 28 歳以上の要素全体を抽出する
$ jq '.[] | select(.age >= 28)' array_data.json

# 2. 年齢が 28 歳以上の要素から「名前(name)」だけを抽出する
$ jq '.[] | select(.age >= 28) | .name' array_data.json

実行結果:

# 1. 年齢が28歳以上の要素全体の出力
{
  "name": "Alice Smith",
  "age": 30
}
{
  "name": "Carol",
  "age": 28
}
# 2. 名前のみの出力
"Alice Smith"
"Carol"

シェルスクリプト連携時の落とし穴とエラーハンドリング

jqコマンドは非常に便利ですが、シェルスクリプト内で使用する際には、初心者が陥りやすい「3つの落とし穴」が存在します。

① -r(raw出力)オプションを指定し忘れた場合の不具合

デフォルトの jq は、抽出した値を有効なJSONデータ形式として出力します。そのため、文字列を抽出した際に "Tokyo" のように前後にダブルクォーテーション(")がついた状態で出力されてしまいます。この状態のままシェル変数に代入して後続の処理で文字列比較などを行うと、一致判定が正しく行われずバグの原因になります。文字列を取り出す際は、必ず -r (または --raw-output)オプションを指定して、ダブルクォーテーションを剥ぎ取った「生のテキスト」を取り出すようにしましょう。

② JSONフォーマットが不正だった時の対処

ファイルが壊れていたり、APIのエラーでHTMLエラーページがJSONの代わりに返ってきた場合など、入力データが正しいJSON形式ではない時、jqは標準エラー出力にエラーを吐いて停止します。特に set -e を指定しているスクリプト内では、そこでプログラムが強制終了してしまいます。API連携などを実用する際は、jqの戻り値(終了ステータス)をチェックするエラーハンドリングを設けて、不正なデータを検知できるように防御処理を仕込むことが重要です。

③ 配列データをループ処理する際の空白区切りとサブシェルの落とし穴

配列内の複数データをシェルスクリプト側で1件ずつループ処理(forやwhile)する際、よく使われる for item in $(jq ...)重大なバグの原因になります。シェルの for は改行だけでなく「スペース(空白)」もデータの区切りとして扱うため、"Alice Smith" のような値があると AliceSmith に分断して処理してしまうためです。

この空白区切りを安全に回避するには、改行だけを区切りとして処理する while read ループを使いますが、ここでも「値の渡し方」によって変数スコープの挙動(サブシェルの壁)が異なります。

  • パイプ(|)でwhileに渡す場合
    jq ... | while read -r line; do ... のようにパイプを使用すると、while ループ内の処理が「サブシェル(別の独立プロセス)」で動くことになります。そのため、ループ内でカウントした変数(count=$((count + 1)) 等)は、ループを抜けた瞬間に消失し、親シェル側には一切反映されません。
  • プロセス置換(<())でwhileに渡す場合(推奨)
    while read -r line; do ... done < <(jq ...) のようにプロセス置換を使うと、ループが同一プロセス上で実行されます。そのため、ループ内の変数の変更や状態が、ループを抜けた親シェル側でもそのまま維持され、正しく件数や結果を利用できます。
#!/bin/bash
count=0

# ⚠️ パイプを使用した場合(ループを抜けると変数が消滅する)
jq -r '.[] | .name' array_data.json | while read -r name; do
    count=$((count + 1))
done
echo "パイプ時のカウント結果: $count" # 出力: 0

# 💡 プロセス置換を使用した場合(変数が維持される:推奨)
while read -r name; do
    count=$((count + 1))
done < <(jq -r '.[] | .name' array_data.json)
echo "プロセス置換時のカウント結果: $count" # 出力: 3

まとめ

今回は、シェルスクリプトでJSONを自在に扱うための必須コマンド「jq」の導入から、基本的なデータの抜き出し、精度シェル変数へ格納する際の実践的なオプション設定について解説しました。

jqの構造化フィルターを使えば一目で何を抽出しているかが理解できる保守性の高いスクリプトになります。ぜひ役立ててください!

コメント

タイトルとURLをコピーしました