Alpine Linux+Python+Django的时区问题

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的官方解释
如果设置为TrueDjango会采用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
...

希望本文对你有帮助!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341