最近なんでもかんでもJSONですが、MongoDB/ElasticsearchにJSONを突っ込もうとして問題にぶつかりました。具体的には表題の通りで、 キーにDot(.)を含むものが上手く動かない
という点です。現状は キーにDotを使うな、もしもデータファイルに入っていたらコンバートしろ
ということなので、コンバートフィルタを書きました。`
どうして困るのか?
{ "id": "hogehoge", "members": [ "hisatoshi", "toshihisa" ], "detail": [ { "name": "toshihisa" }, { "name": "imaoka", "org": { "addr.name": "hogehoge", "tel.num": "fugafuga" } } ] }
こういうJSONの場合、MongoDBやElasticsearchに突っ込んだ後、検索する場合は addr.name
というキーは detail[1].org.addr.name
と表現します。この場合 キーに含まれるDotが、階層を表すDotと競合してしまい、まともに動かないという現象が表れます。
例がよくなかった、途中で配列入っているの気にしないで。要は Object in Object な JSON
- MongoDBの場合 Dotが含まれていようが、JSONインポートが出来ます。が、検索が出来ないので意味なし。
- Elasticsearchの場合 インポートの段階で明確に蹴られます。
JSONキー置換フィルタの構想
最初は、正規表現の置換で良いかと思いましたが。JSONの中に JSON-textを含むような JSON-in-JSONのような構成をとられるとムダに破壊するかもと思い止めました。が、よく考えたら JSONのテキスト要素として JSONを表現する場合は でエスケープするので、正規表現だけでもいけますね、多分。。。
JSON吸って、pythonのオブジェクトにして、JSONで出す
多分正規表現のみでも行けそうと今、書いていて感じてますが、上記の方針で実装しました。 要は泥臭く実装するってことです。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys import json def conv_key(obj): if isinstance(obj, dict): for org_key in obj.keys(): new_key = org_key if '.' in org_key: new_key = org_key.translate({ord("."): ord("_")}) obj[new_key] = obj.pop(org_key) conv_key(obj[new_key]) elif isinstance(obj, list): for val in obj: conv_key(val) else: pass def main(): for line in sys.stdin: my_json = json.loads(line) conv_key(my_json) print json.dumps(my_json) if __name__ == '__main__': main()
使い方
./conv_json_invalid_keys.py < hoge.json
実装の説明
- 再起です。むちゃくちゃ深い JSONだとスタック足らないかもしれません。(そんなJSONで変換が必要とかどうかしている!)
- JSON->Pythonの対応として、受けうる型は list , dict, 普通の値の3種類のはず
- list は dictの listかもしれないので、そのままもう一回解析
気づき
- CSVのように使うJSONは、1行1JSONで書く必要がある。醜いからといってjqで整形してしまうとインポート出来ない
- type(obj) とかやって比較しようとしていたけど、そんなの時代遅れらしい、
isinstance
使え! - tr相当の処理
string.translate
も unicodeかstrかで違うらしい、この実装は unicode向け - json vs simplejson はほとんど変わらんらしいが、 enc/dec がそれぞれ対照的に速い・遅いらしい