天天看点

alpine linux时区设置,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

>>> # 我去?怎么变成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

>>> # 现在是标准时区

>>> 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

...

希望本文对你有帮助!!