天天看點

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

...

希望本文對你有幫助!!