Python的時區(qū)會讓很多人困惑浙炼。我就曾經(jīng)在Alpine的Docker容器中使用Django時遇到時區(qū)總是UTC導(dǎo)致了某些情況下日期格式化時產(chǎn)生了相差一天的問題份氧。
這篇記錄盡量通過詳細(xì)的說明來解釋過程中的所有細(xì)節(jié)問題。希望讀者能夠由此理解Python的時區(qū)管理以及Django的時區(qū)機(jī)制弯屈。
首先說一下Alpine Docker鏡像中的時區(qū)
在Python的官方鏡像中python:alpine
沒有設(shè)置時區(qū)蜗帜,缺省是標(biāo)準(zhǔn)時區(qū)UTC
。
# date
Sat Nov 9 05:25:09 UTC 2019
# # UTC表示標(biāo)準(zhǔn)時間
其他時區(qū)信息需要通過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
安裝完成后系統(tǒng)將所有時區(qū)信息存放在了/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
這個文件是北京時間宴偿。我們將它復(fù)制到/etc/localtime
文件店归。(/etc/localtime
這個文件缺省也是沒有的)。
# cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# date
Sat Nov 9 13:36:15 CST 2019
# # 這時我們看到系統(tǒng)的時區(qū)信息從UTC變?yōu)镃ST酪我,這就是中國時區(qū)了
然后清理安裝的時區(qū)文件消痛。由于Docker鏡像要盡可能的小,所以一些用不到的文件需要刪除掉(這也是缺省鏡像中不包含時區(qū)文件的原因)
# 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
# # 已經(jīng)刪除掉了/usr/share/zoneinfo目錄
OK都哭。第一步Alpine的時區(qū)已經(jīng)設(shè)置完成秩伞。下面我們進(jìn)入Python環(huán)節(jié)
Python中的時區(qū)
我們先進(jìn)入Python看看現(xiàn)在的狀況
>>> import time
>>> time.timezone
-28800
-28800是什么意思呢?Python文檔中關(guān)于time.timezone的描述是“UTC以西的秒數(shù)”欺矫,我們所處與東8區(qū)所以是負(fù)值纱新,-28800/60/60=-8
。這說明python中取得的時區(qū)信息是正確的穆趴。
然而脸爱,真像不僅僅如此。查看文檔time.tzset()未妹。我們發(fā)現(xiàn)Python可以通過這個命令來重置時區(qū)信息簿废。而環(huán)境變量os. environ['TZ']
則指定了重置為哪個時區(qū)。在沒有環(huán)境變量os. environ['TZ']
的情況下Python使用了系統(tǒng)缺省的時區(qū)络它,也就是/etc/localtime
的信息族檬。我們設(shè)置一下看看:
>>> import time,os
>>> os.environ['TZ']='Asia/Shanghai' # 這表示北京時間
>>> time.tzset() # 重置時區(qū)信息
>>> time.timezone
0
>>> # 我去?怎么變成UTC了化戳?
上面我們設(shè)置了北京時間单料,但卻變成了標(biāo)準(zhǔn)時間。原因是,tzset()會去/usr/share/zoneinfo/
目錄下找Asia/Shanghai
這個文件扫尖。而之前我們?yōu)榱藴p少Docker鏡像的大小將這個目錄刪掉了白对。由于我們只用北京時間,我們只需要恢復(fù)這一個文件就可以了换怖。
# mkdir -p /usr/share/zoneinfo/Asia/
# ln -s /etc/localtime /usr/share/zoneinfo/Asia/Shanghai
我們將之前復(fù)制的/etc/localtime
連接為/usr/share/zoneinfo/Asia/Shanghai
再進(jìn)入Python試一下
>>> import time,os
>>> os.environ['TZ']='Asia/Shanghai' # 這表示北京時間
>>> time.tzset() # 重置時區(qū)信息
>>> time.timezone
-28800
>>> # 這下好了
到這里我們的環(huán)境準(zhǔn)備好了躏结。下面需要將這些過程寫入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
>>> # 先設(shè)置時區(qū)為UTC
>>> os.environ['TZ']='UTC'
>>> time.tzset()
>>> time.timezone
0
>>> # 現(xiàn)在是標(biāo)準(zhǔn)時區(qū)
>>> datetime.datetime.now().isoformat()
'2019-11-09T08:57:49.531319'
>>> datetime.datetime.utcnow().isoformat()
'2019-11-09T08:57:45.631473'
>>> # 當(dāng)前時間是下午4點(diǎn)57分,根據(jù)時區(qū)都轉(zhuǎn)化為標(biāo)準(zhǔn)時間了
>>>
>>>
>>> # 現(xiàn)在設(shè)置時區(qū)為CST-8(北京時間黄橘,等同于'Asia/Shanghai')
>>> os.environ['TZ']='CST-8'
>>> time.tzset()
>>> time.timezone
-28800
>>> # 現(xiàn)在是東8區(qū)
>>> datetime.datetime.now().isoformat()
'2019-11-09T17:02:31.682531'
>>> datetime.datetime.utcnow().isoformat()
'2019-11-09T09:02:34.857630'
>>> # 當(dāng)前時間是下午5點(diǎn)02分
>>> # now()方法是根據(jù)時區(qū)返回的時間
>>> # utcnow()方法仍然返回UTC的時間
所以now()方法會根據(jù)當(dāng)前時區(qū)返回時間兆览,而utcnow()只返回UTC時間。
timezone-aware(時區(qū)感知?)
上面關(guān)于now()和utcnow()這兩方法有一個問題塞关,返回的對象中并沒有包含時區(qū)信息抬探。也就是說,僅從方法返回的對象看無法得知時間是屬于哪個時區(qū)帆赢。這就涉及timezone-aware這個概念小压。
簡單的說,Python的日期和時間對象分為兩類椰于,"aware"和"naive"怠益。
"awar對象"是含有時區(qū)信息的時間對象。
"naive對象"是不包含時區(qū)信息的時間對象瘾婿。
now().astimezone()
可以獲得當(dāng)前時區(qū)的aware對象
now().astimezone(tz=datetime.timezone.utc)
可以獲得標(biāo)準(zhǔn)時區(qū)的aware對象
utcnow().replace(tzinfo=datetime.timezone.utc)
也可以獲得標(biāo)準(zhǔn)時區(qū)的aware對象
小貼士: 為什么需要時區(qū)信息
????如果你的應(yīng)用僅服務(wù)于一個時區(qū)的用戶蜻牢,你可以不需要了解關(guān)于時區(qū)信息的內(nèi)容。通過now()取得當(dāng)前時間偏陪,然后直接存入數(shù)據(jù)庫抢呆。數(shù)據(jù)庫基本上均采用UTC時間。例如:
- 你當(dāng)前時間是北京時間中午
12:00 CST
- now()返回的是沒有時區(qū)的
12:00
- 存入數(shù)據(jù)庫中是
12:00 UTC
- 從數(shù)據(jù)庫中讀出的是沒有時區(qū)的
12:00
- 用于顯示時笛谦,用戶理解的是北京時間中午
12:00 CST
雖然數(shù)據(jù)庫中的時間與當(dāng)前時間差8個小時抱虐,但由于一進(jìn)一出同時忽略時區(qū)信息,結(jié)果就負(fù)負(fù)得正了饥脑。但如果你需要服務(wù)于跨時區(qū)的用戶那情況就不一樣了恳邀。
- 今天是10月10日,你在北京(東8區(qū)灶轰,
+8:00
)的辦公室早上10點(diǎn)(2019-10-10T10:00:00+08:00
)寫了一份文檔轩娶,提交給另一位同事協(xié)作。- 與你協(xié)作的這位同事在西雅圖(西8區(qū)框往,
-8:00
)的辦公室打開這份文檔,他看到的應(yīng)該是你什么時間給他的呢鳄抒?應(yīng)該是10月9日的下午6點(diǎn)(2019-10-09T18:00:00-08:00
)。如果不處理時區(qū),那么你和這位同事看到的文檔創(chuàng)建時間只能是同一個時間值许溅,這就不對了瓤鼻。
考慮時區(qū)問題該怎么處理呢?
- 你當(dāng)前時間是北京時間2019年10月10日上午10點(diǎn)
now()
返回的是沒有時區(qū)的2019-10-10T10:00:00
now().astimezone()
返回含有東8區(qū)時區(qū)的時間2019-10-10T10:00:00+8:00
- 存入數(shù)據(jù)庫中時東8區(qū)會轉(zhuǎn)化為UTC時間
2019-10-10T02:00:00Z
- 在西雅圖的辦公室從數(shù)據(jù)庫讀取后通過
astimezone()
根據(jù)西8區(qū)轉(zhuǎn)換為當(dāng)?shù)貢r間2019-10-09T18:00:00-8:00
- 于是你看到是
10月10日上午10點(diǎn)
,你在西雅圖的同事看到的是10月9日下午6點(diǎn)
最后說一下Django中的時區(qū)
上面兩部分設(shè)置好后贤重,Django的內(nèi)容就非常簡單了茬祷,只需要在settings.py
文件中進(jìn)行配置。
...
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = True
...
Settings參數(shù)USE_TZ
TIME_ZONE的官方解釋
此項(xiàng)設(shè)置相當(dāng)于os.environ['TZ']='Asia/Shanghai'
如果不填寫缺省為'America/Chicago'
即西6區(qū)
如果填寫錯誤則會使用標(biāo)準(zhǔn)時區(qū)UTC
也就是說Django不會使用系統(tǒng)的缺省時區(qū)(
/etc/localtime
)并蝗,而是始終在/usr/share/zoneinfo/
目錄下找時區(qū)文件
Settings參數(shù)USE_TZ
USE_TZ的官方解釋
如果設(shè)置為True
Django會采用Aware對象
的形式使用日期和時間祭犯。
設(shè)置為False
會采用Naive對象
的形式使用日期和時間。
具體會影響到數(shù)據(jù)庫存儲和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 | |
+--------------+-----------------------------------------+----------------------------------+----------------------------+-------------------------------------------------------------------------------+
這段我就不分析了沃粗,各位慢慢理解。
建議設(shè)置USE_TZ=True
來使用Django提供的時區(qū)機(jī)制
結(jié)論
Alpine:
在Dockerfile中添加時區(qū)信息键畴,并設(shè)置缺省時區(qū)
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
- 要獲取當(dāng)前時間不要僅使用
now()
,而是使用now().astimezone()
,獲取含有時區(qū)信息的時間對象最盅。 - 如果要獲取UCT時間使用
now().astimezone(tz=datetime.timezone.utc)
Django
設(shè)置settings.py
...
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = True
...
希望本文對你有幫助!!