俵言

しがない社会人が書く、勉強とかのこと。最近は機械学習や kaggle 関連がメイン。

小ネタ:python で json を yaml として読みたいとき無いですか?

多分相当稀なケースなんですがたまにある気がしてます。

yaml 便利ですよね!

実験管理の config (あるいは setting?) を yaml ファイルで書くようになってから随分経ちました。 クォーテーションが要らない、括弧が少なく済む、ブロックスタイル使えるといった理由からスッキリ書けるし楽です。
あと json で書くと迂闊にカンマを入れたときに error が発生するので非常にめんどくさい。まあそもそもログには使うけど人が書くのには向かないのか...?

また、コメント書けるのも人が書きやすい理由ですね。

以下は類似した内容を jsonyaml で書いた例です。

json で書く場合(sample.json)

{
    "Boolean":[true, false],
    "NaN":null,
    "Number List":[1, 2.0, 3e-3, 4e+04, -5],
    "Str2NumberDict":{
        "six":6,
        "seven":7,
        "eight":8
    },
    "DateDictList":[
        {"event_id":0, "start":"2018-10-04","end":"2019-01-11"},
        {"event_id":1, "start":"2019-03-29","end":"2019-06-11"},
        {"event_id":2, "start":"2019-05-18","end":"2019-06-21"},
        {"event_id":3, "start":"2019-10-16","end":"2019-12-20"},
        {"event_id":4, "start":"2019-10-24","end":"2019-01-23"}
    ]
}

yaml で書く場合(sample.yml)

Boolean: [true, false]  # 真理値
NaN: null  # None として読み込まれる
Number List: [1, 2.0, 3e-3, 4.e+04, -5]  # 数字
Str2NumberDict:  # 辞書
    six: 6
    seven: 7
    eight: 8
DateDictList:  # 辞書のリスト
    - {event_id: 0, start: 2018-10-04, end: 2019-01-11}
    - {event_id: 1, start: 2019-03-29, end: 2019-06-11}
    - {event_id: 2, start: 2019-05-18, end: 2019-06-21}
    - {event_id: 3, start: 2019-10-16, end: 2019-12-20}
    - {event_id: 4, start: "2019-10-24", end: "2020-01-23"}

ちなみに yaml でちょっと気を付けないといけないのは少数の指数表記で、1.0e-04 は数字として認識されるのに 1e-04 は文字列として認識されてしまいます。(なので、上の例の 3e-3 は文字列として認識されちゃいます。)

なぜ jsonyaml として読みたいのか?

「書くのはめんどいけど整形されてる json は 読み込めば良いだけでは?」と思われると思うのですが、yamljson と違って datetime 型を扱えるという便利な点があります*1

json ファイルの中に日付(or 日時)が上の例みたいに文字列で格納されてると parse するのめんどくさいから yaml として読みたいよねって言うのがこの記事のお話。(因みに datetime 型を含んだ辞書を json として吐き出すのも少し処理が要ります*2。)

上のファイルを読み込んでみると sample.json の方は日付を文字列として入れてるので当然ながら文字列扱いで、

>>> import json
>>> with open("sample.json", "r") as fr:
...     d1 = json.load(fr)
...    
>>> print(d1["DateDictList"][0]["start"], type(d1["DateDictList"][0]["start"]))
2018-10-04 <class 'str'>

sample.yml の方は datetime 型として認識されます。

>>> import yaml
>>> with open("sample.yml", "r") as fr:
...     d2 = yaml.load(fr)
...    
>>> print(d2["DateDictList"][0]["start"], type(d2["DateDictList"][0]["start"]))
2018-10-04 <class 'datetime.date'>

因みに明示的に 文字列として与えてるやつ(4個目)は文字列として認識されます。

>>> print(d2["DateDictList"][4]["start"], type(d2["DateDictList"][4]["start"]))
2019-10-24 <class 'str'>

json ファイルを yaml として読む

そんなに難しいことはしてなくて、単に文字列として読んでから置換操作をし、最後に yaml として読みます。

>>> with open("sample.json", "r") as fr:
...     d3 = yaml.load(fr.read().replace("'", "").replace('"', '').replace(":", ": "))

やってることとしては

  • クォーテーション(シングル・ダブル)の削除
  • コロンの後ろに空白を作る(yaml として読みたい場合はこれをしないと Error が起きる)

だけですね。2つめについてはログファイルの場合は空白無かったりするのでやる必要があると思います。

>>> print(d3["DateDictList"][0]["start"], type(d3["DateDictList"][0]["start"]))
2018-10-04 <class 'datetime.date'>

ちゃんと datetime.date 型として読まれています。

文字列の置換だけでいいのは、 json ファイルはおおよそ yaml で読める形をしているので(※コロンの後に空白が無い場合は怒られる)、文字列として与えられてる日付の両端のクォーテーション外してやれば良いよねって話です。
かなり適当な方法なので何か問題起こるかも...*3

終わりに

まあ使いどころがあるのかないのか微妙ですがたまに使えるかなって。

json 形式で year, month, day, hour, minute, second を分けて与えてそこから日付を取るとか、今回みたいにかなり綺麗に並んでるなら一回 DataFrame に食わせてしまって変換するでも良いんですけど、読み込み時に一気にparse 出来ると楽なんですよねー。

かなりしょうもないネタでしたが、たまには軽いネタでてきとうな記事書くのも良いかなってことで以上で終わります。

*1:参考: JSON と YAML の対応データ型の比較メモ

*2:参考: JSONファイルにdatetimeとか保存したいやん?

*3:例えば文字列として与えてる id を表す数字を Integer として解釈しちゃうとか。本当は正規表現でちゃんとやった方が良いですね。