私が歌川です

@utgwkk が書いている

JSONの所望の値にアクセスするためのキーを逆算する

手元にJSONはあるけど、人間がパースするには大きくて、ぱっと見では所望の値を取得するキーが分からない。値を取得するために人間が試行錯誤しまくるのは不毛なのでもうちょっとなんとかしたい。

2021/5/3 22:15 追記

id:itchyny さんにjqでキーを逆算する方法を教えてもらった。以下で値にアクセスするためのキーの配列 (.foo[1].bar なら ["foo", 1, "bar"]) の列が得られる

$ jq -r -c --arg value foo --stream 'select(.[1] == $value)[0]'

あとはうまく文字列結合をしたりエスケープしたりすれば完成する。

2021/5/3 追記: tojson でエスケープする方法*1id:itchyny さんに教えてもらったので反映しました。

$ jq -r -c --arg value foo --stream 'select(.[1] == $value)[0] | map("[\(tojson)]") | "." + join("")'

jqで実現する方法がおそらくありそう、と思いつつ慣れたプログラミング言語で書いてしまったので、jqで実現できることが分かってよかった。

以下は試行錯誤の跡です。


与えられたJSONの、与えられた値にアクセスするためのキーを逆算するスクリプトを書いた。やっていることは深さ優先探索で、発見したキーを順に全て返す。キーに含まれる文字列によって場合分けするのが面倒なので全部クォートしている。

得られたキーを jq コマンドにそのまま投げることができる。キーから配列のインデックスを消してjqに投げるとうまく列挙できて便利。

import sys
import json

def main():
    input_json = json.load(sys.stdin)
    target_value = sys.argv[1]
    found_keys = find_keys_of(input_json, target_value)
    if found_keys:
        print('\n'.join(found_keys))
    else:
        print('not found', file=sys.stderr)
        sys.exit(1)

# 面倒なので全部クォートする
def encode_json_key(key: str):
    replaced = key.replace('"', r'\"')
    return f'["{replaced}"]'

# sys.argv[1] はstr型なので、よしなに比較する (うまくいかないこともあるかも)
def equal(json_value, input_value):
    if json_value == input_value:
        return True
    else:
        try:
            return json_value == float(input_value)
        except ValueError:
            pass
    return False

def find_keys_of(data, value):
    found_keys = []

    def _find_key_of_list(xs, value, key):
        for idx, v in enumerate(xs):
            next_key = f'{key}[{idx}]'
            _find_key_of_data(v, value, next_key)

    def _find_key_of_dict(dic, value, key):
        for k, v in sorted(dic.items(), key=lambda x: x[0]):
            next_key = f'{key}{encode_json_key(k)}'
            _find_key_of_data(v, value, next_key)

    def _find_key_of_data(data, value, key):
        if isinstance(data, list):
            _find_key_of_list(data, value, key)
        elif isinstance(data, dict):
            _find_key_of_dict(data, value, key)
        elif equal(data, value):
            found_keys.append(key)

    _find_key_of_data(data, value, '.')
    return found_keys

if __name__ == '__main__':
    main()

こういう感じで使うと、 "foo" という値を持つキーを探索して列挙してくれる。けっこう便利だと思う。

$ python3 find_key_of_value.py foo < input.json

true とか false は検索できないけどまあいいか。

*1:sub だけだと正しくエスケープするのが大変