Pythonでタイムゾーンを扱う簡単な方法!サマータイム(DST)にも対応

pythonでタイムゾーンを扱う

Pythonでタイムゾーンを意識して日付時刻を操作する方法として、pytz, dateutil.tzzoneinfoを使う方法が一般的です。

zoneinfoはpython3.9以降ではないと使えないので、現在主流なのはpytzdateutil.tzです。

pytzdateutil.tzは癖がちょっと違うので、自分の備忘録のためにもそれぞれの時間操作方法をまとめます。

公式ドキュメントによるとpytzではなくdateutil.tzを推奨しているのに加えて、dateutil.tzのほうが直感的にタイムゾーンを扱えるので、個人的にはdateutil.tzがおすすめです!

実行環境
  • Python 3.8
  • dateutil 2.8.2
  • pytz 2021.3

Pythonの環境設定

dateutilとpytzをpipでインストールします。

$ pip install python-dateutil
$ pip install pytz

これからのソースコードでは、下記のパッケージをimportしている前提です。

from dateutil import tz, parser
import datetime
import pytz

必ず知っておくべきnaiveとawareの概念について

datetimeクラスはPythonで日時を扱うことが可能なモジュールですが、これを使う上で必ず知っておかなければならない概念naiveawareです。

違いの詳細はpythonの公式ドキュメント(datetime)を参照してほしいですが、簡単に違いを説明すると下記です。

  • naive: タイムゾーン情報を持たない
  • aware: タイムゾーン情報を持つ

具体的にソースコードで例を紹介します。

dt_naive1 = parser.parse("2022-03-11 08:00:00")  # 文字列からの初期化
dt_naive2 = datetime.datetime(2022, 3, 11, 8)    # datetime()を利用した初期化
print('naive1: ', dt_naive1)
print('naive2: ', dt_naive2)
# naive1:  2022-03-11 08:00:00
# naive2:  2022-03-11 08:00:00


dt_aware_utc1 = parser.parse("2022-03-11 08:00:00 UTC")           # 文字列からの初期化
dt_aware_utc2 = datetime.datetime(2022, 3, 11, 8, tzinfo=tz.UTC)  # datetime()を利用した初期化
# aware1:  2022-03-11 08:00:00+00:00
# aware2:  2022-03-11 08:00:00+00:00

注目する違いは、出力された文字列のオフセット+00:00の有無です。

時差が書いてある場合、awareのオブジェクトとなります。

dateutil.tzによる日付時刻変換操作

dateutil.tzを利用したタイムゾーンを考慮した日付日時変換方法を紹介します。

タイムゾーンの付与と変換

まず、タイムゾーンの「付与」の方法です。あくまで付与なので、naiveオブジェクトに対する操作です。

dt_naive = parser.parse("2022-03-11 08:00:00") # timezone情報は持たない初期化
dt_aware = dt_naive.astimezone(tz.gettz('Asia/Tokyo'))
print('naive :', dt_naive)
print('aware :', dt_aware)
# naive : 2022-03-11 08:00:00
# aware : 2022-03-11 08:00:00+09:00

タイムゾーン情報が付与されていることがわかりますね。

次に、タイムゾーンの「変換」です。つまり、awareオブジェクトに対する操作です。

dt_aware_utc = parser.parse("2022-03-11 08:00:00 UTC")         # utc時刻
dt_aware_tky = dt_aware_utc.astimezone(tz.gettz('Asia/Tokyo')) # Asia/Tokyo時刻に変換
print('utc: ', dt_aware_utc)
print('tky: ', dt_aware_tky)
# utc:  2022-03-11 08:00:00+00:00
# tky:  2022-03-11 17:00:00+09:00

注目するのは、UTC時刻で同じ時間を示す変換が行われている点です。

naiveオブジェクトに対する操作と異なり、時差分を考慮して時間変換が行われています。

ここは非常に重要なので必ず理解してください!naiveオブジェクトへの操作なのか、awareオブジェクトに対する操作なのかをしっかりと意識する必要があります。

日付日時の操作

時間操作を行ってみます。日時変換は、datetime.timedelta()関数を使います(ドキュメント)。

dt_aware = datetime.datetime(2022, 3, 11, 8, tzinfo=tz.gettz('Asia/Tokyo')) # datetime()を利用したawareオブジェクトの初期化
dt_delta = datetime.timedelta(days=1, hours=2, minutes=30)                  # 1日と2時間30分を表す
dt_converted = dt_aware + dt_delta
print('aware    :', dt_aware)
print('converted:', dt_converted)
# aware    : 2022-03-11 08:00:00+09:00
# converted: 2022-03-12 10:30:00+09:00

