シェルスクリプトでJSONパーサを作ろう

2017年12月21日

シェルスクリプトでJSONを扱う場合、jqコマンドを使うと楽みたい。…なんだけど、あえてunixコマンドでやってみる。てかそんな複雑な処理しないし。。。

JSONデータフォーマット

JSONデータはオブジェクトの配列です。例えば今回処理したいフライト情報であれば各便の情報がそれぞれオブジェクトになっています。

オブジェクトには名前と値の対になった各種パラメータが並びます。こんな感じ。

[
  {"名前":"値", "名前";"値", ...},
  {"名前":"値", "名前";"値", ...},
  ...
  {"名前":"値", "名前";"値", ...}
]

今回は処理したい便のデータのみ取得しているので、オブジェクトはひとつだけ。

$ cat jl0001.json
[{"@type":"odpt:FlightInformationArrival","dc:date":"2017-12-21T07:58:11+09:00","owl:sameAs":"odpt.FlightInformationArrival:HND.JL0001","odpt:airline":"JAL","odpt:operator":"odpt.Operator:HND-TIAT","odpt:terminal":"odpt.AirportTerminal:HND.InternationalPassengerTerminal","odpt:actualTime":null,"odpt:flightStatus":null,"odpt:aircraftModel":null,"odpt:estimatedTime":null,"odpt:flightNumbers":["JL0001","AA8400","MH9101"],"odpt:scheduledTime":"19:15","odpt:departureAirport":"odpt.Airport:SFO","odpt:destinationAirport":"odpt.Airport:HND"}]

…。仕様ではわかりやすく改行されているけれど実際は改行なし。unixコマンドは行単位の処理が得意なので、オブジェクトを分解して1パラメータ1行にしてしまいましょう。

各パラメータ値の抽出

JSONのパラメータはそれぞれ,(カンマ)で区切って列挙されているので、trでこれを改行に変換すればOK。頭の[{とお尻の}]が邪魔であれば、あらかじめこれらをsedで落としておきましょう。

$ cat jl0001.json | sed 's/^\[{\(.*\)}]$/\1/' | tr ',' '\n'
"@type":"odpt:FlightInformationArrival"
"dc:date":"2017-12-21T07:58:11+09:00"
"owl:sameAs":"odpt.FlightInformationArrival:HND.JL0001"
"odpt:airline":"JAL"
"odpt:operator":"odpt.Operator:HND-TIAT"
"odpt:terminal":"odpt.AirportTerminal:HND.InternationalPassengerTerminal"
"odpt:actualTime":null
"odpt:flightStatus":null
"odpt:aircraftModel":null
"odpt:estimatedTime":null
"odpt:flightNumbers":["JL0001"
"AA8400"
"MH9101"]
"odpt:scheduledTime":"19:15"
"odpt:departureAirport":"odpt.Airport:SFO"
"odpt:destinationAirport":"odpt.Airport:HND"

これで必要なパラメータのみ名前(キー)を手掛かりにgrepで抽出して値を取り出せばOKですね。

日時データの取得

$JSONは上記改行処理をしたデータファイルです。これを下記フィルタに通すとデータ生成日時や運航時刻(定刻、変更後、実際)を取得できます。

grep $JSON -e "dc:date" | cut -d ':' -f 3- | tr -d '"'
grep $JSON -e "odpt:scheduledTime" | cut -d ':' -f 3- | tr -d '"'
grep $JSON -e "odpt:estimatedTime" | cut -d ':' -f 3- | tr -d '"' | sed 's/null//'
grep $JSON -e "odpt:actualTime" | cut -d ':' -f 3- | tr -d '"' | sed 's/null//'

キーポイントはcutですが、時刻フォーマットは値中に:(コロン)を含むので、-f 3-として3フィールド目以降をすべて取得するようにします。

空港データの取得

時刻と違って値中に:(コロン)を含まないのでsedで切り出しています。

それぞれ:(コロン)の数が違いますがsedの検索は後方一致なので、いずれも^.*:というパターンで:(コロン)区切りの最後の要素を切り出すことができます。

grep $JSON -e "odpt:airline" | sed 's/^.*://' | tr -d '"'
grep $JSON -e "odpt:operator" | sed 's/^.*://' | tr -d '"'
grep $JSON -e "odpt:terminal" | sed 's/^.*://' | tr -d '"'
grep $JSON -e "odpt:destination" | sed 's/^.*://' | tr -d '"'
grep $JSON -e "odpt:departure" | sed 's/^.*://' | tr -d '"'
grep $JSON -e "odpt:flightStatus" | sed 's/^.*://' | tr -d '"' | sed 's/null//'

