3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

jqでノードへのパスと値を並べたファイルを作りたい

Last updated at Posted at 2019-12-27

下記のようなJSONファイルがあるとする。

{"会員名" : "文具 太郎",
 "購入品" : [ "はさみ",
              "ノート(A4,無地)",
              "シャープペンシル",
              {"取寄商品" : "替え芯"},
              "クリアファイル",
              {"取寄商品" : "6穴パンチ"}
            ]
}

予め取り出すべきデータへのパス(下の例だと."会員名"や."購入品"[0]など)が分かっている場合、JSONからCSVやTSVを作るのは容易だ。
しかし、それがわからないときにはどうするか?

ノードを特定する情報(パス)とその値をスペースなり、カンマなりタブなりで並べた値を各行とするファイルを作りたくなる。
例えばこんなような。

.会員名 文具 太郎
.購入品[0] はさみ
.購入品[1] ノート(A4,無地)
.購入品[2] シャープペンシル
.購入品[3].取寄商品 替え芯
.購入品[4] クリアファイル
.購入品[5].取寄商品 6穴パンチ

その上でgrepとかcutとかuniqとかwcとかで色々したいわけだ。
例えばはさみを買った会員は何人いるかな〜とかそういう作業だ。

既存の方法

Qiita内の人気記事に「jq、xmllintコマンドさようなら。俺はパイプが好きだから」というのがある(以下、本稿内では「さようなら記事」と呼ぶ)。
実を言えば例にあげているデータもここから拝借したものである。

めんどうなので、この方法の問題点には立ち入らない。

本稿で紹介する方法

「さようなら記事」によると正味69行のスクリプト(長大なawkワンライナーが一行含まれており実質400行だと思うが・・・)で、この目的を達することができるそうだが、実のところjqでは本来一行でできることだ。

jq -r -c '. as $content|paths(scalars)|. as $path|tostring|. as $ps|$content|[$ps,getpath($path)]|@tsv' <JSON_FILE>

JSON_FILEが各行に一個のJSONを格納している場合は-sオプションをつけて欲しい。

ただし、得られる出力はこのようになる。

["会員名"]	文具 太郎
["購入品",0]	はさみ
["購入品",1]	ノート(A4,無地)
["購入品",2]	シャープペンシル
["購入品",3,"取寄商品"]	替え芯
["購入品",4]	クリアファイル
["購入品",5,"取寄商品"]	6穴パンチ

左側の配列は何かと言うと、jq Manual (development version)ではPATHSと書かれている配列の一要素で入力内の位置を示す「パス」だ。
PATHSは二次元配列になっており、各要素たる配列は入力のJSONノード内での特定の位置を表している)
この配列は、getpathsetpathの引数、pathsの出力などに用いられている。

つまりjqではドキュメント内の位置を示すやり方が2つあるのだ。

  • 一つは普段、コマンドラインで使う."購入品"[0]のような記法。(path_expressionと呼ばれる)
  • もう一つは上に示した形式"array representation"(配列形式パスとでも呼んでおこう)

しかし、この配列形式パスを文字列化してしまうと、もう一度jqでパースしないとパスを表す情報として使えないし不便だ。
だから."会員名"."購入品"[0]のように、jqに与えられる形式で書き出したいものだ。

そして問題は前者から後者の変換はできるが、その逆はできないことなのだ。(Issue-1949 of jq)
少なくとも令和元年12月現在では。

「できない」と言ったが、それはjqに最初からついているビルトインではできないというだけで、自分でjqの関数を書けばできる。

echo '{"会員名" : "文具 太郎",
 "購入品" : [ "はさみ",
              "ノート(A4,無地)",
              "シャープペンシル",
              {"取寄商品" : "替え芯"},
              "クリアファイル",
              {"取寄商品" : "6穴パンチ"}
            ]
}' | jq -r -c 'def path2pexp($v):
  $v | reduce .[] as $segment (""; . + ($segment | if type == "string" then ".\"" + . + "\"" else "[\(.)]" end));
. as $content|paths(scalars)|. as $p|path2pexp(.)|. as $pexp|$content|[$pexp,getpath($p)]|@tsv'

上記のコマンドラインは以下の出力を与える。

."会員名"	文具 太郎
."購入品"[0]	はさみ
."購入品"[1]	ノート(A4,無地)
."購入品"[2]	シャープペンシル
."購入品"[3]."取寄商品"	替え芯
."購入品"[4]	クリアファイル
."購入品"[5]."取寄商品"	6穴パンチ

タブの左側はそのままjqに渡すことができる文字列になっている。

解説

上述のコマンドラインのうち、ここに掲げる2行が配列形式パスをpath_expressionに変換する関数である。
これは上記のIssue-1949 of jqに投稿されたコードスニペットを私が改良したものである。

1-2行目

def path2pexp($v):
  $v | reduce .[] as $segment (""; . + ($segment | if type == "string" then ".\"" + . + "\"" else "[\(.)]" end));

変数$segmentに代入される配列形式パスの各要素を左から順に繋いでいく。ただし、型が文字列のときにはダブルクォートで括る

3行目

. as $content|paths(scalars)|. as $p|path2pexp(.)|. as $pexp|$content|[$pexp,getpath($p)]|@tsv'

ビルトイン関数pathsに同じくビルトインscalarsを与え、文書内すべてのスカラー(非オブジェクト、非配列)への配列形式パスを得る。
各配列形式パスを$pに代入(. as $p)。これをpath2pexpによりpath_expressionに変換したものを$pexpに代入(path2pexp(.)|. as $pexp)
あとは$pexpとそれに対応する位置にある値をgetpath($p)で得て配列に格納し、TSVに変換する。([$pexp,getpath($p)]|@tsv)

まとめに代えて

まとめに代えて以下を指摘しておく。

  • そもそもJSONはその仕様上、行指向のデータとも言える。
  • jqは入力をJSONとし、出力は整形されたJSONまたは行指向のデータ(JSON、CSV、TSV等)である。
  • jqはC99でコンパイルできる(試したことはないがそういうことになっている)
  • JSONをUNIXやUNIX-likeなOSで使うならjqの文法は覚えた方がいい。

参考

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?