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類的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
ZERO_TIME_DELTA = timedelta(0) LOCAL_TIME_DELTA = timedelta(hours=8) # 本地時區偏差 class UTC(tzinfo): def utcoffset(self, dt): return ZERO_TIME_DELTA def dst(self, dt): return ZERO_TIME_DELTA class LocalTimezone(tzinfo): def utcoffset(self, dt): return LOCAL_TIME_DELTA def dst(self, dt): return ZERO_TIME_DELTA def tzname(self, dt): return '+08:00' |
一個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物件:
1 2 3 4 5 6 7 |
>>> from datetime import datetime >>> import pytz >>> tz = pytz.timezone(pytz.country_timezones('cn')[0]) >>> tz >>> datetime.now(tz) datetime.datetime(2010, 12, 14, 19, 26, 12, 656000, tzinfo=) |
可是它有個很奇怪的陷阱,注意看那個tz,它實際上是+8:06:00,比北京時間快了6分鐘。更扯淡的是,中國的時區裡找不到北京時間,只能拿上海時間來湊數…
平時使用時可能沒什麼問題,但是構造datetime物件,或呼叫replace方法時就會莫名其妙地差6分鐘了:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> dt = datetime.now(tz) >>> dt datetime.datetime(2010, 12, 14, 19, 32, 23, 281000, tzinfo=) >>> dt2 = datetime(2010, 12, 14, 19, 32, 23, 281000, tzinfo=tz) >>> dt2 datetime.datetime(2010, 12, 14, 19, 32, 23, 281000, tzinfo=) >>> dt == dt2 False >>> dt - dt2 datetime.timedelta(0, 360) >>> dt.tzinfo >>> dt2.tzinfo |
用同一個tz生成的datetime物件,居然會出現不相等的結果,而且連它們的tzinfo都不一樣…
要解決這個問題,最簡單的方法就是使用臺北(Asia/Taipei)時間,它正好是+08:00。不過這樣治標不治本,畢竟還有那麼多不是整點的時區,不可能一一去找替代時區。
實際上limodou在《關於pytz的一些記錄(續)》這篇文章中也提到了這個問題,他指出只要構造時生成offset-naive型別的datetime物件,再用tz.localize(dt)就能生成正確的時間了:
1 2 3 4 5 6 |
>>> dt3 = datetime(2010, 12, 14, 19, 32, 23, 281000) >>> dt3 = tz.localize(dt3) >>> dt3 datetime.datetime(2010, 12, 14, 19, 32, 23, 281000, tzinfo=) >>> dt == dt3 True |
最後就是關鍵的處理夏時令的程式碼,需要用到normalize方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> eastern = pytz.timezone('US/Eastern') >>> dt4 = datetime(2002, 10, 27, 1, 0) >>> dt4 = eastern.localize(dt4) >>> print dt4 2002-10-27 01:00:00-05:00 >>> dt5 = dt4 - timedelta(minutes=10) >>> print dt5 2002-10-27 00:50:00-05:00 >>> dt6 = eastern.normalize(dt5) >>> print dt6 2002-10-27 01:50:00-04:00 >>> dt5.tzinfo >>> dt6.tzinfo |
可以看到,normalize以後,就能正確處理夏時令的變更了。