【问题标题】:How to convert to a Python datetime object with JSON.loads?如何使用 JSON.loads 转换为 Python 日期时间对象?
【发布时间】:2012-01-09 18:43:03
【问题描述】:

我有一个 JSON 对象的字符串表示形式。

dumped_dict = '{"debug": false, "created_at": "2020-08-09T11:24:20"}'

当我用这个对象调用 json.loads 时;

json.loads(dumped_dict)

我明白了;

{'created_at': '2020-08-09T11:24:20', 'debug': False}

这里没有错。但是,我想知道是否有办法将上面带有 json.loads 的对象转换成这样的:

{'created_at': datetime.datetime(2020, 08, 09, 11, 24, 20), 'debug': False}

简而言之,我们是否能够将日期时间字符串转换为实际的 datetime.datetime 对象,同时 调用 json.loads?

【问题讨论】:

  • 我也把日期作为字符串(用双引号括起来),是不是因为 json 没有日期数据类型,所以日期是字符串?
  • @Simplecode 刚刚看到了这一点,但是是的,json 实际上是相当原始的。它只知道字符串、数字、布尔值、数组和映射的类型(我想我都知道了)。所以是的,所有日期/时间都必须作为字符串传输,但日期类型也可以作为数字 - 来自时间戳。

标签: python json datetime


【解决方案1】:

到目前为止我的解决方案:

>>> json_string = '{"last_updated": {"$gte": "Thu, 1 Mar 2012 10:00:49 UTC"}}'
>>> dct = json.loads(json_string, object_hook=datetime_parser)
>>> dct
{u'last_updated': {u'$gte': datetime.datetime(2012, 3, 1, 10, 0, 49)}}


def datetime_parser(dct):
    for k, v in dct.items():
        if isinstance(v, basestring) and re.search("\ UTC", v):
            try:
                dct[k] = datetime.datetime.strptime(v, DATE_FORMAT)
            except:
                pass
    return dct

关于object_hook的进一步使用参考:JSON encoder and decoder

在我的情况下,json 字符串来自对我的 REST API 的 GET 请求。只要输入字符串符合 DATE_FORMAT 即:

DATE_FORMAT = '%a, %d %b %Y %H:%M:%S UTC'

正则表达式模式可能应该进一步完善

PS:如果您想知道,json_string 是一个 MongoDB/PyMongo 查询。

【讨论】:

  • 请提供一些反馈/建议,而不是简单的-1,这样我至少可以学到一些东西:)
  • @NicolaIarocci 看起来是一个很棒的解决方案,但这不是也迫使客户将后缀“UTC”硬编码到他们的 json 中吗?
  • 如果您不想这样做,可以删除 UTC 测试。我只是不想尝试对有效负载中的每个字符串进行日期转换(因为我有很多)。 re.search 是否更快或执行日期转换还有待观察。
  • 请注意,这只适用于字典。如果您在列表中有日期,这将不起作用。
  • @NicolaIarocci 您可以只使用.endswith(" UTC"),我还没有测试过,但我猜这会比正则表达式更快。它只有在字符串末尾才有效,但我认为这是一个优势:我不想将提到时间的故事解码为日期时间对象。
【解决方案2】:

你需要传递一个object_hook。来自documentation:

object_hook 是一个可选函数,将使用 任何对象文字解码的结果(字典)。的返回值 将使用 object_hook 代替 dict。

像这样:

import datetime
import json

def date_hook(json_dict):
    for (key, value) in json_dict.items():
        try:
            json_dict[key] = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S")
        except:
            pass
    return json_dict

dumped_dict = '{"debug": false, "created_at": "2020-08-09T11:24:20"}'
loaded_dict = json.loads(dumped_dict, object_hook=date_hook)

如果您还想处理时区,则必须使用 dateutil 而不是 strptime。

【讨论】:

  • 使用 try/catch 作为控制结构并不理想。
  • 只想指出,这将就地编辑传入的json_dict,即loaded_dict 将是一个指向与dumped_dict 相同数据的名称,因此没有意义返回json_dict 在函数中,如果这是有意的(它可能是为了 json.loads 从字符串反序列化)。如果这不是您的意图,那么您需要构建一个新字典,或者在编辑之前对函数 arg json_dict 进行深度复制。顺便说一句,try/catch 作为一种控制结构很好,关于“在你跳跃之前先看”与“请求宽恕而不是许可”之间存在激烈的争论,但两者都是有效的。
【解决方案3】:

我会按照 Nicola 的建议进行 2 处更改:

  1. 使用dateutil.parser 而不是datetime.datetime.strptime
  2. 明确定义我想要捕获的异常。我通常建议不惜一切代价避免使用空的except:

或者在代码中:

import dateutil.parser

def datetime_parser(json_dict):
    for (key, value) in json_dict.items():
        try:
            json_dict[key] = dateutil.parser.parse(value)
        except (ValueError, AttributeError):
            pass
    return json_dict

str = "{...}"  # Some JSON with date
obj = json.loads(str, object_hook=datetime_parser)
print(obj)