配列データの処理

JL0001便は共同運航便なので、フライト番号がカンマ区切りの配列になっています。trを通したら、ばらばらになっちゃいました。

加えて成田便はプロパティ名(キー)がodpt:flightNumberと単数形になっていてちょっと違う。

$ cat sq0637.json | sed 's/^\[{\(.*\)}]$/\1/' | tr ',' '\n'
"@id":"urn:uuid:b91070d7-2fdc-46a3-9501-135f7b080910"
"@type":"odpt:FlightInformationDeparture"
"dc:date":"2017-12-21T08:15:02+09:00"
"@context":"http://vocab.odpt.org/context_odpt.jsonld"
"odpt:gate":"46"
"owl:sameAs":"odpt.FlightInformationDeparture:NRT.SIA.SQ0637"
"odpt:airline":"odpt.Airline:SIA"
"odpt:airport":"odpt.Airport:NRT"
"odpt:operator":"odpt.Operator:NAA"
"odpt:terminal":"odpt.AirportTerminal:NRT.Terminal1"
"odpt:flightNumber":"SQ0637"
"odpt:flightStatus":"odpt.FlightStatus:OnTime"
"odpt:estimatedTime":"11:05"
"odpt:scheduledTime":"11:05"
"odpt:departureAirport":"odpt.Airport:NRT"
"odpt:destinationAirport":"odpt.Airport:SIN"

これらをまとめてうまく処理するため、フライト番号だけは改行していないデータに対して以下を適用します。

cat $JSON | sed 's/^.*flightNumbers*":\[*\(.*\)/\1/' | cut -d ':' -f 1 | sed 's/\]*,"odpt.*//' | sed 's/","/\//g' | tr -d '"'

パターン文字列のflightNumbersに*を足しているので、成田仕様のflightNumberにも羽田仕様のflightNumbersにも対応しています。

今回はcutを使うことでフライト番号に続く不定数のodptを一気に削除してから、残った&rblack*,”odpt以降を削除しています。

カンマ区切りをスラッシュに変更しているのは趣味の問題。

JSONパーサモジュール

以上を踏まえてまとめたスクリプトがこちら。一応冒頭の3行目で正しいデータかどうかの簡易チェックを入れています。

#! /bin/sh
JSON=`cat | sed 's/\[]//g' | sed 's/^\[{\(.*\)}]$/\1/'`
if test ! `echo $JSON | grep -e "FlightInformation"`; then
  echo "There is no data."
  exit
fi
FLIGHT=`echo $JSON | sed 's/^.*flightNumbers*":\[*\(.*\)/\1/' | cut -d ':' -f 1 | sed 's/\]*,"odpt.*//' |\
  sed 's/","/\//g' | tr -d '"'`
DATETIME=`echo $JSON | tr ',' '\n' | grep -e "dc:date" | cut -d ':' -f 3- | tr -d '"'`
DEPT=`echo $JSON | tr ',' '\n' | grep -e "odpt:departure" | sed 's/^.*://' | tr -d '"'`
ARRV=`echo $JSON | tr ',' '\n' | grep -e "odpt:destination" | sed 's/^.*://' | tr -d '"'`
SCH=`echo $JSON | tr ',' '\n' | grep -e "odpt:scheduledTime" | cut -d ':' -f 3- | tr -d '"'`
STATUS=`echo $JSON | tr ',' '\n' | grep -e "odpt:flightStatus" | sed 's/^.*://' | tr -d '"' | sed 's/null//'`
EST=`echo $JSON | tr ',' '\n' | grep -e "odpt:estimatedTime" | cut -d ':' -f 3- | tr -d '"' | sed 's/null//'`
ACT=`echo $JSON | tr ',' '\n' | grep -e "odpt:actualTime" | cut -d ':' -f 3- | tr -d '"' | sed 's/null//'`
echo "Record date: $DATETIME"
echo "Flight No.: $FLIGHT"
echo "From $DEPT to $ARRV"
echo "Scheduled time: $SCH"
echo "Status: $STATUS"
if test "$EST" != ""; then
  echo "Estimated time: $EST"
fi
if test "$ACT" != ""; then
  echo "Actual time: $ACT"
fi