XPath で dict:辞書を参照

区切り文字 '/' で表現するXPathdict:辞書を参照するものを作りました。
jsonpath-ng · PyPI ではありません。

XPath の書式で配列の何番目かの指定をするインデックス [n] を単独で区切る場合、、
 /key/key/[0]
と書くのが正しいのか?
それとも
 /key/key[0]
が正しいのか?、わからなくなってしまいましたが、後者を採用することにして、
以下、クラスの staticメソッドとして用意しました。

dicttools.py

# -*- coding: UTF-8 -*-
import re
class dictXpath():
    @staticmethod
    def get(dct, path):
        p = path.replace('[', '/[') if re.match(r'^.+\[[0-9]+.+', path) else path
        return dictXpath._get(dct, p)
    @classmethod
    def _get(self, _dct, _path):
        pl = _path.strip('/').split('/')
        if re.match(r'^\[[0-9]+\]$', pl[0]):
            i = int(pl[0].strip('[').strip(']'))
            if len(_dct) <= i: raise IndexError(pl)
            return self._get(_dct[i], '/'.join(pl[1:])) if len(pl) > 1 else _dct[i]
        d = _dct.get(pl[0])
        return self._get(d, '/'.join(pl[1:])) if d is not None and len(pl) > 1 else d

これは、無理やり最初にリストの何番目かを示すインデックス [n] が path に
含まれたら区切り文字で区切っています。

使用方法は、予め用意した dict を 以下のように書きます

from dicttools import dictXpath

type_scheme = dictXpath.get(foo, "/tokyo/scheme/type")
first_group = dictXpath.get(foo, "/tokyo/scheme/group[0]")

jsonpath-ng より、機能は劣っています。

JSONDecoder を継承してJSON→任意クラスへの変換

任意クラスをネストした構造への JSON読込みオブジェクト変換は、json.load() の結果のdict:辞書
丹念にパースすればできますが、膨大なコードを書きたくありません。
json.load() object_hook でなんとかならないのかと悩みましたが、日付時刻型などは対応できても
任意クラスの自由な構造を許可する構造を汎用的に書くには無理があります。
日付時刻型だけをobject_hook でなんとかするとして、
任意クラスの自由な構造は、やはり JSONDecoder を継承して最後のdict:辞書を解析するしかないようです。
object_hook で、日付時刻型も対処したJSONDecoder継承にすれば、良いようです。

JSONファイル:item.json

{
  "name": "Apple",
  "price": 1200,
  "flag": false,
  "mount": [
    120,
    140
  ],
  "current": 1.02,
  "latest": "2020-12-11 09:46:26",
  "history": [
    {
      "version": 1.0,
      "updated": "2020-12-01 10:00:00"
    },
    {
      "version": 1.01,
      "updated": "2020-12-09 22:30:52"
    }
  ]
}

このJSONを読込んで、構成するクラス Item と ItemHistory

from datetime import datetime
from pytz import timezone
from itemhistory import ItemHistory

class Item():
    def __init__(self):
        pass
    @property
    def _name(self):
        return self.name
    @_name.setter
    def _name(self, value):
        self.name = value
    @property
    def _price(self):
        return self.price
    @_price.setter
    def _price(self, value):
        self.price = value
    @property
    def _flag(self):
        return self.flag
    @_flag.setter
    def _flag(self, value):
        self.flag = value
    @property
    def _mount(self):
        return self.mount
    def addMount(self, value):
        self.mount.append(value)
    @property
    def _current(self):
        return self.current
    @_current.setter
    def _current(self, value):
        self.current = value
    @property
    def _latest(self):
        return self.latest
    @_latest.setter
    def _latest(self, value):
        self.latest = value
    @property
    def _history(self):
        return self.history
    def addHistory(self):
        h = ItemHistory()
        h.version = self.current
        h.updated = self.latest
        self.history.append(h)
        self.current = self.current + 0.01
        self.latest = datetime.now(tz=timezone('Asia/Tokyo'))

Item の historyが、ItemHistoryのリストになります

class ItemHistory():
    def __init__(self):
        self.version = None
        from datetime import datetime
        from pytz import timezone
        self.updated = datetime.now(tz=timezone('Asia/Tokyo'))
    @property
    def _version(self):
        return self.version
    @_version.setter
    def _version(self, value):
        self.version = value
    @property
    def _updated(self):
        return self.updated
    @_updated.setter
    def _updated(self, value):
        self.updated = value

