Python JSON编码器支持日期时间?

Posted

技术标签:

【中文标题】Python JSON编码器支持日期时间?【英文标题】:Python JSON encoder to support datetime? 【发布时间】:2012-08-20 18:17:24 【问题描述】:

有没有什么优雅的方法可以让 Python JSON 编码器支持日期时间?一些第 3 方模块或简单的 hack?

我正在使用 tornado 的数据库包装器从 db 中获取一些行以生成 json。查询结果包含一个常规的 mysql 时间戳列。

很烦人的是 Python 的默认 json 编码器不支持它自己的 datetime 类型,这在各种数据库查询中都很常见。

我不想修改 Python 自己的 json 编码器。有什么好的做法吗?非常感谢!

ps:我通过修改 Python JSON 编码器默认方法发现了一个肮脏的 hack:

变化:

def default(self, o):
    raise TypeError(repr(o) + " is not JSON serializable")

收件人:

def default(self, o):
    from datetime import date
    from datetime import datetime
    if isinstance(o, datetime):
        return o.isoformat()
    elif isinstance(o, date):
        return o.isoformat()
    else:
        raise TypeError(repr(o) + " is not JSON serializable")

好吧,这将是仅适用于开发环境的临时解决方案。

但是对于长期的解决方案或生产环境,这很丑陋,每次部署到新服务器时都必须进行修改。

有没有更好的方法?我不想修改 Python 代码本身,也不想修改 Tornado 源代码。我可以用我自己的项目代码做些什么来实现这一点吗?最好是一步到位。

非常感谢!

【问题讨论】:

见:***.com/questions/455580/… 子类方法的问题,是它对于所有其他json编码的使用都失败了,比如simple django, "dumpdata" 【参考方案1】:

json.dumps(thing, default=str)

【讨论】:

如果您认为这符合问题中提到的“easy hack”的定义,请点赞。 我想这应该是正确的答案,不知道这个答案有什么问题? 我觉得很有趣。 5 年后,这仍然是解决这个问题的最简单和 Pythonic 的方法,但这个答案仍然被低估了。 thingDict 时,这实际上保留了float 和int 值。 你宁愿同意一些常见的格式并在你传递给default的某个函数中使用strftime【参考方案2】:

The docs suggest 继承 JSONEncoder 并实现您自己的默认方法。好像你基本上在那里,而且它不是一个“肮脏的黑客”。

默认编码器不处理日期的原因是 JSON 中没有标准的日期表示。 Some people 使用的是 /Date(1198908717056)/ 格式,但我个人更喜欢 ISO 格式。

import json
import datetime


class DateTimeEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
            return obj.isoformat()
        elif isinstance(obj, datetime.timedelta):
            return (datetime.datetime.min + obj).time().isoformat()

        return super(DateTimeEncoder, self).default(obj)

now = datetime.datetime.now()
encoder = DateTimeEncoder()
encoder.encode("datetime": now, "date": now.date(), "time": now.time())
> "datetime": "2019-07-02T16:17:09.990126", "date": "2019-07-02", "time": "16:17:09.990126"

【讨论】:

您可以将if 和第一个elif 组合在一起,因为您对return obj.isoformat() 执行相同的操作并且isinstance 支持元组:isinstance(obj, (datetime.datetime, datetime.date))跨度> 这对我有用,我在现有的自定义 JSON 编码器中添加了额外的检查 timedelta 编码对于超过(或等于)24 小时的值会失败。例如。对于timedelta(hours=24),编码值为00:00:00(0 小时),对于timedelta(hours=25),编码值为01:00:00(1 小时)。 @cod3monk3y 如果您的时间增量超过 24 小时,请使用 total_seconds 方法,或进行手动字符串格式化。 isoformat 对于 stort 来说是一个简单的技巧。【参考方案3】:

我为我的项目制作了自己的课程:

import datetime
import decimal
import json
import sys

class EnhancedJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            ARGS = ('year', 'month', 'day', 'hour', 'minute',
                     'second', 'microsecond')
            return '__type__': 'datetime.datetime',
                    'args': [getattr(obj, a) for a in ARGS]
        elif isinstance(obj, datetime.date):
            ARGS = ('year', 'month', 'day')
            return '__type__': 'datetime.date',
                    'args': [getattr(obj, a) for a in ARGS]
        elif isinstance(obj, datetime.time):
            ARGS = ('hour', 'minute', 'second', 'microsecond')
            return '__type__': 'datetime.time',
                    'args': [getattr(obj, a) for a in ARGS]
        elif isinstance(obj, datetime.timedelta):
            ARGS = ('days', 'seconds', 'microseconds')
            return '__type__': 'datetime.timedelta',
                    'args': [getattr(obj, a) for a in ARGS]
        elif isinstance(obj, decimal.Decimal):
            return '__type__': 'decimal.Decimal',
                    'args': [str(obj),]
        else:
            return super().default(obj)


class EnhancedJSONDecoder(json.JSONDecoder):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, object_hook=self.object_hook,
                         **kwargs)

    def object_hook(self, d): 
        if '__type__' not in d:
            return d
        o = sys.modules[__name__]
        for e in d['__type__'].split('.'):
            o = getattr(o, e)
        args, kwargs = d.get('args', ()), d.get('kwargs', )
        return o(*args, **kwargs)

