将时间转化为时间戳输出给前端,可以减少前端工作量,毕竟从时间戳转换为本地时间很简单,也减少了时区转换的麻烦。但是DRF本身是没有提供时间戳输出选项的,准确的是python的strftime并没有提供标准的时间戳输出格式,但是%s作为GNU 扩展的时间输出格式,因此DRF的DATETIME_FORMAT可以设置为’%s’,能够输出时间戳字符串(注意,不是数字,是字符串),但是不要高兴的太早,如果不是UTC时间,那么这样设置是有问题的。

在最开始并没有注意输出的时间戳值有什么不对,直到前端的小伙伴跟我反应我传给他的时间戳与他POST给我的不一样啊,差了8个小时,还不是比本地时间少了8个小时,而是多出了8个小时,这就很奇怪了,赶紧上数据库里边瞅一眼,发现时间以及时区都是正确的(Django托管的Postgresql 只存储UTC时间,读出来的时候会自动转换为本地时间),那么肯定就是’%s’这个时间格式化出的幺蛾子了。

查看DRF的DateTimeField的源码,发现其to_representaton函数并没有做特殊处理,所以问题所在应该是python的datetime的strftime方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def to_representation(self, value):
if not value:
return None

output_format = getattr(self, 'format', api_settings.DATETIME_FORMAT)

if output_format is None or isinstance(value, six.string_types):
return value

value = self.enforce_timezone(value)

if output_format.lower() == ISO_8601:
value = value.isoformat()
if value.endswith('+00:00'):
value = value[:-6] + 'Z'
return value
return value.strftime(output_format)

继续跟踪发现,datetime的strftime传入的参数是timetuple,问题也就在这里,根据Python官方文档说明,timetuple()返回的是本地时间元组,丢失了时区信息的,看下面的例子。

1
2
3
def strftime(self, fmt):
"Format using strftime()."
return _wrap_strftime(self, fmt, self.timetuple())

date.timetuple()
Return a time.struct_time such as returned by time.localtime().

在Django shell 中测试,可以看到 lt == t,是同一时间,但他们的timetuple()不等,是各自的本地时间,’%s’格式化输出后也是不等的,CST时间要比UTC时间早8个小时,时间戳也就大出8个小时的秒数,但如果直接调用他们但timestamp()方法,则会发现其结果是一样的,即同一时刻,各个时区的时间戳都是一样的。 datetime还有一个utctimetuple()方法,从名字可以看出其返回的是UTC timetuple,可以肯定,tlt的该方法调用结果是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
In [201]: t
Out[201]: datetime.datetime(2019, 1, 17, 1, 43, 19, 300000, tzinfo=<UTC>)

In [202]: lt = timezone.localtime(t)

In [203]: lt == t
Out[203]: True

In [204]: t.timetuple()
Out[204]: time.struct_time(tm_year=2019, tm_mon=1, tm_mday=17, tm_hour=1, tm_min=43, tm_sec=19, tm_wday=3, tm_yday=17, tm_isdst=0)

In [205]: lt.timetuple()
Out[205]: time.struct_time(tm_year=2019, tm_mon=1, tm_mday=17, tm_hour=9, tm_min=43, tm_sec=19, tm_wday=3, tm_yday=17, tm_isdst=0)

In [206]: t.strftime('%s')
Out[206]: '1547689399'

In [207]: lt.strftime('%s')
Out[207]: '1547718199'

In [208]: t.timestamp()
Out[208]: 1547689399.3

In [209]: lt.timestamp()
Out[209]: 1547689399.3

In [211]: t.utctimetuple()
Out[211]: time.struct_time(tm_year=2019, tm_mon=1, tm_mday=17, tm_hour=1, tm_min=43, tm_sec=19, tm_wday=3, tm_yday=17, tm_isdst=0)

In [212]: lt.utctimetuple()
Out[212]: time.struct_time(tm_year=2019, tm_mon=1, tm_mday=17, tm_hour=1, tm_min=43, tm_sec=19, tm_wday=3, tm_yday=17, tm_isdst=0)

在Python 的文档中也说明了,Python给出的格式化字符都是标准C覆盖的,各系统平台都可无差异运行,但是其他C平台扩展的也可以运行,能否支持就看Python的底层环境了。

The full set of format codes supported varies across platforms, because Python calls the platform C library’s strftime() function, and platform variations are common. To see the full set of format codes supported on your platform, consult the strftime(3) documentation.

The following is a list of all the format codes that the C standard (1989 version) requires, and these work on all platforms with a standard C implementation. Note that the 1999 version of the C standard added additional format codes.

找到了原因,也要给出解决方案,既然“标准”的格式不能直接输出时间戳,那么就自定义一个DateTimeField好了,在Serializer中使用自定义的DateTimeField来代替默认的DRF的DateTimeField即可得到时间戳,当然,如果有多个Model都有时间字段,我们可以直接继承自ModelSerializer,修改他的serializer_field_mapping,将时间类型的序列化字段类型修改为我们自定义的字段即可,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from rest_framework import serializers

class DateTimeField(serializers.DateTimeField):
# custom field
def to_representation(self, value):
return int(value.timestamp())

class ModelSerializer(serializers.ModelSerializer):
# modify default serializer_field_mapping
serializer_field_mapping = serializers.ModelSerializer.serializer_field_mapping
serializer_field_mapping[models.DateTimeField] = DateTimeField

class SampleModelSerializer(ModelSerializer): # our own ModelSerializer, not drf's serialiers.ModelSerializer
pass
# ...