0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】Webアプリ開発におけるタイムゾーンの扱い方・注意点(datetime / zoneinfo)

Posted at

本記事のポイント

Datetime型にはタイムゾーン情報を付けましょう。とりあえず以下のような関数を使うのが良いかと思います。

from datetime import datetime
from zoneinfo import ZoneInfo

def get_utc_now():
    return datetime.now(ZoneInfo("UTC"))

def get_jst_now():
    return datetime.now(ZoneInfo("Asia/Tokyo"))

def parse_str_as_jst(dt_str, dt_format, dt_timezone="Asia/Tokyo"):
    return datetime.strptime(dt_str, dt_format).astimezone(ZoneInfo(dt_timezone))

はじめに

アプリケーションをサーバーで動かすとなれば、タイムゾーンの管理が問題となります。仮に日本国内でサービス提供するとしても、サーバー時刻がUTCであれば考慮が必要になります。そんな日時オブジェクトの扱い方・注意点についてまとめました。

Pythonの標準ライブラリであるdatetimeを中心にまとめていきます。また、タイムゾーン情報を扱うライブラリにはzoneinfopytzが存在します。zoneinfoが標準ライブラリであるため、こちらを利用します。(Python3.9で追加された)

Native / Aware datetimeの違い

日時オブジェクトは2種類存在します。両者の違いは、

  • Native datetime:
    • タイムゾーン情報を「持たない」日時オブジェクト
  • Aware datetime:
    • タイムゾーン情報を「持つ」日時オブジェクト

たったこれだけのことですが、基本的にはAware datetimeを使うほうが安全でしょう。

実行環境に依存しないdatetime.now()

まず、現在時刻を取得する方法であるdatetime.now()は、実行環境のシステム時刻に依存します。したがって、サーバーを借りてシステムを稼働させるときには、タイムゾーン情報を指定して現在時刻を取得するなどの対応が必要です。

from datetime import datetime, timezone
import time
from zoneinfo import ZoneInfo

# システム設定のタイムゾーン情報を取得する
print(time.tzname)

# 環境依存
print(datetime.now())

# UTCで固定して取得する
print(datetime.now(timezone.utc))
print(datetime.now().astimezone(timezone.utc))

# 東京時間で取得する
print(datetime.now(ZoneInfo("Asia/Tokyo")))

# ('東京 (標準時)', '東京 (夏時間)')
# 2024-12-26 22:10:00.000000
# 2024-12-26 13:10:00.000000+00:00
# 2024-12-26 13:10:00.000000+00:00
# 2024-12-26 22:10:00.000000+09:00

文字列からパースする(タイムゾーン付き)

文字列から日時オブジェクトを作るとき、datetime.strptime()を使うことが多いと思いますが、これによって生成されるのはNative datetimeです。

from datetime import datetime
from zoneinfo import ZoneInfo

JST_ZONEINFO = ZoneInfo("Asia/Tokyo")
date_str = "2024-12-26 15:00:00"

# Native
date_native = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
print(date_native)

# Aware
date_aware = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=JST_ZONEINFO)
print(date_aware)

# 2024-12-26 15:00:00
# 2024-12-26 15:00:00+09:00

文字列へフォーマットする(タイムゾーン付き)

こちらもstrftime()を使うことが多いかと思いますが、避けるほうが良いかもしれません。

dt = datetime.now().replace(tzinfo=ZoneInfo("UTC"))

print(dt.strftime("%Y-%m-%d %H:%M:%S"))
print(dt.isoformat()) # ISO 8601

# 2024-12-26 22:10:00
# 2024-12-26T22:10:00.000000+00:00

ちなみに、例えばTypeScriptでこれをパースするには、以下のコードで十分です。(PythonでAPIを立てて、フロントエンドにTSを使うシーンを想定)標準化されたフォーマットに乗っかるほうが、機械可読性が高いので適切に使い分けたいです。

const isoString: string = "2024-12-26T15:00:00+09:00";
const date: Date = new Date(isoString);

.replace().astimezone()の違い

どちらも日時オブジェクトにタイムゾーン情報を付与する・書き換えるときに使うものですが、挙動が少し異なります。

replace()は単純にtzinfo(タイムゾーン)を書き換えるだけで、時差の計算は行われません。Native datetimeにタイムゾーン情報を付与するときに使うことが多いと思います。

astimezone()はタイムゾーン情報を書き換えて、時差の計算も行います。Aware datetimeのみで利用可能です。

from datetime import datetime
from zoneinfo import ZoneInfo

# 現在時刻(UTC)を取得
utc_now = datetime.now(ZoneInfo("UTC"))
print(f"UTC: {utc_now}")

# replace()
wrong_tokyo = utc_now.replace(tzinfo=ZoneInfo("Asia/Tokyo"))
print(f"replace: {wrong_tokyo}")

# astimezone()
correct_tokyo = utc_now.astimezone(ZoneInfo("Asia/Tokyo"))
print(f"astimezone: {correct_tokyo}")

# UTC: 2024-12-26 12:10:00.000000+00:00
# replace: 2024-12-26 12:10:00.000000+09:00
# astimezone: 2024-12-26 21:10:00.000000+09:00

時刻比較における注意点

また、時刻を比較するときにAwareとNativeで異なる場合にエラーが発生します。こちらも頭の片隅においておくと良いかもしれません。

print(datetime.now() > datetime.now())
print(datetime.now(timezone.utc) > datetime.now(timezone.utc))
print(datetime.now() > datetime.now(timezone.utc))

# False
# False
# ---------------------------------------------------------------------------
# TypeError                                 Traceback (most recent call last)
# Cell In[6], line 3
#       1 print(datetime.now() > datetime.now())
#       2 print(datetime.now(timezone.utc) > datetime.now(timezone.utc))
# ----> 3 print(datetime.now() > datetime.now(timezone.utc))
#
# TypeError: can't compare offset-naive and offset-aware datetimes

テストにおけるタイムゾーンの指定

テストコードでもタイムゾーンの固定をする場合には、freezegunを使います。

インストール:pip install freezegun

import unittest
from datetime import datetime
from freezegun import freeze_time
from zoneinfo import ZoneInfo

class TestDateTime(unittest.TestCase):
    # クラスメソッドに対して
    @freeze_time("2024-12-26 15:00:00", tz_offset=9)
    def test_fixed_time(self):
        current = datetime.now(ZoneInfo("Asia/Tokyo"))
        self.assertEqual(current.hour, 15)
        self.assertEqual(current.minute, 0)

    # withステートメントを使用する場合
    def test_with_freeze(self):
        with freeze_time("2024-12-26 15:00:00", tz_offset=9):
            current = datetime.now(ZoneInfo("Asia/Tokyo"))
            self.assertEqual(current.hour, 15)

おわりに

Webアプリを開発するにあたって、「開発環境ではJST(日本標準時)だが、サーバー環境はUTC(世界協定時)で動いている」といった問題がしばしば発生します。さらに、実装段階では意識していないとタイムゾーンがごちゃごちゃになってしまうとか、取り扱いを間違えると深刻な不具合になるとか、厄介さがあります。

毎回これで悩まされているので、この内容を意識して実装するようにしたいです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?