customs.py:JSONDecoder を継承したクラス、CustomDecoder を定義します。

# -*- coding: UTF-8 -*-
import re
from json import JSONDecoder, JSONDecodeError
from datetime import datetime, date

class CustomDecoder(JSONDecoder):
    FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
    WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)

    def __init__(self, decode_hook = None):
        JSONDecoder.__init__(self, object_hook=self.object_hook)
        self.decode_hook = decode_hook
        self._rt = [
            (
                re.compile(
                    '^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
                    '%Y-%m-%d %H:%M:%S'
                ),
            (
                re.compile(
                    '^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
                 '%Y-%m-%dT%H:%M:%S'
                ),
            (
                re.compile(
                    '^\d{4}/(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
                    '%Y/%m/%d %H:%M:%S'
                ),
            (
                re.compile('^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$'),
                '%Y-%m-%d'
            ),
            (
                re.compile('^\d{4}/(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01])$'),
                '%Y/%m/%d'
            ),
        ]
    # Object hook
    def object_hook(self, obj):
        if isinstance(obj, dict):
            for k, v in obj.items():
                obj[k] = self.object_hook(v)
        elif isinstance(obj, list):
            for i, e in enumerate(obj):
                obj[i] = self.object_hook(e)
        elif isinstance(obj, str):
            try:
                dlst = [datetime.strptime(obj, rtt[1]) for rtt in self._rt if rtt[0].match(obj)]
                if len(dlst) > 0:
                    obj = dlst[0]
            except (ValueError, AttributeError):
                pass
        return obj
    # decode を override して、最後に decode_hook を実行する
    def decode(self, s, _w=WHITESPACE.match):
        obj, end = self.raw_decode(s, idx=_w(s, 0).end())
        end = _w(s, end).end()
        if end != len(s):
            raise JSONDecodeError("Extra data", s, end)
        return self.decode_hook(obj)

dict:(辞書)から任意のクラスオブジェクトに変換する為のメソッド
=decode_hook に指定するメソッドを定義します。

from item import Item

def item_parser(obj):
    item = Item()
    for k,v in obj.items():
        if k=='history':
            if isinstance(obj[k], list):
                res = list()
                for _o in obj[k]:
                    h = ItemHistory()
                    for _k,_v in _o.items():
                        setattr(h, _k, _v)
                    res.append(h)
            else:
                res = None
            setattr(item, k, res)
        else:
            setattr(item, k, v)
    return item

'history' キー名で読込んだものをリストとして、ItemHistory を生成するようにしてます。

CustomDecoder をcls= で指定、decode_hook をパラメータ追加で呼出し実行します。

import codecs
import json
from customs import CustomDecoder

with codecs.open('item.json', 'r', 'utf-8') as f:
    i = json.load(f, cls=CustomDecoder, decode_hook=item_parser)


結果をtype() で調べると

    print(type(i))
    # <class 'item.Item'>

    print(i.name)
    print(i.price)
    print(i.current)
    print(i.flag)
    print(i.mount)
    print(i.latest)
    print(type(i.latest))
    print(i.history)

    for h in i.history:
        print(type(h))
        print(h.version)
        print(h.updated)
        print(type(h.updated))

decode_hook に指定するメソッドは、Item クラスのメソッドとして
定義しても良いと思います。
その場合呼出しは、Item インスタンスからの呼びだしではなくて
staticメソッドとしての呼び出しです。

  i = json.load(f, cls=CustomDecoder, decode_hook=Item.item_parser)

JSON → Object 変換(構造が単純な場合)

JSON を読込み任意のオブジェクトにする場合、
 オブジェクトが単純な構造=ネストで任意の構造のオブジェクトを持たない!
という比較的単純な構造の場合に限り、
次のサンプルのように、json.load実行object_hook を定義して渡すことで解決する。

JSONから変換されるオブジェクトのクラス

class User():
    @property
    def _name(self):
        return self.name
    @_name.setter
    def _name(self, value):
        self.name = value
    @property
    def _flag(self):
        return self.flag
    @_flag.setter
    def _flag(self, value):
        self.flag = value
    @property
    def _time(self):
        return self.time
    @_time.setter
    def _time(self, value):
        self.time = value

object_hook で実行するメソッドの定義
isinstance() で、dict :辞書の時に、リフレクションである setattr 関数で値をセットして
User を返します。

def parser(obj):
    if isinstance(obj, dict):
        for k,v in obj.items():
            obj[k] = parser(v)
    elif isinstance(obj, list):
        for i,e in enumerate(obj):
            obj[i] = parser(e)
    elif isinstance(obj, str):
        try:
            obj = datetime.strptime(obj, '%Y-%m-%d %H:%M:%S')
        except (ValueError, AttributeError):
            pass
    if isinstance(obj, dict):
        u = User()
        for k,v in obj.items():
            setattr(u, k, v)
        return u
    return obj

対象のJSON

string = '{"name":"Apple","flag":true,"time":"2020-12-09 17:21:03"}'

実行

user = json.loads(string, object_hook=parser)

print(type(user))
print('%s %r %s' % (user.name, user.flag, user.time.strftime('%Y/%m/%d %H:%M:%S')))

結果

<class '__main__.User'>
Apple True 2020/12/09 17:21:03

Python プロパティのgettter & setter

クラスのプロパティ getter / setter の定義は2通りある。
・@property デコレータの方法
・property関数の定義の方法

’@’が付くのを デコレータと呼ぶ? → Java に慣れしたんだ筆者はどうしても
アノテーションと呼んでしまいそうだ。
@property デコレータの方法

class User():
    def __init__(self, value=None):
        self.name = value
    @property
    def _name(self):
        return self._name
    @_name.setter
    def _name(self, value):
        self._name = value
    @_name.deleter
    def _name(self):
        del self._name

property関数の方法

class User():
    def __init__(self, value=None):
        self._name = value
    def getName(self):
        return self._name
    def setName(self, value):
        self._name = value
    name = property(getName, setName)

両方とも、以下のように実行して結果は勿論同じである。

u = User()
u.name = 'uran'
print(u.name)
# uran

しかし、、、

import json
print( json.dumps(u, default=lambda o:o.__dict__, indent=2) )

@property デコレータの方法のjson.dumps の結果

{
   "name": "uran"
}

property関数の方法のjson.dumps の結果

{
   "_name": "uran"
}

property関数の方法は、@property デコレータの方法より、記述が1行少なくて済み
メソッド名もかなり自由にできて良いかと思ったが、
上のように、jsonリアライスを実行すると、"_name" と
__init__ で定義した名称になってしまう。当たり前といえば、そうなのだが、、、
こうなると、property関数の方法のスタイルを採用するのも、
考え物である。

JSONデシリアライズで、datetime オブジェクトにする。

JSONシリアライズで、datetime を考慮するケースは、
JSONシリアライズで、datetime に注意する - Oboe吹きプログラマの黙示録
を書いたが、シリアライズする場合も datetime オブジェクトに変換する場合は以下のような
メソッドをフックとして実行させる必要がある。

import json
from datetime import datetime

def datetime_parser(value):
    if isinstance(value, dict):
        for k,v in value.items():
            value[k] = datetime_parser(v)
    elif isinstance(value, list):
        for i,e in enumerate(value):
            value[i] = datetime_parser(e)
    elif isinstance(value, str):
        try:
            value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
        except (ValueError, AttributeError):
            pass
    return value
jsonstr = '{"a":10,"b":"2020-12-09 18:21:34","c":[true,"test","2020-12-09 19:09:21",null],"d":{"e":16.4,"f":"2020-12-09 20:37:06"}}'
d = json.loads(jsonstr, object_hook=datetime_parser)

でも、これは JSON に記述された日付時刻が
常に、
'%Y-%m-%d %H:%M:%S' のフォーマットであることが前提である。

もう少し汎用的、状況に応じた strptime を実行できるようにしたい。

クラスとして用意し、インスタンス生成時に、状況に応じた strptime のフォーマットを
用意する。

class Datetimetool():
    def __init__(self):
        import re
        self._rt = [
            (
                re.compile('^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
                '%Y-%m-%d %H:%M:%S'
            ),
            (
                re.compile('^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
                '%Y-%m-%dT%H:%M:%S'
            ),
            (
                re.compile('^\d{4}/(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'),
                '%Y/%m/%d %H:%M:%S'
            ),
            (
                re.compile('^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$'),
                '%Y-%m-%d'
            ),
            (
                re.compile('^\d{4}/(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01])$'),
                '%Y/%m/%d'
            ),
        ]
    def datetime_parser(self, value):
        if isinstance(value, dict):
            for k,v in value.items():
                value[k] = self.datetime_parser(v)
        elif isinstance(value, list):
            for i,e in enumerate(value):
                value[i] = self.datetime_parser(e)
        elif isinstance(value, str):
            from datetime import datetime
            try:
                for rtt in self._rt:
                    if rtt[0].match(value):
                        value = datetime.strptime(value, rtt[1])
                        break
            except (ValueError, AttributeError):
                pass
        return value
jsonstr = '{"a":10,"b":"2020-12-09 18:21:34","c":[true,"test","2020/12/09 19:09:21",null],"d":{"e":16.4,"f":"2020/12/11"}}'
d = json.loads(jsonstr, object_hook=Datetimetool().datetime_parser)

上の datetime_parser メソッドですが、
文字列で日付時刻に変換を試す部分、以下は、

        elif isinstance(value, str):
            from datetime import datetime
            try:
                for rtt in self._rt:
                    if rtt[0].match(value):
                        value = datetime.strptime(value, rtt[1])
                        break
            except (ValueError, AttributeError):
                pass

以下のように、書いた方が良いのかもしれません。

        elif isinstance(value, str):
            from datetime import datetime
            try:
                dlst = [datetime.strptime(value, rtt[1]) for rtt in self._rt if rtt[0].match(value)]
                if len(dlst) > 0:
                    value = dlst[0]
            except (ValueError, AttributeError):
                pass

datetime への変換

日付時刻文字列から datetime オブジェクトを求めるのに、
正規表現
日付の正規表現 - Oboe吹きプログラマの黙示録

時刻正規表現 - Oboe吹きプログラマの黙示録

これらで毎回チェックしてから datetime.strptime を実行するのは面倒くさいです。
Java と違って存在しない日付も Python strptime はエラーにしてくれます。

でも、チェックも変換も両方一度にしたいです。(わがまま。。)

datetimetool.py

from datetime import datetime
import re

class Datetimetool():
    def __init__(self, format):
        self.__format = format
        self.__regex = format.replace('-', '\-')\
                .replace('%Y', '\d{4}')\
                .replace('%m', '(0[1-9]|1[012])')\
                .replace('%d', '(0[1-9]|[12][0-9]|3[01])')\
                .replace('%H', '(0[0-9]|1[0-9]|2[0-3])')\
                .replace('%M', '(0[0-9]|[0-5][0-9])')\
                .replace('%S', '(0[0-9]|[0-5][0-9])')
    @property
    def regex(self):
        return self.__regex
    def parse(self, str, errout=False):
        try:
            if re.match(self.__regex, str):
                return (True, datetime.strptime(str, self.__format))
            else:
                if errout: raise ValueError('%s is not date format %s' % (str, self.__format))
                return (False, None)
        except BaseException as e:
            if errout: raise e
            return (False, None)

実行、インスタンス生成で、%Y,%m,%d,%H,%M,%S によるフォーマットを指定します。
一見、わかりづらい長くて間違いやすい正規表現を書きません。

from datetimetool import Datetimetool

dtool = Datetimetool('parse %Y-%m-%d %H:%M:%S test')

r = dtool.parse('parse 2020-12-08 21:08:16 test', True)
print(r)
# (True, datetime.datetime(2020, 12, 8, 21, 8, 16))

タプルの[0] でチェックできます。
タプルの[1] に、datetimeが入ります。

dtool = Datetimetool('parse %Y-%m-%d test')
r = dtool.parse('parse 2020-11-31 test')
print(r)
# (False, None)
if not r[0]:
    print('NG')
# NG

エラーは、errout=True で例外発生させます

r = dtool.parse('parse 2020-11-31 test', errout=True)
# ValueError: day is out of range for month
r = dtool.parse('parse 2020-12-8 test', errout=True)
# ValueError: parse 2020-12-8 test is not date format parse %Y-%m-%d test

tuple から dict

問題:

a = [ {'name':'A','id':1103},{'name':'B','id':1104},{'name':'C','id':1105} ]

ここから

{'A': 1103, 'B': 1104, 'C': 1105}

を求めたい

values() で tuple リストを求めて、

a_tuplelist = [ tuple(v.values())  for v in a ]
# [('A', 1103), ('B', 1104), ('C', 1105)]

dict() で Dictionary にする。

adict = dict([ tuple(v.values()) for v in a ])
# {'A': 1103, 'B': 1104, 'C': 1105}