Pythonで、pytzを使ったタイムゾーンの変換

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - サードパーティ製ライブラリ
2018年12月4日8:20に更新(約9日前)
2018年11月15日0:34に作成(約28日前)

旧ブログ移行記事です。

Pythonで日付を扱う際は標準ライブラリのdatetimeモジュールを使うことになりますが、タイムゾーン関連の処理にはpytzを使います。

datetimeモジュールの使い方については、Pythonで、datetimeモジュールを使うをご覧ください。

なぜpytzが必要か

datetime関連オブジェクトにはタイムゾーンを扱うための機能が備わっています。

以下は現在の日付を表す、タイムゾーン情報を持ったdatetime.datetimeオブジェクトを作成しています。

import datetime

now = datetime.datetime.now(tz=datetime.timezone.utc)

tz=datetime.timezone.utcは、UTCというタイムゾーンに変換してください、という指定です。tz引数に、そのタイムゾーンを表すオブジェクトを指定するだけです。

しかし、標準ライブラリ内にはdatetime.timezone.utc以外にタイムゾーンを表すオブジェクトがありません。日本やアメリカ、中国等のタイムゾーン情報も当然欲しいわけです。

pytzは様々なタイムゾーン情報オブジェクトを提供しており、公式サイトには以前、このように書かれていました。

標準ライブラリには UTC 以外の tzinfo インスタンスはありませんが、サードパーティーのライブラリで (Olson データベースとしても知られる) IANA タイムゾーンデータベース を Python に提供するものが存在します: それが pytz です。

pytz は最新の情報を含み、使用を推奨されています。

WebフレームワークのDjangoでも使われているポピュラーなライブラリです。

pytzのインストール

インストールはすぐできます。

pip install pytz

ざっくりとした簡単な使い方

まずですが、現在日付を取得し、日本のタイムゾーンを適用する例です。

import datetime
import pytz

jst = pytz.timezone('Asia/Tokyo')
jst_now = datetime.datetime.now(tz=jst)
print(jst_now)

以下のように表示されます。

2018-11-15 17:35:35.048167+09:00

+09:00という見慣れないものがついていますが...これはタイムゾーン情報を表しています。ちょっと段階的に説明していきましょう。

住んでいる地域(と、時代)にはタイムゾーンという概念があります。いわゆるひとつの時差です。

日本で11/15 17時ならば、アメリカ...ニューヨークとしましょう、ニューヨークは11/15 3時です。時差は14時間あります。

では+09:00は何でしょうか。アメリカとの時差ではなさそうに見えます。この9時間の差はUTC(協定世界時)との差であり、世界標準の時間との差を表しています。まずですが、このような世界標準の時間が存在し、UTCという名前であることは押さえておきましょう。

日本はこの世界標準から見ると+9時間の時差で、ニューヨークは世界標準から見ると-5時間です。なので、日本とニューヨークだけを比較すると14時間の差となります。

2018-11-15 17:35:35.048167+09:00という出力の意味をざっくりまとめると、UTCから見ると+9時間の場所(タイムゾーン)にいて、そこでは2018/11/15 17時35分といった内容になります。

タイムゾーンの設定

タイムゾーン情報を持った日付オブジェクトをaware、そうでないものをnaiveと呼びます。

datetime.datetime.now(tz=)の場合はタイムゾーン情報を持った(awareな)日付オブジェクトが得られるので良いのですが、datetime.datetime(year=2018, month=11, day=15)で作った場合はタイムゾーン情報がありません。

datetimeオブジェクトがどのタイムゾーンかをハッキリさせないとタイムゾーン関連の処理はできません。どこを基準に作られたわからないような日付・時刻を、他の国の日付・時刻に変換することは当然できないのです。

なので、私たちがタイムゾーン関連の処理をするためには、作成されたdatetimeオブジェクトにタイムゾーンを設定することがファーストステップです。

pytzにおいて、タイムゾーン情報のないdatetimeオブジェクト(naive)にタイムゾーンを設定するにはtzinfo.localize()とします。tzinfoはタイムゾーンの情報を表すオブジェクトで、既に↑でちょっと作りましたが、改めて説明します。まず、以下のように作ります。

import datetime
import pytz

# 日本標準時(JSTとも呼ぶ)のタイムゾーン情報オブジェクト
jst = pytz.timezone('Asia/Tokyo')

