ついカッとなって、Pythonで時刻文字列のタイムゾーン変換をまとめてみた

はじめに

こんにちは、平野です。

PythonでUTCからJSTへの時刻の変換を行いたいと思った時、みなさんの中で必勝法は固まっているでしょうか? 私はWebで調べて、なんかいろんなパッケージが出てくるなー、と思いつつ貼り付けをしていました。 そんな状態がしばらく続いて、理解できていない自分に嫌気が差したので、 自分の中の必勝法を確立するべくいくつかのやり方を試してみました。

検証環境

  • macOS High Sierra バージョン10.13.6
  • Python 3.7.3

題材

UTCの時刻文字列をJSTの時刻文字列に変換します。

標準ライブラリだけを使う

datetime

datetimeは日付・時刻を扱うPythonの標準ライブラリです。 標準ライブラリでできることは標準ライブラリでやる、 というのがプログラムを書く上ではかなり重要かな、と思っております。 何はなくとも、まずはこのライブラリに当たるべきです。

datetimeで気をつけるべき点としてawareとnaiveという概念があります。

公式ドキュメントから

aware オブジェクトは他の aware オブジェクトとの相対関係を把握出来るように、タイムゾーンや夏時間の情報のような、アルゴリズム的で政治的な適用可能な時間調節に関する知識を持っています。aware オブジェクトは解釈の余地のない特定の実時刻を表現するのに利用されます [1]。

naive オブジェクトには他の日付時刻オブジェクトとの相対関係を把握するのに足る情報が含まれません。あるプログラム内の数字がメートルを表わしているのか、マイルなのか、それとも質量なのかがプログラムによって異なるように、naive オブジェクトが協定世界時 (UTC) なのか、現地時間なのか、それとも他のタイムゾーンなのかはそのプログラムに依存します。Naive オブジェクトはいくつかの現実的な側面を無視してしまうというコストを無視すれば、簡単に理解でき、うまく利用することができます。

「aware」はタイムゾーンも考慮した"絶対的なあるタイミング"を指定し、 「naive」はタイムゾーンを指定しないと"絶対的なあるタイミング"が特定できない表現だと解釈できます1。 言葉にするとややこしいですが、そんなに難しい概念ではないですね。

datetimeだけでUTC->JST変換

なんということはなく、datetimeだけでも目的の変換は達成できました。 datetimeだけを使ってUTCからJSTへの変換を行うコードは以下になります。

なお、datetimeだけをimportして、datetime.datetime.strftimeなどを冗長に書いているのは、 「datetimeモジュール」の中に「datetimeクラス」がある、というややこしい事情を意識するために敢えてやっています。 必要に応じて適宜importして頂ければと思います。

import datetime

def utc_to_jst(timestamp_utc):
    datetime_utc = datetime.datetime.strptime(timestamp_utc + "+0000", "%Y-%m-%d %H:%M:%S.%f%z")
    datetime_jst = datetime_utc.astimezone(datetime.timezone(datetime.timedelta(hours=+9)))
    timestamp_jst = datetime.datetime.strftime(datetime_jst, '%Y-%m-%d %H:%M:%S.%f')
    return timestamp_jst

input = '2019-04-09 10:57:13.015'
print("[UTC] " + input)
print("[JST] " + utc_to_jst(input))
# ==> "[UTC] 2019-04-09 10:57:13.015"
# ==> "[JST] 2019-04-09 19:57:13.015000"

datetime.datetime.strptimeで文字列をパースしてオブジェクトを作ります。 %Y-%m-%dなどの表記は煩わしく感じますが、 変に曖昧なパースをされるよりも、正確に記述した方が安心感があります。 ここでは文字列として+0000を後置させ、%zでパースすることでUTCとしてawareなオブジェクトを作成しています。

次にJSTに変換する部分ですが、ポイントはastimezoneの引数をどう作成するかです。 datetimeだけを使う場合はdatetime.timedelta(hours=+9)とすればOKです。 ここでpytzというライブラリを使っているケースなども見受けられますが、標準ライブラリだけでも実現できます。

タイムゾーン作成の別の方法

タイムゾーンのオブジェクトを作成する際に、hours=+9のような表記ではなく、 JSTAsia/Tokyoなどの表記でタイムゾーンを表現したいこともあります。

しかしdatetimeはそれらの表記とUTCからの相対時間の関係を把握していないため、 datetimeだけで実現することはできません。 そこでdateutilパッケージも少しだけ使ってみます。

ただし、以下の記事によれば、JSTAsia/Tokyoなどの表記よりも相対時間で表した方が良いようです。
タイムゾーン呪いの書#"JST": Jerusalem Standard Time?
こちらの記事、非常に勉強になって素晴らしいです。 ただ、読めば読むほどタイムゾーンも闇が深くて怖いです。。。

dateutil

dateutil

powerful extensions to datetime

と書かれている通り、datetimeを拡張させたものです。 標準ライブラリではないのでどこでも使えるとは限りませんが、 少なくともAWS LambdaのPythonでは標準で使うことができるようです。

datetimeとdateutilでUTC->JST変換

dateutilも使用して、タイムゾーン指定にAsia/Tokyoを使用した例です。

import datetime
from dateutil.tz import gettz

def utc_to_jst(timestamp_utc):
    datetime_utc = datetime.datetime.strptime(timestamp_utc + "+0000", "%Y-%m-%d %H:%M:%S.%f%z")
    datetime_jst = datetime_utc.astimezone(gettz('Asia/Tokyo'))
    timestamp_jst = datetime.datetime.strftime(datetime_jst, '%Y-%m-%d %H:%M:%S.%f')
    return timestamp_jst

input = '2019-04-09 10:57:13.015'
print("[UTC] " + input)
print("[JST] " + utc_to_jst(input))
# ==> "[UTC] 2019-04-09 10:57:13.015"
# ==> "[JST] 2019-04-09 19:57:13.015000"

タイムゾーンの取得の部分が

dateutil.tz.gettz('Asia/Tokyo')

に変わっています。 JSTという文字列でも同様の動作になりましたが、先ほどの記事を見る限り、使わない方が良さそうです。

なお、strptimeしている部分も+0000ではなくUTCなどと書きたかったのですが、 なぜか上手くいきませんでした。 %Zは、JSTは日本標準時としてパースしてくれたのですが、 UTCはnaiveなオブジェクトになってしまうようなので+0000のままにしています。

使わなかったメソッド・パッケージ

datautil.parser

datautil.parserというメソッドを使うと%Y-%m-%dなどを使わなくても、 かなり"いい感じ"に文字列をパースしてくれます。

書きなぐりのプログラムには向いていますが、 このくらいの厳密さはあってもいいと思うので、 今回は特に取り上げませんでした。

pytz

pytzは、introductionの先頭に

pytz brings the Olson tz database into Python.

と書かれている通り、Olsonタイムゾーンの定義を使用するためのパッケージです。 Olsonタイムゾーンについてはtz databaseを参照して下さい。

前述のように、dateutilを使ってもAsia/Tokyoなどの文字列からタイムゾーンの情報を得ることができているので、 少なくとも今回のようなUTC->JSTの変換のような用途ではpytzは不要なようです。

なお、以下の記事にpytzが推奨されると書いてありますが、Python3.3と古く、 Python3.7の同一URLのページにはそのような記載は見当たりません。
Pythonの日付処理とTimeZone

まとめ

Python3.7で、ただの文字列である時刻をUTCとみなして、JSTの時間に変更するやり方についてまとめてみました。 Webを検索すると主にdatetimedateutilpytzの3つのパッケージが登場していて、 それぞれ少しずつやり方も違うので混乱していたのですが、 自分なりには納得できる形でまとめることができました。

調べてみた結果、datetimeだけで変換ができるということが一番の収穫でした。 今回のような変換に限らず、基本的にはdatetimeだけでできる処理のやり方を模索し、 それがダメだった(り、あまりにもやりづらかったりした)場合に他のパッケージに当たるのが良さそうです。

日本では意識することが少ないですが、タイムゾーンも文字コード並みに闇が深そうなので、 「完全に理解した」の精神の下、今の理解で全て対応できると思い込むことで、 あまり深淵には近づきたくないという気持ちでいっぱいです。

以上、誰かの参考になれば幸いです。


  1. "絶対的なあるタイミング"とは、ドキュメントの脚注にも書かれている通り、相対性理論を考慮しない前提に依っている。特殊相対性理論を考慮した場合、異なる慣性系における「同時」は(日常の感覚では)定義できない。あれ、もしかしてこの注釈要らない?