Python和JavaScript之间的JSON日期时间格式转换问题

413

我想使用JSON在Python中以序列化的形式发送datetime.datetime对象,并在JavaScript中使用JSON进行反序列化。如何最好地实现这一点?


你更喜欢使用库还是自己编写代码? - guettli
13个回答

377
你可以在json.dumps中添加'default'参数来处理这个问题:
date_handler = lambda obj: (
    obj.isoformat()
    if isinstance(obj, (datetime.datetime, datetime.date))
    else None
)
json.dumps(datetime.datetime.now(), default=date_handler)
'"2010-04-20T20:08:21.634121"'

日期格式为ISO 8601格式。

一个更全面的默认处理程序函数:

def handler(obj):
    if hasattr(obj, 'isoformat'):
        return obj.isoformat()
    elif isinstance(obj, ...):
        return ...
    else:
        raise TypeError, 'Object of type %s with value of %s is not JSON serializable' % (type(obj), repr(obj))

更新:还添加了类型和值的输出。
更新:还处理了日期。


11
问题在于,如果列表/字典中存在其他对象,这段代码将会将它们转换为None。 - Tomasz Wysocki
5
json.dumps也不知道如何转换它们,但是异常被抑制了。遗憾的是,一行lambda修复也有其缺点。如果你更愿意在未知情况下引发异常(这是一个好主意),请使用我上面添加的函数。 - JT.
9
完整的输出格式应该包含时区信息,而 isoformat() 方法无法提供这个功能。因此,在返回字符串之前,您应该确保将该信息附加到字符串中。 - Nicholas Franceschina
16
lambda函数可以根据需要调用非datetime类型的基本实现,因此如果需要,可以引发TypeError异常。以下是翻译后的内容: “lambda函数可以适应非datetime类型并调用基本实现,以便在需要时引发TypeError异常: dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime) else json.JSONEncoder().default(obj)。” - Pascal Bourque
3
你的 lambda 函数可以进行轻微简化:将 or 替换为 if isinstance(obj, (datetime.datetime, datetime.date)) 即可。这样做不会改变原有意思,但能让代码更加易懂。 - hobs
显示剩余8条评论

89
对于跨语言项目,我发现包含RFC 3339日期的字符串是最好的选择。RFC 3339日期的格式如下:
  1985-04-12T23:20:50.52Z