DSTを考慮した時間変換

datetime.tzを利用する場合、特別な処理はいらずにDST(Daylight Saving Time:夏時間)を考慮できます。

下記は、夏時間が存在するタイムゾーンである、America/New_Yorkでの処理例です。

dt_aware_utc = parser.parse("2022-11-06 05:00:00 UTC")
dt_aware_nyc = dt_aware_utc.astimezone(tz.gettz('America/New_York'))
dt_converted = dt_aware_nyc + datetime.timedelta(hours=1)

print('aware utc:', dt_aware_utc, ',', dt_aware_utc.astimezone(tz.UTC))
print('aware nyc:', dt_aware_nyc, ',', dt_aware_nyc.astimezone(tz.UTC))
print('converted:', dt_converted, ',', dt_converted.astimezone(tz.UTC))

# aware utc: 2022-11-06 05:00:00+00:00 , 2022-11-06 05:00:00+00:00
# aware nyc: 2022-11-06 01:00:00-04:00 , 2022-11-06 05:00:00+00:00
# converted: 2022-11-06 02:00:00-05:00 , 2022-11-06 07:00:00+00:00

現地時刻とそれをUTCに変換した時刻を出力しています。

ニューヨークでは、現地時間の2022-11-06 02:00に夏時間が終わり、通常の時刻に戻ります(UTC時刻では1時間分の時間が進みます)。

出力結果からわかるように、ローカル時間に1時間分を足すことで、ローカル時間では1時間分進み、UTC時刻では2時間分すすんでいることが確認できます。

DSTは頭が混乱してしまいがちですが、現地時間での処理なのか、UTC時刻での処理なのかを正しく理解したうえでdatetime.tzを使えば難しくありません。

pytzを利用した時間変換

pytzを使ってタイムゾーンを扱う記事も多いので、こちらの方法も紹介します。

pytzは使い方に一癖あるので注意が必要です。

awareオブジェクトの初期化

datetilme.tzのような初期化を行うと失敗してしまいます。必ずlocalize()関数を使いましょう。

nyc = pytz.timezone('America/New_York')
dt_aware1 = datetime.datetime(2022, 3, 11, 8, tzinfo=nyc)   # 期待通りではない結果になる
dt_aware2 = nyc.localize(datetime.datetime(2022, 3, 11, 8)) # 期待通りの結果になる
print("not correct:", dt_aware1)
print("correct    :", dt_aware2)

# not correct: 2022-03-11 08:00:00-04:56
# correct    : 2022-03-11 08:00:00-05:00
なぜオフセットがずれる?

-04:56は標準時刻が決まる前のニューヨークの太陽時刻で、昔の標準時刻です。

pytzは計算の効率化のために、標準的なライブラリが利用するタイムゾーン情報を一部で利用していません。

DSTの利用方法

pytzでDSTを扱うときも注意が必要です。normalized()関数を使う必要があります。

nyc = pytz.timezone('America/New_York')
dt_nyc = nyc.localize(datetime.datetime(2022, 2, 14, 12))
dt_nyc_dst1 = dt_nyc + datetime.timedelta(days=180)  # これだけではDSTを考慮できない
dt_nyc_dst2 = nyc.normalize(dt_nyc_dst1)             # normalize()によって、DST変換する
print('nyc            : ', dt_nyc)
print('not correct dst: ', dt_nyc_dst1)
print('correct dst    : ', dt_nyc_dst2)

# nyc            :  2022-02-14 12:00:00-05:00
# not correct dst:  2022-08-13 12:00:00-05:00
# correct dst    :  2022-08-13 13:00:00-04:00

2022-08-13はニューヨークでは夏時間のため、UTCに対して-04:00となります。

ソースコードを見ていただくとわかりますが、夏時間を考慮するためにはnormalize()関数を利用する必要があります

まとめ:datetime.tzを使う方が簡単

この記事ではdateutil.tzpytzを使ったPythonでの日付日時変換処理をまとめました。

コードを見ていただければわかるかと思いますが、dateutil.tzのほうが難しいことを考えずに変数の初期化とDSTの考慮が可能となります。

バグの少ないコードを書くためにはdatetime.tzを利用した方が良いです。

また、この記事では紹介しませんでしたが、python3.9以降では標準ライブラリにzoneinfoと呼ばれるタイムゾーンを扱うモジュールが追加されています。

pytzやdatetime.tzのようなサードパーティ製よりは、標準ライブラリを利用した方が簡単だと思うので、もしpython3.9以降を使っている場合はzoneinfoも調べてみてください。