if __name__ == '__main__':
    j1 = json.dumps('now': datetime.datetime.now(),
        'val': decimal.Decimal('9.3456789098765434987654567'),
        cls=EnhancedJSONEncoder)
    print(j1)
    o1 = json.loads(j1, cls=EnhancedJSONDecoder)
    print(o1)

结果:

"val": "args": ["9.3456789098765434987654567"], "__type__": "decimal.Decimal", "now": "args": [2014, 4, 29, 11, 44, 57, 971600], "__type__": "datetime.datetime"
'val': Decimal('9.3456789098765434987654567'), 'now': datetime.datetime(2014, 4, 29, 11, 44, 57, 971600)

参考资料:

json Documentation Mark Hildreth -- Subclassing JSONEncoder and JSONDecoder Cédric Krier -- trytond.protocols.jsonrpc source code

注意:通过将类型作为键和参数、kwargs 作为值的自定义字典传递给编码器的__init__() 并在default() 方法中使用它(或默认字典),可以使其更加灵活。

【讨论】:

迄今为止的最佳答案。当我试图了解如何反序列化 UUID 时,它会有所帮助。太好了! 很高兴您对此感到满意。我自己用过几次。【参考方案4】:
json.dumps(r, default=lambda o: o.isoformat() if hasattr(o, 'isoformat') else o)

【讨论】:

虽然这段代码 sn-p 可以解决问题,但including an explanation 确实有助于提高帖子的质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。【参考方案5】:

Tryton 项目为 datetime.datetimedatetime.datedatetime.time 对象(与其他对象)提供了 JSONEncoder 实现。它用于服务器和客户端之间的 JSON RPC 通信。

见http://hg.tryton.org/2.4/trytond/file/ade5432ac476/trytond/protocols/jsonrpc.py#l53

【讨论】:

【参考方案6】:

创建自定义解码器/编码器:

class CustomJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return http_date(obj)
        if isinstance(obj, uuid.UUID):
            return str(obj)
        return json.JSONEncoder.default(self, obj)

class CustomJSONDecoder(json.JSONDecoder):
    def __init__(self, *args, **kwargs):
        json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)

    def object_hook(self, source):
        for k, v in source.items():
            if isinstance(v, str):
                try:
                    source[k] = datetime.datetime.strptime(str(v), '%a, %d %b %Y %H:%M:%S %Z')
                except:
                    pass
        return source

【讨论】:

【参考方案7】:

将 datetime 类型转换为 unix 时间戳,然后将内容编码为 json。

例如: http://codepad.org/k3qF09Kr

【讨论】:

你的意思是改变 Python 的默认日期时间类型?怎么做?会不会太冒险?它会破坏东西吗? @horacex 不,只需修改来自 tornado 数据库包装器的结果集。【参考方案8】:

我建议使用ujson 包或orjson 包。

它们速度更快,并且仍然支持多种复杂类型。

【讨论】:

ujson 对我不起作用(至少不是开箱即用)。我得到了:TypeError: datetime.datetime(2020, 4, 18, 17, 50, 42, 418419, tzinfo=datetime.timezone(datetime.timedelta(seconds=10800))) is not JSON serializable ujson 本身不会序列化 datetime.datetime。然而,orjson 成功了。 orjson 还序列化了其他常用类型,如uuid.UUID【参考方案9】:

只需创建一个自定义编码器

(Cole 回答的一个小而重要的补充是处理 pd.NaT(或空/空时间戳值),因为如果不添加,您将获得非常奇怪的 NaT/缺少时间戳数据的时间戳转换)

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if pd.isnull(obj):
            return None
        elif isinstance(obj, datetime):
            return obj.isoformat()
        elif isinstance(obj, date):
            return obj.isoformat()
        elif isinstance(obj, timedelta):
            return (datetime.min + obj).time().isoformat()
        else:
            return super(CustomEncoder, self).default(obj)

然后用它来编码一个数据帧:

df_as_dict = df.to_dict(outtype = 'records')  # transform to dict

df_as_json = CustomEncoder().encode(df_as_dict) #transform to json

由于编码器对数据进行了标准化,因此常规解码器可以很好地将其转换回数据帧:

result_as_dict = json.JSONDecoder().decode(df_as_json) # decode back to dict

result_df = pd.DataFrame(result)  # transform dict back to dataframe

当然,如果您在编码之前将数据帧放入更大的字典中,这也将起作用,例如

input_dict = 'key_1':val_1,'key_2':val_2,...,'df_as_dict':df_as_dict
input_json = CustomEncoder().encode(input_dict)
input_json_back_as_dict = json.JSONDecoder().decode(input_json)
input_df_back_as_dict = input_json_back_as_dict['df_as_dict']
input_df_back_as_df = pd.DataFrame(input_df_back_as_dict)

【讨论】:

以上是关于Python JSON编码器支持日期时间?的主要内容,如果未能解决你的问题,请参考以下文章

尝试构建 JSON 文件,但将日期时间错误连接到字符串

JSON 意外语法错误 - 日期解析/编码/解码

将 JSON 日期字符串转换为 Python 日期时间

关于json编码时间格式的转换

为啥python中日期时间对象的json序列化不能为日期时间对象开箱即用

Python 和 JavaScript 之间的 JSON 日期时间