【讨论】:

  • 有趣的方向尝试。但是对 json 中的每个项目运行 datetime 解析看起来有点慢。大多数项目不会是日期时间值。
  • dateutil.parser.parse 确实很慢,应该是在非结构化数据中寻找日期的最后手段。如果您知道日期的格式,则可以使用 datetime.strptime。
【解决方案4】:

按照您提出问题的方式,json 没有迹象表明该字符串是日期值。这与具有示例字符串的 json 文档不同:

'{"__complex__": true, "real": 1, "imag": 2}'

这个字符串有一个指示符"__complex__": true,可以用来推断数据的类型,但是除非有这样一个指示符,一个字符串只是一个字符串,你所能做的就是通过所有的正则表达式字符串并决定它们是否看起来像日期。

在您的情况下,如果您的格式可用,您绝对应该使用架构。

【讨论】:

  • json 的哪些文档建议使用双下划线名称?例如,我见过 __type,但所有这些看起来都像是使用有限的约定。
  • 示例取自 json 包文档。
【解决方案5】:

您可以使用正则表达式来确定是否要将某个字段转换为日期时间,如下所示:

def date_hook(json_dict):
    for (key, value) in json_dict.items():
        if type(value) is str and re.match('^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d*$', value):
            json_dict[key] = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
        elif type(value) is str and re.match('^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$', value):
            json_dict[key] = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S")
        else:
            pass

    return json_dict

然后您可以在调用 json.loads() 时使用 object_hook 参数引用 date_hook 函数:

json_data = '{"token": "faUIO/389KLDLA", "created_at": "2016-09-15T09:54:20.564"}'
data_dictionary = json.loads(json_data, object_hook=date_hook)

【讨论】:

    【解决方案6】:

    虽然它在技术上只是为了提供对象挂钩功能,但我建议使用 JSONDecoder 的适当子类,因为它是框架开发人员的意图:

    class _JSONDecoder(json.JSONDecoder):
        def __init__(self, *args, **kwargs):
            json.JSONDecoder.__init__(
                self, object_hook=self.object_hook, *args, **kwargs)
    
        def object_hook(self, obj):
            ret = {}
            for key, value in obj.items():
                if key in {'timestamp', 'whatever'}:
                    ret[key] = datetime.fromisoformat(value) 
                else:
                    ret[key] = value
            return ret
    

    为了完整起见,这里是解码器的对应物,自定义 JSONEncoder:

    class _JSONEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj, (datetime.date, datetime.datetime, pd.Timestamp)):
                return obj.isoformat()
            return json.JSONEncoder.default(obj)
    

    两者的实际效果如下:

    json_str = json.dumps({'timestamp': datetime.datetime.now()}, cls=_JSONEncoder)
    d = json.loads(json_str, cls=_JSONDecoder)
    

    【讨论】:

    • 正是我需要的,非常感谢
    【解决方案7】:

    该方法实现了日期时间格式的递归字符串搜索

    import json
    from dateutil.parser import parse
    
    def datetime_parser(value):
        if isinstance(value, dict):
            for k, v in value.items():
                value[k] = datetime_parser(v)
        elif isinstance(value, list):
            for index, row in enumerate(value):
                value[index] = datetime_parser(row)
        elif isinstance(value, str) and value:
            try:
                value = parse(value)
            except (ValueError, AttributeError):
                pass
        return value
    
    json_to_dict = json.loads(YOUR_JSON_STRING, object_hook=datetime_parser)
    

    【讨论】:

    • 你能解释一下吗?谢谢!
    【解决方案8】:

    据我所知,没有现成的解决方案。

    首先,解决方案应该考虑json schema,以正确区分字符串和日期时间。在某种程度上,您可以使用 json schema inferencer(google for json schema inferencer github)猜测 schema,然后修复真正日期时间的地方。

    如果架构是已知的,那么创建一个函数应该很容易,它解析 json 并用 datetime 替换字符串表示。代码的一些灵感也许可以从validictory 产品中找到(并且 json 模式验证也可能是个好主意)。

    【讨论】:

      【解决方案9】:

      受 Nicola 的 answer 启发并适应 python3(str 代替 basestring):

      import re
      from datetime import datetime
      datetime_format = "%Y-%m-%dT%H:%M:%S"
      datetime_format_regex = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$')
      
      
      def datetime_parser(dct):
          for k, v in dct.items():
              if isinstance(v, str) and datetime_format_regex.match(v):
                  dct[k] = datetime.strptime(v, datetime_format)
          return dct
      

      这避免了使用 try/except 机制。 关于OP的测试代码:

      >>> import json
      >>> json_string = '{"debug": false, "created_at": "2020-08-09T11:24:20"}'
      >>> json.loads(json_string, object_hook=datetime_parser)
      {'created_at': datetime.datetime(2020, 8, 9, 11, 24, 20), 'debug': False}
      

      regex 和 datetime_format 变量可以很容易地适应其他模式,例如中间没有 T。

      要将保存为 isoformat(因此以微秒存储)的字符串转换回日期时间对象,请参阅this question。

      【讨论】:

        【解决方案10】:

        建议创建 JSON 编码器和解码器的解决方案都是完全有效的。我唯一能看到的错误是对性能的轻微影响,如果您正在扫描每个 JSON 值以检查是否与日期/时间格式匹配,则可能会发生这种情况。

        这是我将采用的方法,使用 dataclass-wizard 库(注意:它实际上是为 API 响应而设计的)

        1. 使用随附的 CLI 实用程序将 JSON 响应转换为数据类架构。请注意,debug 的值在此处被编码为字符串,因此我将传递 -f 以便它强制解析为 Python bool 类型。否则,它应该显示为Union[bool, str],这是默认的推断类型。

          $ echo '{"debug": "false", "created_at": "2020-08-09T11:24:20"}' | wiz gs -f
          

          输出,包括顶部的导入(未显示):

          @dataclass
          class Data(JSONWizard):
              """
              Data dataclass
          
              """
              debug: bool
              created_at: datetime
          
        2. 现在我们可以将上面的示例 JSON 字符串反序列化为 Data 对象。注意 created_at 应该是 datetime 类型。与debug 的值类似,它应该被解码为bool。

          string = """{"debug": "false", "created_at": "2020-08-09T11:24:20"}"""
          
          c = Data.from_json(string)
          
          print(repr(c))
          
        3. 将其序列化回 JSON。 datetime 对象应该被转换回来
          一个字符串:

          print(c.to_json())
          # {"debug": false, "createdAt": "2020-08-09T11:24:20"}
          

        【讨论】:

        • Pydantic也支持这个,用来帮助构建fastapi的解析库
        • 是的,同意,如果您还需要数据验证,pydantic 是一个不错的选择。
        【解决方案11】:

        在大多数情况下,这是一个双向问题,如果您使用自定义编码器,您可能需要自定义解码器(反之亦然)。在这种情况下,解码器应该能够解析编码数据并返回原始 json 对象。

        下面有一个完整的练习,使用 2 种不同的策略将 python 不可序列化的对象转换为 json:

        1. 修补 JSONEncoder 类以序列化任何实现“json”方法以序列化类的类。
        2. 使用“转换器”方法列表来序列化特定的 Python 类型。

        在下面的示例中,我使用自定义 json 方法将 Enum 类序列化为 {enum.name: enum.value} dict,这里 enun.value 对象在 python 中是不可序列化的类型(日期和元组),通过使用列出的 CONVERTERS 方法,我们可以将这些类型转换为可序列化类型。

        编码后,可以调用 custom_json_decoder 方法将该 json 转换回 python 原始类型。 下面这个脚本示例是完整的,它应该“按原样”运行:

        from enum import Enum
        from dateutil.parser import parse as dtparse
        from datetime import datetime
        from datetime import date
        from json import JSONEncoder
        from json import loads as json_loads
        from json import dumps as json_dumps
        
        
        def wrapped_default(self, obj):
            json_parser = getattr(obj.__class__, "__json__", lambda x: x.__dict__)
            try:
                return json_parser(obj)
            except Exception:
                return wrapped_default.default(obj)
        
        
        wrapped_default.default = JSONEncoder().default
        JSONEncoder.default = wrapped_default
        
        CONVERTERS = {
            "datetime": dtparse,
            "date": lambda x: datetime.strptime(x, "%Y%m%d").date(),
            "tuple": lambda x: tuple(x),
        }
        
        
        class RskJSONEncoder(JSONEncoder):
            def default(self, obj):
                if isinstance(obj, date):
                    return {"val": obj.strftime("%Y%m%d"), "pythontype": "date"}
                elif isinstance(obj, datetime):
                    return {"val": obj.isoformat(), "pythontype": "datetime"}
                elif isinstance(obj, tuple):
                    return {"val": list(obj), "pythontype": "tuple"}
                return super().default(obj)
        
        
        def custom_json_decoder(obj):
            def json_hook(json_obj):
                try:
                    return CONVERTERS[json_obj.pop("pythontype")](json_obj["val"])
                except Exception:
                    res = json_obj
                return res
        
            return json_loads(obj, object_hook=json_hook)
        
        
        def custom_json_encoder(obj):
            return json_dumps(obj, cls=RskJSONEncoder)
        
        
        if __name__ == "__main__":
        
            class Test(Enum):
                A = date(2021, 1, 1)
                B = ("this", " is", " a", " tuple")
        
                def __json__(self):
                    return {self.name: self.value}
        
            d = {"enum_date": Test.A, "enum_tuple": Test.B}
            this_is_json = custom_json_encoder(d)
            this_is_python_obj = custom_json_decoder(this_is_json)
            print(f"this is json, type={type(this_is_json)}\n", this_is_json)
            print(
                f"this is python, type={type(this_is_python_obj)}\n",
                this_is_python_obj,
            )
        

        【讨论】:

          猜你喜欢
          • 2011-12-02
          • 2010-12-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-01-08
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多