Closed5

Python の datetime でタイムゾーンを扱う

fujimotoshinjifujimotoshinji

Python で日時を扱う場合、標準ライブラリの datetime を利用することがほとんどだと思います。
一方で datetime はそのまま使うとタイムゾーンを考慮しないため、ローカル端末は JST で、サーバに置いた時に UTC で想定と違う結果になるようなことがあります。
なので datetime を扱う時はタイムゾーンを扱うように実装しましょう。
またシステム的に datetime を扱って、外部とのやり取りでは文字列で扱うことも多いので datetime <-> str の情報も整理する。

まとめ

timezone 付きで現在日時を取得する(UTC)

>>> from datetime import datetime, timezone

>>> now = datetime.now(timezone.utc)
>>> now
datetime.datetime(2022, 4, 5, 3, 0, 6, 998079, tzinfo=datetime.timezone.utc)

timezone 付きで現在日時を取得する(JST)

※ ZoneInfo は Python 3.9 以上で利用できます。Python 3.8 以前は後述の timezone/timedelta を利用。

>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo

>>> now = datetime.now(ZoneInfo("Asia/Tokyo"))
>>> now
datetime.datetime(2022, 4, 6, 12, 49, 5, 800266, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))

timezone 付きで ISO 8601 フォーマットの文字列に変換する(datetime -> str)

>>> now.isoformat()
'2022-04-06T12:49:05.800266+09:00'

timezone 付きで任意のフォーマットの文字列に変換する(datetime -> str)

>>> now.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
'2022-04-06T12:49:05.800266+0900'

%z: がありません。

UTC/JST みたいな名前を取得する

ZoneInfo をセットした datetime から取得できます。

>>> now.tzname()
'JST'

timezone を変更する

UTC から JST に変換します

>>> now = datetime.now(timezone.utc)
>>> now
datetime.datetime(2022, 4, 6, 4, 6, 26, 404529, tzinfo=datetime.timezone.utc)

>>> now.astimezone(ZoneInfo("Asia/Tokyo"))
datetime.datetime(2022, 4, 6, 13, 6, 26, 404529, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))

timezone 付き文字列から datetime を生成する

>>> isoformat = '2022-04-06T12:49:05.800266+09:00'

>>> datetime.fromisoformat(isoformat)
datetime.datetime(2022, 4, 6, 12, 49, 5, 800266, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400)))
fujimotoshinjifujimotoshinji

まず個人的に外部と日時をやり取りする時はタイムスタンプ形式を推奨したい。
タイムスタンプはタイムゾーン関係なく、地理に関係なく数値が同じであれば同じ時間になる。
ただしタイムスタンプは人間が理解できないので利便性としては悪い。
なので人間が関わる時は基本的に人間が理解できる文字列で表現すべきだ。
たとえば、ログだったり、アプリケーションの画面表示だったり。

人間が理解できる文字列で表現すると必ずといっていいほどタイムゾーンが関わってくる。

日時の文字列表現で代表されるのが ISO8601 だ。
https://en.wikipedia.org/wiki/ISO_8601

といっても ISO8601 も様々な表記があるのでここでは YYYY-MM-DDThh:mm:ss[.SSSSSS][+-hh:mm] を扱う。なぜなら datetime ライブラリの仕様がそうなっているから。

fujimotoshinjifujimotoshinji

Python インタプリタで datetime をおさらいしながら、タイムゾーンの扱いを整理します。

ローカル端末はタイムゾーンが JST、Python バージョンは 3.9.9。

datetime.datetime.now()

まずは Python の現在日時を取得する now 関数。

>>> from datetime import datetime
>>> datetime.now()
datetime.datetime(2021, 11, 26, 11, 46, 19, 43326)

>>> datetime.now().isoformat()
'2021-11-26T11:49:20.694621'

datetime.datetime.now() はタイムゾーンの情報を持たない。datetime は JST の時間、数値がセットされる。
ISO8601 形式で出力しても JST タイムゾーンでタイムゾーン情報は含まない。
これではローカルタイムゾーン端末とUTC 時間のサーバで挙動の違いが生じる可能性がある。

datetime.datetime.utcnow()

datetime の now 関数に似たもので utcnow 関数がある。

>>> datetime.utcnow()
datetime.datetime(2021, 11, 26, 2, 51, 18, 159378)

>>> datetime.utcnow().isoformat()
'2021-11-26T02:51:21.797950'

数値は 9時間前になった。
これであればローカルタイムゾーン端末とUTC 時間のサーバで同じ datetime を扱うので挙動の違いが生じなさそうだが、あくまで一つのマシンに閉じた時の話。
外部システムとやり取りする場合、タイムゾーンによって問題が発生する可能性がある。

また utcnow 関数は公式ドキュメントでも推奨されていない。

https://docs.python.org/3/library/datetime.html

timezone

ここから timezone を扱っていく。
datetime はデフォルト timezone を扱わないだけで timezon を扱える。
ただ明示的に指定しなきゃいけないので指定しよう。

>>> from datetime import datetime, timezone

>>> datetime.now(timezone.utc)
datetime.datetime(2021, 11, 26, 3, 0, 6, 998079, tzinfo=datetime.timezone.utc)

>>> datetime.now(timezone.utc).isoformat()
'2021-11-26T03:00:20.249396+00:00'

datetime に tzinfo のパラメータが増えた。
tzinfo は datetime でタイムゾーンを扱うための基底クラスで tzinfo を UTC 基準で実装したクラスが timezone らしい。
これでタイムゾーンがあるので外部システムとやり取りしても時間の認識の違いをなくすことができる

fujimotoshinjifujimotoshinji

UTC 以外のタイムゾーン

timezone クラスはそのままだと UTC しか扱えないけど、完全に日本時間で閉じたシステムを構築する場合に JST を扱いたくなるかもしれません。
その場合は timedelta を使って表現する必要がある。

>>> from datetime import datetime, timezone, timedelta

>>> datetime.now(timezone(timedelta(hours=+9)))
datetime.datetime(2021, 11, 26, 12, 26, 45, 377576, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400)))

>>> datetime.now(timezone(timedelta(hours=+9))).isoformat()
'2021-11-26T12:26:30.433431+09:00'
fujimotoshinjifujimotoshinji

ZoneInfo

Python 3.9 から標準ライブラリに ZoneInfo が導入され、timezone/timedelta を使わなくとも直感的に指定できるようになった。

https://docs.python.org/3/library/zoneinfo.html

>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo

>>> now = datetime.now(ZoneInfo("Asia/Tokyo"))
>>> now
datetime.datetime(2022, 4, 6, 12, 49, 5, 800266, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))

>>> now.isoformat()
'2022-04-06T12:49:05.800266+09:00'
このスクラップは2021/11/26にクローズされました