用datetime和pytz來轉換時區

發表於2016-01-22

Python標準庫裡提供了time、datetime和calendar這3個模組來進行時間和日期的處理,其中應用最廣的是datetime,而轉換時區也是靠它來做的。

時區這個玩意非常抽象,處理它時經常弄得我頭暈,只好記錄下來,免得以後再犯暈。

首先要知道時區之間的轉換關係,其實這很簡單:把當地時間減去當地時區,剩下的就是格林威治時間了。
例如北京時間的18:00就是18:00+08:00,相減以後就是10:00+00:00,因此就是格林威治時間的10:00。

而把格林威治時間加上當地時區,就能得到當地時間了。
例如格林威治時間的10:00是10:00+00:00,轉換成太平洋標準時間就是加上-8小時,因此是02:00-08:00。

而太平洋標準時間轉換成北京時間轉換也一樣,時區相減即可。
例如太平洋標準時間的02:00-08:00,與北京時間相差-16小時,因此結果是18:00+08:00。

而Python的datetime可以處理2種型別的時間,分別為offset-naive和offset-aware。前者是指沒有包含時區資訊的時間,後者是指包含時區資訊的時間,只有同型別的時間才能進行減法運算和比較。

不幸的是datetime模組的函式在預設情況下都只生成offset-naive型別的datetime物件,例如now()、utcnow()、fromtimestamp()、utcfromtimestamp()和strftime()。
其中now()和fromtimestamp()可以接受一個tzinfo物件來生成offset-aware型別的datetime物件,但是標準庫並不提供任何已實現的tzinfo類,只能自己動手豐衣足食了…
下面就是實現格林威治時間和北京時間的tzinfo類的例子:

一個tzinfo類需要實現utcoffset、dst和tzname這3個方法。其中utcoffset需要返回夏時令的時差調整;tzname需要返回時區名,如果你不需要用到的話,也可以不實現。

一旦生成了一個offset-aware型別的datetime物件,我們就能呼叫它的astimezone()方法,生成其他時區的時間(會根據時差來計算)。
而如果拿到的是offset-naive型別的datetime物件,也是可以呼叫它的replace()方法來替換tzinfo的,只不過這種替換不會根據時差來調整其他時間屬性。
因此,如果拿到一個格林威治時間的offset-naive型別的datetime物件,直接呼叫replace(tzinfo=UTC())即可轉換成offset-aware型別,然後再呼叫astimezone()生成其他時區的datetime物件。
而如果是+6:00時區的offset-naive型別的datetime物件,則可以建立一個+6:00時區的tzinfo類,然後用上述方式轉換。
而反過來要將offset-aware型別轉換成offset-naive型別時,為了不至於弄混,建議先用astimezone(UTC())生成格林威治時間,然後再replace(tzinfo=None)。

看上去一切都很簡單,但不知道你還是否記得上文所述的夏時令。
提起夏時令這個玩意,真是讓我頭疼,因為它沒有規則可循:有的國家實行夏時令,有的國家不實行,有的國家只在部分地區實行夏時令,有的地區只在某些年實行夏時令,每個地區實行夏時令的起止時間都不一定相同,而且有的地方TMD還不是用幾月幾日來指定夏時令的起止時間的,而是用某月的第幾個星期幾這種形式,。
所以說要寫這樣一個通用的dst方法估計能把人氣死,於是我只好找來了pytz這個第三方庫。(注意不要去sourceforge下載,那個版本4年沒更新了,有嚴重bug。)

這個pytz的文件初看起來很簡單,用pytz.country_timezones(‘國家程式碼’)可以拿到這個國家的時區名列表,而用pytz.timezone(‘時區名’)就能獲取一個tzinfo物件。
例如取中國的第一個時區來生成datetime物件:


可是它有個很奇怪的陷阱,注意看那個tz,它實際上是+8:06:00,比北京時間快了6分鐘。更扯淡的是,中國的時區裡找不到北京時間,只能拿上海時間來湊數…
平時使用時可能沒什麼問題,但是構造datetime物件,或呼叫replace方法時就會莫名其妙地差6分鐘了:

用同一個tz生成的datetime物件,居然會出現不相等的結果,而且連它們的tzinfo都不一樣…

要解決這個問題,最簡單的方法就是使用臺北(Asia/Taipei)時間,它正好是+08:00。不過這樣治標不治本,畢竟還有那麼多不是整點的時區,不可能一一去找替代時區。
實際上limodou在《關於pytz的一些記錄(續)》這篇文章中也提到了這個問題,他指出只要構造時生成offset-naive型別的datetime物件,再用tz.localize(dt)就能生成正確的時間了:


最後就是關鍵的處理夏時令的程式碼,需要用到normalize方法:

可以看到,normalize以後,就能正確處理夏時令的變更了。

相關文章