我认为大部分的格式都是显而易见的。唯一有些不寻常的可能是末尾的“Z”。它代表的是GMT/UTC。你也可以添加一个时区偏移,比如+02:00表示CEST(德国夏季时间)。我个人更喜欢将所有时间都保持在UTC,直到需要显示出来为止。
对于显示、比较和存储,你可以在所有语言中都保持字符串格式。如果需要进行计算,大多数语言都可以将其轻松转换回本地日期对象。
所以,生成JSON的方式如下:
  json.dump(datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ'))

很不幸,JavaScript的Date构造函数不接受RFC 3339字符串,但是互联网上有很多解析器可供使用。
huTools.hujson试图处理Python代码中可能遇到的最常见的编码问题,包括正确处理日期/时间对象和时区。

17
这个日期格式化机制在datetime模块中具有本地支持:datetime.isoformat()同时,在 simplejson 中也同样支持将datetime对象默认转换成isoformat字符串进行导出。无需手动使用strftime函数调整格式。 - jrk
9
我无法自动将datetime对象转换为isoformat字符串。 对于我来说,simplejson.dumps(datetime.now())会产生TypeError: datetime.datetime(...)不可JSON序列化的错误。 - kostmo
7
json.dumps(datetime.datetime.now().isoformat()) 是魔法发生的地方。 - jathanism
2
simplejson 的美妙之处在于,如果我有一个复杂的数据结构,它会解析并将其转换为 JSON。如果我必须为每个 datetime 对象执行 json.dumps(datetime.datetime.now().isoformat()),那么我就会失去这个功能。有没有什么办法可以解决这个问题? - andrewrk
1
那个Z代表格林威治时间(GMT/UTC)的军事无线电密码,也叫做祖鲁时间:http://zh.wikipedia.org/wiki/%E5%86%9B%E4%B8%9A%E6%97%B6%E9%97%B4%E5%8C%BA - Daniel J. Pritchett
显示剩余3条评论

73
我已经解决了。
假设你有一个使用datetime.now()创建的Python datetime对象d,它的值是:
datetime.datetime(2011, 5, 25, 13, 34, 5, 787000)

你可以将它序列化为 JSON,作为一个 ISO 8601 的日期时间字符串:
import json
json.dumps(d.isoformat())

示例的日期时间对象将被序列化为:
'"2011-05-25T13:34:05.787000"'

这个值一旦在JavaScript层接收到,就可以构造一个Date对象。
var d = new Date("2011-05-25T13:34:05.787000");

从JavaScript 1.8.5版本开始,Date对象拥有一个toJSON方法,该方法以标准格式返回一个字符串。因此,要将上述JavaScript对象序列化为JSON,命令如下:
d.toJSON()

这将给你带来什么?
'2011-05-25T20:34:05.787Z'

这个字符串一旦在Python中接收到,可以被反序列化为一个日期时间对象:
datetime.strptime('2011-05-25T20:34:05.787Z', '%Y-%m-%dT%H:%M:%S.%fZ')

这将导致以下的日期时间对象,它与你开始的那个是相同的,因此是正确的:
datetime.datetime(2011, 5, 25, 20, 34, 5, 787000)

6
时区问题会很麻烦。我们假设你在 Python 中使用的是协调世界时(UTC)(只有疯子才会使用其他时间),Python 的 JSON 输出没有时区,因此 JavaScript 会将其解释为本地时区。JavaScript 的 d.toJSON 会再次转换为 UTC。因此,对于你的示例日期(2011-04-25),在英国的浏览器上(夏令时为 UTC+1),Python 输出为13:34 - JS 将其解释为本地时区或 UTC 12:34 - JS 再输出为 UTC,所以为12:34。而 Python 则会将其解释为12:34。你已经丢失了一小时(如果你只处理日期而不考虑时间,则可能会丢失一整天)。除非是在冬季。 - Ryan

52

使用json,您可以子类化JSONEncoder并覆盖default()方法,以提供自己的自定义序列化器:

import json
import datetime

class DateTimeJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.isoformat()
        else:
            return super(DateTimeJSONEncoder, self).default(obj)

然后,你可以这样调用它:

>>> DateTimeJSONEncoder().encode([datetime.datetime.now()])
'["2010-06-15T14:42:28"]'

7
小幅改进-使用obj.isoformat()。您还可以使用更常见的 dumps() 调用,它接受其他有用的参数(例如indent): simplejson.dumps(myobj, cls=JSONEncoder, ...) - rcoup
3
因为那会调用JSONEncoder的父级方法,而不是DateTimeJSONEncoder的父级方法。也就是说,你将向上跳两个层级。 - Brian Arsuaga

31
这里有一个相当完整的解决方案,可以使用标准库的json模块对datetime.datetime和datetime.date对象进行递归编码和解码。这需要Python >= 2.6,因为datetime.datetime.strptime()格式字符串中的%f格式代码仅在此之后才受支持。对于Python 2.5的支持,去掉%f并从ISO 8601日期字符串中去掉微秒,然后再尝试转换,但是你会失去微秒的精度。为了与其他来源的ISO 8601日期字符串(可能包含时区名称或UTC偏移量)的互操作性,您可能还需要在转换之前去掉日期字符串的某些部分。对于ISO 8601日期字符串(以及许多其他日期格式)的完整解析器,请参阅第三方dateutil模块。
只有当ISO 8601日期字符串是JavaScript文字对象符号或对象内嵌结构中的值时,解码才有效。ISO 8601日期字符串作为顶级数组的项将不会被解码。
也就是说,这样可以工作:
date = datetime.datetime.now()
>>> json = dumps(dict(foo='bar', innerdict=dict(date=date)))
>>> json
'{"innerdict": {"date": "2010-07-15T13:16:38.365579"}, "foo": "bar"}'
>>> loads(json)
{u'innerdict': {u'date': datetime.datetime(2010, 7, 15, 13, 16, 38, 365579)},
u'foo': u'bar'}

还有这个:
>>> json = dumps(['foo', 'bar', dict(date=date)])
>>> json
'["foo", "bar", {"date": "2010-07-15T13:16:38.365579"}]'
>>> loads(json)
[u'foo', u'bar', {u'date': datetime.datetime(2010, 7, 15, 13, 16, 38, 365579)}]

但是这并不像预期的那样起作用:
>>> json = dumps(['foo', 'bar', date])
>>> json
'["foo", "bar", "2010-07-15T13:16:38.365579"]'
>>> loads(json)
[u'foo', u'bar', u'2010-07-15T13:16:38.365579']

这是代码:
__all__ = ['dumps', 'loads']

import datetime

try:
    import json
except ImportError:
    import simplejson as json

class JSONDateTimeEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime.date, datetime.datetime)):
            return obj.isoformat()
        else:
            return json.JSONEncoder.default(self, obj)