適当なdatetimeオブジェクトを作り、作成した日本標準時tzinfoオブジェクトのlocalize()を呼びます。

import datetime
import pytz

# 日本標準時(JSTとも呼ぶ)のタイムゾーン情報オブジェクト
jst = pytz.timezone('Asia/Tokyo')
naive_dt = datetime.datetime(year=2018, month=11, day=15, hour=18, minute=30)
aware_dt = jst.localize(naive_dt)
print(aware_dt)

+09:00となっています。

2018-11-15 18:30:00+09:00

localize()とすることで、日付オブジェクトに(←日本時間)のような注釈をつけるとイメージするとわかりやすいかもしれないですね。2018-11-15 18:30:00+09:00(←日本時間)

タイムゾーンの変換

日付オブジェクトのastimezone()に、tzinfoオブジェクトを引数として渡すだけです。

import datetime
import pytz

# 日本標準時 JSTとも呼ぶ
jst = pytz.timezone('Asia/Tokyo')
jst_now = datetime.datetime.now(tz=jst)
print(jst_now)

# 東部標準時 ESTとも呼ぶ。ニューヨーク、ワシントン、オタワ、モントリオール等
est = pytz.timezone('US/Eastern')
est_now = jst_now.astimezone(est)
print(est_now)

変換しているのは以下の部分です。est = pytz.timezone('US/Eastern')として別のタイムゾーンを取得し、astimezone()引数にそのタイムゾーンを指定します。

# 東部標準時 ESTとも呼ぶ。ニューヨーク、ワシントン、オタワ、モントリオール等
est = pytz.timezone('US/Eastern')
est_now = jst_now.astimezone(est)
print(est_now)

今は日本時間で18:34ですが、ニューヨーク等では4:34だそうです。+9や-5から逆算すると、UTCでは9:34ですね。

2018-11-15 18:34:00.574574+09:00
2018-11-15 04:34:00.574574-05:00

サマータイム(DST)

まずサマータイムについて簡単に説明しましょう。

日本ではあまり馴染みがありませんが、緯度の高い国では夜でも明るいということがよくあるそうです。試しに7月あたりのロンドンの日出・日入をググってみましたが、5時頃にお日様がのぼり、9時頃にお日様が沈むようです... 5時から9時までお日様がいる。やばい。

明るいお外で遊びたいのは全国共通みたいで、朝を1時間前倒すことで、もっと明るいお外で遊べるのでは?という考えのもと生まれたそうです。Daylight Saving Timeの名のとおり、太陽の光をSavingする時間制度的なことなのでしょう。

サマータイムを導入していない国も多くありますし、導入している国も多くあります。日本は導入していませんが、2020オリンピックで云々という話もありますね。

国でも、アメリカのように州ごとに導入したりしなかったりしています。

そして、国や地域ごとにサマータイムの期間も異なります。ロンドンの次回(2019)のサマータイム期間は3月31日から10月27日ですが、ニューヨークでは3月10日から11月3日です。

このような混沌な状況ですが、datetimeモジュールとpytzはこれらを上手く対応しています。

時間の進み方

話を簡単にするために、ここはニューヨークで、今は2019年3月10日(日)1時59分としましょう。2019年のニューヨーク(EST)では、2019年3月10日(日)2時00分からサマータイムが始まります。つまり、今はサマータイム1分前です。

1分後にサマータイムが開始され、なんと1時間進みます。1時59分の次は3時になります。2時台の時間が消し飛びましたが、これはサマータイム終了時に補正されます。

それでは時が経ち、今は2019年11月3日(日)1時59分としましょう。1分後にサマータイムが終了します。

1分後、サマータイムが終わり...今度はなんと1時になります。前に消し飛んだ1時間分をここで調節しているわけですね。

サマータイムをまたいだ処理の注意点

実際にいくつか試しながら、注意点を説明していきます。

import datetime
import pytz

est = pytz.timezone('US/Eastern')
now = datetime.datetime(year=2019, month=3, day=10, hour=2)
est_now = est.localize(now)
print(est_now)

2019年の3/10 2時の日付オブジェクトを作成し、東部標準時でローカライズしています。この時刻はサマータイム開始時で、実際には2時になることはなく3時になる予定です。

しかし出力はあまり良くありません。3時と表示してほしいものです。

2019-03-10 02:00:00-05:00

