Python的时区会让很多人困惑。我就曾经在Alpine的Docker容器中使用Django时遇到时区总是UTC导致了某些情况下日期格式化时产生了相差一天的问题。
这篇记录尽量通过详细的说明来解释过程中的所有细节问题。希望读者能够由此理解Python的时区管理以及Django的时区机制。
首先说一下Alpine Docker镜像中的时区
在Python的官方镜像中python:alpine
没有设置时区,缺省是标准时区UTC
。
# date
Sat Nov 9 05:25:09 UTC 2019
# # UTC表示标准时间
其他时区信息需要通过apk
安装tzdata
。为了保证镜像尽可能的小,缺省是不安装这个包的。
# apk --update add --no-cache tzdata
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/community/x86_64/APKINDEX.tar.gz
(1/1) Installing tzdata (2019c-r0)
Executing busybox-1.30.1-r2.trigger
OK: 21 MiB in 36 packages
安装完成后系统将所有时区信息存放在了/usr/share/zoneinfo/
目录下
# ls /usr/share/zoneinfo/
Africa CET Egypt GMT+0 Iran MST7MDT Poland UTC zone.tab
America CST6CDT Eire GMT-0 Israel Mexico Portugal Universal zone1970.tab
Antarctica Canada Etc GMT0 Jamaica NZ ROC W-SU
Arctic Chile Europe Greenwich Japan NZ-CHAT ROK WET
Asia Cuba Factory HST Kwajalein Navajo Singapore Zulu
Atlantic EET GB Hongkong Libya PRC Turkey iso3166.tab
Australia EST GB-Eire Iceland MET PST8PDT UCT posixrules
Brazil EST5EDT GMT Indian MST Pacific US right
其中/usr/share/zoneinfo/Asia/Shanghai
这个文件是北京时间。我们将它复制到/etc/localtime
文件。(/etc/localtime
这个文件缺省也是没有的)。
# cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# date
Sat Nov 9 13:36:15 CST 2019
# # 这时我们看到系统的时区信息从UTC变为CST,这就是中国时区了
然后清理安装的时区文件。由于Docker镜像要尽可能的小,所以一些用不到的文件需要删除掉(这也是缺省镜像中不包含时区文件的原因)
# apk del tzdata
(1/1) Purging tzdata (2019c-r0)
Executing busybox-1.30.1-r2.trigger
OK: 18 MiB in 35 packages
#
# ls /usr/share/zoneinfo
ls: /usr/share/zoneinfo: No such file or directory
# # 已经删除掉了/usr/share/zoneinfo目录
OK。第一步Alpine的时区已经设置完成。下面我们进入Python环节
Python中的时区
我们先进入Python看看现在的状况
>>> import time
>>> time.timezone
-28800
-28800是什么意思呢?Python文档中关于time.timezone的描述是“UTC以西的秒数”,我们所处与东8区所以是负值,-28800/60/60=-8
。这说明python中取得的时区信息是正确的。
然而,真像不仅仅如此。查看文档time.tzset()。我们发现Python可以通过这个命令来重置时区信息。而环境变量os. environ['TZ']
则指定了重置为哪个时区。在没有环境变量os. environ['TZ']
的情况下Python使用了系统缺省的时区,也就是/etc/localtime
的信息。我们设置一下看看:
>>> import time,os
>>> os.environ['TZ']='Asia/Shanghai' # 这表示北京时间
>>> time.tzset() # 重置时区信息
>>> time.timezone
0
>>> # 我去?怎么变成UTC了?
上面我们设置了北京时间,但却变成了标准时间。原因是,tzset()会去/usr/share/zoneinfo/
目录下找Asia/Shanghai
这个文件。而之前我们为了减少Docker镜像的大小将这个目录删掉了。由于我们只用北京时间,我们只需要恢复这一个文件就可以了。
# mkdir -p /usr/share/zoneinfo/Asia/
# ln -s /etc/localtime /usr/share/zoneinfo/Asia/Shanghai
我们将之前复制的/etc/localtime
连接为/usr/share/zoneinfo/Asia/Shanghai
再进入Python试一下
>>> import time,os
>>> os.environ['TZ']='Asia/Shanghai' # 这表示北京时间
>>> time.tzset() # 重置时区信息
>>> time.timezone
-28800
>>> # 这下好了
到这里我们的环境准备好了。下面需要将这些过程写入Dockerfile
。
FROM python-alpine
...
RUN apk --update add --no-cache tzdata \
; cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
; apk del tzdata \
; ln -s /etc/localtime /usr/share/zoneinfo/Asia/Shanghai
...
datetime中的now()和utcnow()
在程序中获取时间常用的两个方法now()和utcnow(),他们的差别是什么呢?看代码
>>> import os,time,datetime
>>> # 先设置时区为UTC
>>> os.environ['TZ']='UTC'
>>> time.tzset()
>>> time.timezone
0
>>> # 现在是标准时区
>>> datetime.datetime.now().isoformat()
'2019-11-09T08:57:49.531319'
>>> datetime.datetime.utcnow().isoformat()
'2019-11-09T08:57:45.631473'
>>> # 当前时间是下午4点57分,根据时区都转化为标准时间了
>>>
>>>
>>> # 现在设置时区为CST-8(北京时间,等同于'Asia/Shanghai')
>>> os.environ['TZ']='CST-8'
>>> time.tzset()
>>> time.timezone
-28800
>>> # 现在是东8区
>>> datetime.datetime.now().isoformat()
'2019-11-09T17:02:31.682531'
>>> datetime.datetime.utcnow().isoformat()
'2019-11-09T09:02:34.857630'
>>> # 当前时间是下午5点02分
>>> # now()方法是根据时区返回的时间
>>> # utcnow()方法仍然返回UTC的时间
所以now()方法会根据当前时区返回时间,而utcnow()只返回UTC时间。
timezone-aware(时区感知?)
上面关于now()和utcnow()这两方法有一个问题,返回的对象中并没有包含时区信息。也就是说,仅从方法返回的对象看无法得知时间是属于哪个时区。这就涉及timezone-aware这个概念。
简单的说,Python的日期和时间对象分为两类,"aware"和"naive"。
"awar对象"是含有时区信息的时间对象。
"naive对象"是不包含时区信息的时间对象。
now().astimezone()
可以获得当前时区的aware对象
now().astimezone(tz=datetime.timezone.utc)
可以获得标准时区的aware对象
utcnow().replace(tzinfo=datetime.timezone.utc)
也可以获得标准时区的aware对象
小贴士: 为什么需要时区信息
如果你的应用仅服务于一个时区的用户,你可以不需要了解关于时区信息的内容。通过now()取得当前时间,然后直接存入数据库。数据库基本上均采用UTC时间。例如:
- 你当前时间是北京时间中午
12:00 CST
- now()返回的是没有时区的
12:00
- 存入数据库中是
12:00 UTC
- 从数据库中读出的是没有时区的
12:00
- 用于显示时,用户理解的是北京时间中午
12:00 CST
虽然数据库中的时间与当前时间差8个小时,但由于一进一出同时忽略时区信息,结果就负负得正了。但如果你需要服务于跨时区的用户那情况就不一样了。
- 今天是10月10日,你在北京(东8区,
+8:00
)的办公室早上10点(2019-10-10T10:00:00+08:00
)写了一份文档,提交给另一位同事协作。- 与你协作的这位同事在西雅图(西8区,
-8:00
)的办公室打开这份文档,他看到的应该是你什么时间给他的呢?应该是10月9日的下午6点(2019-10-09T18:00:00-08:00
)。如果不处理时区,那么你和这位同事看到的文档创建时间只能是同一个时间值,这就不对了。
考虑时区问题该怎么处理呢?
- 你当前时间是北京时间2019年10月10日上午10点
now()
返回的是没有时区的2019-10-10T10:00:00
now().astimezone()
返回含有东8区时区的时间2019-10-10T10:00:00+8:00
- 存入数据库中时东8区会转化为UTC时间
2019-10-10T02:00:00Z
- 在西雅图的办公室从数据库读取后通过
astimezone()
根据西8区转换为当地时间2019-10-09T18:00:00-8:00
- 于是你看到是
10月10日上午10点
,你在西雅图的同事看到的是10月9日下午6点
最后说一下Django中的时区
上面两部分设置好后,Django的内容就非常简单了,只需要在settings.py
文件中进行配置。
...
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = True
...
Settings参数USE_TZ
TIME_ZONE的官方解释
此项设置相当于os.environ['TZ']='Asia/Shanghai'
如果不填写缺省为'America/Chicago'
即西6区
如果填写错误则会使用标准时区UTC
也就是说Django不会使用系统的缺省时区(
/etc/localtime
),而是始终在/usr/share/zoneinfo/
目录下找时区文件
Settings参数USE_TZ
USE_TZ的官方解释
如果设置为True
Django会采用Aware对象
的形式使用日期和时间。
设置为False
会采用Naive对象
的形式使用日期和时间。
具体会影响到数据库存储和template
中的显示。
我们通过一个mysql
的例子看看具体情况
+--------------+-----------------------------------------+----------------------------------+----------------------------+-------------------------------------------------------------------------------+
| env | cmd | isoformat | UTC in mysql | exception |
+--------------+-----------------------------------------+----------------------------------+----------------------------+-------------------------------------------------------------------------------+
| USE_TZ=False | datetime.datetime.now() | 2019-11-09T16:40:18.969039 | 2019-11-09 16:40:18.969039 | |
| USE_TZ=False | datetime.datetime.now().astimezone() | 2019-11-09T16:40:18.993389+08:00 | NULL | MySQL backend does not support timezone-aware datetimes when USE_TZ is False. |
| USE_TZ=False | datetime.datetime.utcnow() | 2019-11-09T08:40:18.996537 | 2019-11-09 08:40:18.996537 | |
| USE_TZ=False | datetime.datetime.utcnow().astimezone() | 2019-11-09T08:40:18.999031+08:00 | NULL | MySQL backend does not support timezone-aware datetimes when USE_TZ is False. |
| USE_TZ=True | datetime.datetime.now() | 2019-11-09T16:40:19.414235 | 2019-11-09 08:40:19.414235 | |
| USE_TZ=True | datetime.datetime.now().astimezone() | 2019-11-09T16:40:19.469696+08:00 | 2019-11-09 08:40:19.469696 | |
| USE_TZ=True | datetime.datetime.utcnow() | 2019-11-09T08:40:19.473182 | 2019-11-09 00:40:19.473182 | |
| USE_TZ=True | datetime.datetime.utcnow().astimezone() | 2019-11-09T08:40:19.478074+08:00 | 2019-11-09 00:40:19.478074 | |
+--------------+-----------------------------------------+----------------------------------+----------------------------+-------------------------------------------------------------------------------+
这段我就不分析了,各位慢慢理解。
建议设置USE_TZ=True
来使用Django提供的时区机制
结论
Alpine:
在Dockerfile中添加时区信息,并设置缺省时区
FROM python-alpine
...
RUN apk --update add --no-cache tzdata \
; cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
; apk del tzdata \
; ln -s /etc/localtime /usr/share/zoneinfo/Asia/Shanghai
...
Python
- 要获取当前时间不要仅使用
now()
,而是使用now().astimezone()
,获取含有时区信息的时间对象。 - 如果要获取UCT时间使用
now().astimezone(tz=datetime.timezone.utc)
Django
设置settings.py
...
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = True
...
希望本文对你有帮助!!