def datetime_decoder(d):
    if isinstance(d, list):
        pairs = enumerate(d)
    elif isinstance(d, dict):
        pairs = d.items()
    result = []
    for k,v in pairs:
        if isinstance(v, basestring):
            try:
                # The %f format code is only supported in Python >= 2.6.
                # For Python <= 2.5 strip off microseconds
                # v = datetime.datetime.strptime(v.rsplit('.', 1)[0],
                #     '%Y-%m-%dT%H:%M:%S')
                v = datetime.datetime.strptime(v, '%Y-%m-%dT%H:%M:%S.%f')
            except ValueError:
                try:
                    v = datetime.datetime.strptime(v, '%Y-%m-%d').date()
                except ValueError:
                    pass
        elif isinstance(v, (dict, list)):
            v = datetime_decoder(v)
        result.append((k, v))
    if isinstance(d, list):
        return [x[1] for x in result]
    elif isinstance(d, dict):
        return dict(result)

def dumps(obj):
    return json.dumps(obj, cls=JSONDateTimeEncoder)

def loads(obj):
    return json.loads(obj, object_hook=datetime_decoder)

if __name__ == '__main__':
    mytimestamp = datetime.datetime.utcnow()
    mydate = datetime.date.today()
    data = dict(
        foo = 42,
        bar = [mytimestamp, mydate],
        date = mydate,
        timestamp = mytimestamp,
        struct = dict(
            date2 = mydate,
            timestamp2 = mytimestamp
        )
    )

    print repr(data)
    jsonstring = dumps(data)
    print jsonstring
    print repr(loads(jsonstring))

如果您打印日期,例如 datetime.datetime.utcnow().isoformat()[:-3]+"Z",它将与 JavaScript 中的 JSON.stringify() 生成的完全相同。 - w00t

24
如果你确定只有 JavaScript 会使用这个 JSON 数据,我更倾向于直接传递 JavaScript 的 Date 对象。
datetime 对象上的 ctime() 方法会返回一个字符串,JavaScript 的 Date 对象可以理解。
import datetime
date = datetime.datetime.today()
json = '{"mydate":new Date("%s")}' % date.ctime()

JavaScript会愉快地将其作为对象字面量使用,并且您已经拥有了内置的Date对象。

12
从技术上讲,它不是有效的JSON格式,但它是一个有效的JavaScript对象字面量。(为了原则起见,我会将Content-Type设置为text/javascript而不是application/json。)如果消费者始终且永远只有JavaScript实现,那么这非常优雅。我会使用它。 - system PAUSE
13
.ctime() 不是一个很好的传递时间信息的方式,.isoformat() 更好。.ctime() 会像时区和夏令时不存在一样舍弃这些信息。 这个函数应该被废除。 - Evgeny
多年以后:请不要考虑这样做。这只有在您使用JavaScript中的eval()函数来解析JSON时才能正常工作,但您真的不应该这样做... - domenukk

12
一个非常简单的解决方案是修补json模块的默认设置。
例如:
import json
import datetime

json.JSONEncoder.default = lambda self,obj: (obj.isoformat() if isinstance(obj, datetime.datetime) else None)

现在,你可以像一直支持datetime一样使用json.dumps()...
json.dumps({'created':datetime.datetime.now()})

这样做是有道理的,如果您需要这个扩展始终生效,并且希望不改变您或他人在JSON序列化方面的使用方式(无论是在现有代码中还是其他地方)。
请注意,有些人可能认为以这种方式修补库是不好的做法。
如果您希望以多种方式扩展应用程序,则需要特别注意。在这种情况下,我建议使用by ramenJT的解决方案,并在每种情况下选择适当的JSON扩展。

6
这段代码会悄无声息地吞噬非序列化对象,并将它们转换成None。你可能希望改为抛出异常。 - Blender

7

除了时间戳,我没有其他要补充社区维基的回答!

Javascript使用以下格式:

new Date().toJSON() // "2016-01-08T19:00:00.123Z"

Python 方面(对于json.dumps处理程序,请参见其他答案):

>>> from datetime import datetime
>>> d = datetime.strptime('2016-01-08T19:00:00.123Z', '%Y-%m-%dT%H:%M:%S.%fZ')
>>> d
datetime.datetime(2016, 1, 8, 19, 0, 0, 123000)
>>> d.isoformat() + 'Z'
'2016-01-08T19:00:00.123000Z'

如果你省略掉那个 Z,就会导致像 Angular 这样的前端框架无法在浏览器本地时区中显示日期:

> $filter('date')('2016-01-08T19:00:00.123000Z', 'yyyy-MM-dd HH:mm:ss')
"2016-01-08 20:00:00"
> $filter('date')('2016-01-08T19:00:00.123000', 'yyyy-MM-dd HH:mm:ss')
"2016-01-08 19:00:00"

4

在Python端:

import time, json
from datetime import datetime as dt
your_date = dt.now()
data = json.dumps(time.mktime(your_date.timetuple())*1000)
return data # data send to javascript

在 JavaScript 方面:
var your_date = new Date(data)

数据来自Python的结果


4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接