このような場合、tzinfo.normalize()を呼び出すことで上手くサマータイムに対応してくれます。

est_now = est.normalize(est.localize(now))

normalizeによって、本来の表示になりました。localize()を使うときは、毎回normalize()も一緒に使っても良いぐらいです。

2019-03-10 03:00:00-04:00

-05:00から-04:00に変換されたことにも注目してください。そして、逆算してUTCだと両方とも同じ時間を指していることにも注目してください。サマータイムを導入していようといまいと、サマータイム中だろうとそうでなかろうと、UTC(世界標準時)では同じ時間になりますし、そうしないと問題が出てきます。

なんとなくわかってきました。次は日付を加算してサマータイムをまたいでみます。

import datetime
import pytz

est = pytz.timezone('US/Eastern')
now = datetime.datetime(year=2019, month=3, day=10, hour=1, minute=59)
est_now = est.normalize(est.localize(now)) + datetime.timedelta(minutes=1)
print(est_now)

ここでやっているのは、ESTにおけるサマータイム開始1分前の日付オブジェクトを作り、1分足すという処理です。出力は上と同じになるのが正しいのですが...

しかし、表示は正しくありません。

2019-03-10 02:00:00-05:00

この場合もnormalize()しましょう。日付全体を囲みます。localize()したり日付を足したりしたら、normalize()です。

est_now = est.normalize(est.localize(now) + datetime.timedelta(minutes=1))

今度はOKですね。

2019-03-10 03:00:00-04:00

サマータイム後をまたいだ加算も注意ですが、やはりnormalize()で正しく動作します...と思いきや落とし穴があります。

import datetime
import pytz

est = pytz.timezone('US/Eastern')
now = datetime.datetime(year=2019, month=11, day=3, hour=1, minute=59)
est_now = est.normalize(est.localize(now) + datetime.timedelta(minutes=1))
print(est_now)

この出力は間違っているように見えます(実は正しいのですが!)。時間は2時ではなく1時にならなければなりません。

2019-11-03 02:00:00-05:00

気づいている方もいるかもしれませんが、サマータイム終了後は同じ時間が出現します

サマータイムが終了すると1時間戻りますので、1:59分の次は1時に戻ります。時間が経つと...また1:59分が訪れることになります。つまり、サマータイム中の1:59分とサマータイム終了後の1:59分があるわけです。

datetime.datetime(year=2019, month=11, day=3, hour=1, minute=59)という指定では、どちらの1時59分を指しているかわかりません。このような場合にそなえて、localize()には専用の引数があります。

is_dst=Trueとすると、これがサマータイム中の1:59分であることを伝えれます。

est_now = est.normalize(est.localize(now, is_dst=True) + datetime.timedelta(minutes=1))

注意点として、est.localize(now, is_dst=True)の段階ではまだサマータイム中です。なので、1分足したならばnormalize()しないとサマータイム中の表示のままになります。

今度はちゃんと、サマータイム終了後としてちゃんと表示されていますね。

2019-11-03 01:00:00-05:00

is_dstのデフォルトはFalseで、これはサマータイム後の1:59分という意味になります。サマータイム中ならばTrueにします。ちなみにですがNoneも指定でき、Noneの場合は曖昧な日付...今回のように2通りの解釈ができる日付だとエラーを起こしてくれます。

pytz.exceptions.AmbiguousTimeError: 2019-11-03 01:59:00

後で紹介しますが、どちらの意味か判断しかねる状況もありますので、そういう場合には有用です。

さて、もう一つ軽く覚えておくことがあります。以下の日付ですが、実は何かがヘンです(見づらいので、normalizeはしていません)。

now = datetime.datetime(year=2019, month=3, day=10, hour=2, minute=30)
est_now = est.localize(now)

東部標準時において、この2019/3/10 2:30 という時刻は基本的に見ないはずです。2時になると3時になることを思い出してください。これが表示されるのは、調節し忘れた腕時計ぐらいのものです。

この場合でも、is_dst=Noneは活躍します。存在しない日付を渡されると、以下のような例外を送出します。

pytz.exceptions.NonExistentTimeError: 2019-03-10 02:30:00

長くなったので、サマータイム対応の要点をまとめます。

  • 日付を操作したら、normalize()する
  • 存在しない日付や2度現れるローカル時間は、is_dst引数で対処する

