jqfpyというコマンドを作りました。
はじまり
jqfpyというコマンドを作りました。作った理由は概ねこの記事に書いてあるとおりです。
むずかしさの段階
JSONを整形したいときには概ね以下の3段階くらいの状況があるきがします。
はじめはかんたんな整形でここではjqが楽です。そして2つ目を飛ばして最後の3段階目はワンライナーでは限界になり真面目に何らかのスクリプトを書いたほうが良いと言う感じです。そして飛ばした2つ目の部分がここでの焦点です。ぱっと見そんなに複雑ではないように思えるもののいざjqで書こうとすると思うようにかけず無駄に時間を消費してしまうという類の状況に結構悩まされていました。
特に、jqは最終的には記述が短く簡潔になるものの、その記述にたどり着くために苦労するみたいなことが結構ありました。
jqという別の言語をもう1つ覚える必要ある?
この時思ったのは、そもそも、jqというDSLを使うからだめで、自分の母国語(今の所個人的にはpythonになっている。そういうことにしておいてください。人によって変わると思います)で直接ワンライナーが書ければ捗るのでは?ということでした。
自分の母国語で記述することにしてしまえば、最初の段階ではある程度文字数が増えるものの、作業の複雑さに対しての学習難度のようなものの増加が穏やかになるんじゃないかな?なったらいいなーという感じです。pythonなら書けるので(ここでのpythonは自由に読み替えてください)。
jqfpyという名前
名前は jq for pythonista (jq for python) の略です。最初はjqの代替なのでqjにしようと思いましたが、jqの代替は他にも出てくる可能性が在るということで止めました。
けっこうてきとうです。
大雑把な解説
まず前提として、シェル上からワンライナーで使うために在るので、シェル的な意味で一行で書けることが望まれています。
その上で、コア部分は単純で、;
を改行扱いにしてコードを生成しexecしているだけです。
なので例えば、x = f(); y = g(x); h(y)
という記述があった場合には、以下の様なコードになるだけです。
x = f() y = g(x) h(y)
これでは入力も出力も表せられないので変換する関数を生成することにしました。入力はjsonをパースした辞書でも良かったのですが。その引数名をどうしようかなど考える内にただただ入力となるデータを取り出せる関数が1つあれば良いのではないかと思いました。
そのため、get()
という関数がひとつだけ実行時のスコープで利用できます。またrubyやrustなどを考えてみるに、関数のボディの最終行を戻り値とするという規則はそれなりに広く受けいられそうなきがします。なのでこれを採用することにしてみました。
実際のコードでは上の x = get(); y = g(x); h(y)
は以下の様な関数に変換されます。
def transform(get): x = get() y = g(x) return h(y)
get
もうすこし get()
について話すと、get()
は dict.get()
の亜種のようなものです。
ただ少し高機能です。
1つには、ネストしたアクセスが可能になっています。これはワンライナーを書く上で意外と添字アクセスとそのキーとなる文字列をクォートで括るのがめんどくさいと思ったからです。なので以下の様にかけます。
get("foo/bar/boo")
これと似たようなものです。
get()["foo"]["bar"]["boo"]
ただし実際には等価ではないです。途中でキーが見つからなかった場合に評価がそこで打ち切られてNoneが返ります。これはjqのドットによるアクセスも同様の結果になるので採用しました。
ただなんとなくの好みでドットによるアクセスではなく、json pointerに似た記法を採用しています。なのでキーが /api/foo
などのJSONには、~1api~1foo
でアクセスします。
もう1つ、トップレベルのデータへのアクセスと、辞書に対するアクセスを兼ねています。第二引数が辞書です。そして第二引数は省略可能で省略した場合にはトップレベルのデータということになっています。
なのでちょっと不格好ですが。以下は等価です。
get("foo/bar/boo") get("bar/boo", get("foo")) get("boo", get("bar", get("foo")))
もちろん変数も使えるのでこう書いても大丈夫です。
foo = get("foo"); bar = get("bar"); get("boo")
ちなみにこの変数を手軽に定義できて、手続き的に書けるというのが学習曲線が緩やに感じる点のひとつになるような気がします。 jqの文法は少し頭の使いかたを変えないとだめです。個人的には。
インストール方法
インストールはいつもどおりpipを使って以下の様にします。
pip install jqfpy
examples
幾つかの例を書いてみます。例えば以下のようなJSONからnameを抜き出す場合には以下のようにします。
1つの単純なJSON
00data.json
{ "name": "foo", "age": 20 }
おおよそ、jqと同じような形で使えます。
$ cat 00data.json | jqfpy 'get("name")' "foo" $ cat 00data.json | jqfpy -r 'get("name")' "foo" $ jqfpy -r 'get("name")' 00data.json "foo"
複数のJSONが繋がった場合
jqと同様に複数のJSONが繋がった場合の入力にも対応しています。不揃いであっても大丈夫です。
01data.json
{ "name": "foo", "age": 20 } { "name": "bar", "age": 10 } { "name": "boo", "age": 11 }
-c
と一緒に使うことでコンパクトな出力になります。
$ jqfpy -c 'get()' 01data.json {"name": "foo", "age": 20} {"name": "bar", "age": 10} {"name": "boo", "age": 11}
年齢順でソートしてみます。ただし複数のJSONが繋がったものはarrayではないので、--slurp
オプションが必要になります。
それ以外はは普通のpythonのコードなのでsorted()
でソートする感じです。このあたりはvalidなpythonのコードを書かなければ行けないのでちょっと長くなります。
$ jqfpy --slurp 'sorted(get(), key=lambda x: int(x["age"]))' 01data.json [ { "name": "bar", "age": 10 }, { "name": "boo", "age": 11 }, { "name": "foo", "age": 20 } ]
もとのファイルと同様に1つネストを外したい場合には、--squash
をつけます。これはjqにはないです(jqは通常の文法の中にこの機能を持っています)。
$ jqfpy -c --squash --slurp 'sorted(get(), key=lambda x: int(x["age"]))' 01data.json {"name": "bar", "age": 10} {"name": "boo", "age": 11} {"name": "foo", "age": 20}
フィルタリング
フィルタリングをしてみましょう。このあたりになるとただのpythonのワンライナーです。
偶数だけ取り出してみます。ただのリスト内包表記です。
$ jqfpy -c --squash --slurp 'L = [x for x in get() if int(x["age"]) % 2 == 0]; sorted(L, key=lambda x: int(x["age"]))' 01data.json {"name": "bar", "age": 10} {"name": "foo", "age": 20}
これは以下の様なコードになっています。--show-code-only
というオプションをつけると実行コード自体を出力してくれます。
$ jqfpy --show-code-only -c --squash --slurp 'L = [x for x in get() if int(x["age"]) % 2 == 0]; sorted(L, key=lambda x: int(x["age"]))' 01data.json def _transform(get): L = [x for x in get() if int(x["age"]) % 2 == 0] return sorted(L, key=lambda x: int(x["age"]))
ふつうのpythonコードですね。
エラーメッセージ
ここで少しだけ間違ってみましょう。エラーメッセージは以下の様になります。なんとなくエラーの箇所が把握できる程度には情報があります。たぶん。
jqfpy --squash --slurp 'sorted(get(), key=lambda x: int(x["ag"]))' 01data.json code: ---------------------------------------- def _transform(get): L = [x for x in get() if int(x["age"]) % 2 == 0] return sorted(L, key=lambda x: int(x["ag"])) ---------------------------------------- error: Traceback (most recent call last): File "/home/podhmo/my/bin/jqfpy", line 11, in <module> load_entry_point('jqfpy', 'console_scripts', 'jqfpy')() File "/home/podhmo/my/jqfpy/jqfpy/__main__.py", line 102, in main r = jqfpy.transform(transform_fn, d) File "/home/podhmo/my/jqfpy/jqfpy/__init__.py", line 54, in transform return fn(Getter(d).get) File "<string>", line 3, in _transform File "<string>", line 3, in <lambda> KeyError: 'ag'
‘age'をtypoしています。
正規表現でフィルタリング
正規表現でフィルタリングしたいとおもいます。 このあたりになるとjqの場合にはどのような関数を呼ぶかを思い出せずドキュメントを見に行く必要が出ると思うのですが。 こちらはただのpythonのこーどなので、通常のpythonのコードと同じにreモジュールをimportして使えば良いです。
ワンライナーですが。
$ alias QJ="jqfpy -c --squash --slurp" $ QJ 'import re; rx = re.compile("o{2}"); [x for x in get() if rx.search(x["name"])]' 01data.json {"name": "foo", "age": 20} {"name": "boo", "age": 11}
ちなみにインデントが必要なコードは非推奨です。
バッファリング
デフォルトでバッファリングが無効です。
$ tail -f app.log | jqfpy 'get("msg")' | grep foo # 同じ意味 $ tail -f app.log | jqfpy --unbuffered 'get("msg")' | grep foo
クローズされたりするまで後段のプロセスに出力しないみたいな状況にはならないです。