Alpine Linux+Python+Django的時區(qū)問題

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è)置為TrueDjango會采用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
...

希望本文對你有幫助!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市起惕,隨后出現(xiàn)的幾起案子涡贱,更是在濱河造成了極大的恐慌,老刑警劉巖惹想,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件问词,死亡現(xiàn)場離奇詭異,居然都是意外死亡嘀粱,警方通過查閱死者的電腦和手機(jī)戏售,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來草穆,“玉大人灌灾,你說我怎么就攤上這事”” “怎么了锋喜?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長豌鸡。 經(jīng)常有香客問我嘿般,道長,這世上最難降的妖魔是什么涯冠? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任炉奴,我火速辦了婚禮,結(jié)果婚禮上蛇更,老公的妹妹穿的比我還像新娘瞻赶。我一直安慰自己赛糟,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布砸逊。 她就那樣靜靜地躺著璧南,像睡著了一般。 火紅的嫁衣襯著肌膚如雪师逸。 梳的紋絲不亂的頭發(fā)上司倚,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機(jī)與錄音篓像,去河邊找鬼动知。 笑死,一個胖子當(dāng)著我的面吹牛员辩,可吹牛的內(nèi)容都是我干的盒粮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼屈暗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了脂男?” 一聲冷哼從身側(cè)響起养叛,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎宰翅,沒想到半個月后弃甥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汁讼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年淆攻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘿架。...
    茶點(diǎn)故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡瓶珊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耸彪,到底是詐尸還是另有隱情伞芹,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布蝉娜,位于F島的核電站唱较,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏召川。R本人自食惡果不足惜南缓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望荧呐。 院中可真熱鬧汉形,春花似錦纸镊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至届案,卻和暖如春庵楷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背楣颠。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工尽纽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人童漩。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓弄贿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親矫膨。 傳聞我的和親對象是個殘疾皇子差凹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評論 2 348