サマータイムのように、ある日付からタイムゾーンが変化する、というケースが幾つかあります。実は日本もそれがあり、今の標準時間のほかに2つあります(過去なので、それにぶつかることは少ないです)。そういった場合もnormalize()で正規化することができます。

ベストな扱い方

タイムゾーン関連の処理はもう怖くないと思いますが、日付や時刻に関しては昔からUTCで扱うのが一番と言われており、こちらを使うほうがnormalize()だとかを呼ばなくて済むので簡単です。

ベストな扱い方を端的に述べると、極力UTCで扱いエンドユーザー等への出力のときだけ変換することです。

エンドユーザー等への出力というのは、例えば以下のようなケースです。

  • HTML上に日付を表示する場合。このブログの日付欄がUTCだったら、見る人は戸惑うかもしれません。
  • 顧客に渡すファイル等に日付を出力する場合。UTCだと怒られるかもしれません。
  • テストやデバッグ、ツールの出力等。馴染みのある日付で表示してほしいかもしれません。

つまり、人間とやりとりする場合と考えると良いでしょう。こういったケースを除き、すべてUTCで扱うのが最良です。実際、Djangoフレームワークもこのやり方です。

現在の日付・時刻をUTCで取得する例を紹介します。非常にシンプルに書けます。

import datetime
import pytz

now = datetime.datetime.now(tz=pytz.utc)
# now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)  # これもよく見る書き方、こちらのほうが早いっちゃ早い

ちょっとJSTとEST時間を表示してみましょう。

est = pytz.timezone('US/Eastern')
jst = pytz.timezone('Asia/Tokyo')

print(now.astimezone(est))
print(now.astimezone(jst))

正しく現在時刻が取れています。

2018-11-15 08:54:48.884871-05:00
2018-11-15 22:54:48.884871+09:00

↑でやったタイムゾーンをまたいだ処理を、UTCに変換してからやってみる例です。

import datetime
import pytz

# 東部標準時でのawareな日付作成
est = pytz.timezone('US/Eastern')
naive_est = datetime.datetime(year=2019, month=3, day=10, hour=1, minute=59)
aware_est = est.localize(naive_est)

# UTCに変換
utc = aware_est.astimezone(pytz.utc)
utc = utc + datetime.timedelta(minutes=1)

# 東部標準時で出力
print(utc.astimezone(est)) 

結果も問題なしですね。

2019-03-10 03:00:00-04:00

UTCへの変換、UTCからの変換、UTCへの時間の加算・減算、これらにnormalize()は不要です。サマータイム中であればlocalize()時にnormalize()をかぶせていましたが、UTCに後で変換するならこれも不要です。データベース等への保存も基本的にはUTCで行いますし、UTCで持っておくと色々と捗ります。

もう少し現実的な例では、他のファイルであったりユーザーが文字で入力した日付を扱うケースがあるでしょう。

例えば、HTML上のフォームはよくありますね。 HTMLのフォームに入力してもらう

このようにHTMLのフォームから入力してもらう場合...つまり、日付が文字列で相手から渡されるケースでは、ちょっと処理が増えます。

import datetime
import pytz

# ユーザーが入力した日付の文字列とします。
user_input = '2019-11-3 01:30:22'

# ユーザーのタイムゾーン。
user_tz = pytz.timezone('US/Eastern')

user_input_dt_naive = datetime.datetime.strptime(user_input, '%Y-%m-%d %H:%M:%S')
user_input_dt_aware = user_tz.localize(user_input_dt_naive, is_dst=None)

# UTCへ変換する。
user_input_dt_utc = user_input_dt_aware.astimezonoe(pytz.utc)

user_input = '2019-11-3 01:30:22'は、例えばユーザーがフォームで入力した文字列です。それをstrptime()で日付型のオブジェクトにし、ユーザーのタイムゾーンでuser_tz.localize()します。そして最後にUTCへと変換し、後は煮るなり焼くなりします。

ちなみにですが、この日付は先ほどもあったあいまいな日付です。is_dst=Noneにし、例外を送出するようにしました。この例外を処理(try~except)して、画面に2019-11-03 01:30:22 はUS/Easternのタイムゾーンでは解釈できませんでした。それは曖昧であるか、存在しない可能性があります。などと表示するのも丁寧かもしれません。

実際、Djangoフレームワークはそのようなことをします。

もう少し込み入った話

Python、pytzの詳細をご覧ください。

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする