Pythonで日付文字列からのdatetime変換やタイムゾーンの変更などをいい加減覚えたい

Pythonで日付文字列からのdatetime変換やタイムゾーンの変更などをいい加減覚えたい
目次

仕事がらpythonを使って、データのコンバータを作成することも度々あるのですが、pythonのdatetimeを使った文字列から日時への変換タイムゾーンの変更 を毎回ネットで調べているので、いい加減覚えないと業務効率上差し支えそうです。

今回は自分の備忘録的な意味も込めて書こうと思います。

なお、Pythonでは日付時刻の処理を行う場合に datetimedatetime などの型を使って処理をしますが、今回は datetime を使います。

環境情報

今回のPythonの実行環境は以下になります。

  • python 3.6
  • pytz
  • jupyter notebook

epochtimeからdatetime

epochtimeを表す 数値型 から datetime に変換します。

epochtimeはUnix時間とも言われますが、世界標準時の1970年1月1日午前0時0分0秒からの経過秒数を整数値で表したものです。 詳細はwikipediaを見た方が早いと思いますので、 こちら をみてください。

UTCからの経過秒数を表現していることから、その数字からタイムゾーン付きのデータに変換することは容易です。

強いて言えば、epochtimeの数値データが エポック秒 なのか エポックミリ秒 なのかの確認をしておくと良いでしょう。 桁数を見るか、関数に実データを放り込んで判別することが多いです。

datetime型の fromtimestamp 関数を使えば記述もシンプルに済ませることができます。

epochtimeからdatetime

fromtimestamp 関数を使った変換のサンプルは以下になります。

1import datetime
2
3e = 1524349374
4dt = datetime.datetime.fromtimestamp(e)
5
6print(dt)
7>> 2018-04-22 07:22:54

ミリ秒を含むepochtimeからdatetime

少数点以下にミリ秒を含んでいても問題なく変換できます。

1# epochtimeからdatetime(ミリ秒含む)
2import datetime
3
4mills = 1524349374.099776
5dt2 = datetime.datetime.fromtimestamp(mills)
6
7print(dt2)
8>> 2018-04-22 07:22:54.099776

エポックミリ秒からdatetime

整数部分でミリ秒部分が表現されている(エポックミリ秒表記)場合には、何桁までがミリ秒を表しているのか確認した後、割ってあげます。

1# epochmillitimeからdatetime
2import datetime
3
4mills = 1524349374099
5dt3 = datetime.datetime.fromtimestamp(mills / 1000)
6
7print(dt3)
8>> 2018-04-22 07:22:54.099000

文字列からdatetime

次に文字列からdatetimeに変換します。

タイムゾーンあり日付文字列からdatetime

strptime 関数を使えば簡単に変換できます。 ミリ秒は %f 、 タイムゾーンは %z を使えばパースしてくれます。

1# タイムゾーンあり
2import datetime
3
4utc_date_str = '2018-04-01 20:10:56.123+0900'
5dt = datetime.datetime.strptime(utc_date_str, '%Y-%m-%d %H:%M:%S.%f%z')
6
7print(dt)
8>> 2018-04-01 20:10:56.123000+09:00

タイムゾーンなし日付文字列からdatetime

厄介なのが、 タイムゾーンのない日付文字列をdatetimeに変換する 場合です。 日付文字列がどのタイムゾーンのデータを表しているか を調べる必要があります。 少し邪道感ありますが、データ仕様(タイムゾーンが何か)を確認した後に文字列結合してしまうのが楽ちんです。

 1# タイムゾーンなし日付文字列(文字列結合)
 2import datetime
 3
 4utc_date_str = '2018-04-01 20:10:56'
 5# JSTとして取扱う
 6dt = datetime.datetime.strptime(utc_date_str + '+0900', '%Y-%m-%d %H:%M:%S%z')
 7
 8print(dt)
 9print(dt.tzinfo)
10>> 2018-04-01 20:10:56+09:00
11>> UTC+09:00

別パターンは dateutil を使うパターンも書いておきます。 dateutilparse 関数を使用する際に tzinfos を引数に与えることで指定のtimezoneで処理をしてくれる書き方です。 先程の例と比べて、パッと見でどこのタイムゾーンかが識別しやすくなる、という利点があります。

1# タイムゾーンなし日付文字列(dateutilを使う)
2import datetime
3from dateutil.parser import parse
4from dateutil.tz import gettz
5
6tzinfos = {'JST' : gettz('Asia/Tokyo')}
7date_str = '2018-04-01 20:10:56'
8str_to_dt = parse(date_str + ' JST', tzinfos=tzinfos)
9print(str_to_dt)

日時データを扱う上で注意すべきこと

naiveとaware

そもそもPythonで日時データを扱う場合には、naiveaware の2種類のオブジェクトがあることに注意が必要です。

以下、Pythonの公式ドキュメントから引用します。

  • aware オブジェクト

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

  • naive オブジェクト

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

つまり、タイムゾーンに依存したデータを扱いたい場合には aware オブジェクトが必要なことを意味します。

しかし厄介なのが、型とaware/naiveオブジェクトの関係です。型に対して利用するオブジェクトが一意に決まりません。

オブジェクト
datenaive
timenaive または aware
datetimenaive または aware

time 型と datetime 型がそれぞれ、 awarenaive かは以下で確認できます。

オブジェクトawareになる条件naiveになる条件
timeオブジェクト tの t.tzinfo が None でなく t.tzinfo.utcoffset(None) が None を返さない場合aware以外の場合
datetimeオブジェクト dの d.tzinfo が None でなく d.tzinfo.utcoffset(d) が None を返さない場合d.tzinfo が None であるか d.tzinfo が None でないが d.tzinfo.utcoffset(d) が None を返す場合

マシン上のタイムゾーンで処理しないように注意する

awareとnaiveに留意せずにタイムゾーン変換の処理を書くと、動作環境によって得られる結果が変わってしまうため、注意が必要です。

 1# 文字列から日付(実行マシン上のタイムゾーンに引きずられる)
 2import datetime
 3from pytz import timezone
 4import pytz
 5
 6# タイムゾーンなし文字列からdatetimeに変換する
 7date_str = '2018-04-01 20:10:56'
 8# この処理で得られるstr_to_dtはnaive
 9str_to_dt = datetime.datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
10print("Str to dt")
11print(str_to_dt)                      # 2018-04-01 20:10:56
12print(str_to_dt.timestamp())          # 1522581056.0
13print(str_to_dt.tzname())             # None
14
15# 以下、naiveな時刻をベースに演算すると、ずれる
16utc = timezone('UTC')
17utc_dt = str_to_dt.astimezone(utc)
18print("UTC dt")
19print(utc_dt)                         # 2018-04-01 11:10:56+00:00
20print(utc_dt.timestamp())             # 1522581056.0
21print(utc_dt.tzname())                # UTC
22print(utc_dt.tzinfo.utcoffset(utc_dt))# 0:00:00
23
24jst = timezone('Asia/Tokyo');
25jst_dt = str_to_dt.astimezone(jst);
26print("JST dt")
27print(jst_dt)                         # 2018-04-01 20:10:56+09:00
28print(jst_dt.timestamp())             # 1522581056.0
29print(jst_dt.tzname())                # JST
30print(jst_dt.tzinfo.utcoffset(jst_dt))# 9:00:00

この例ではタイムゾーンなしの文字列からdatetime型の str_to_dt を作成するのですが、 そこから astimezone 関数を使って任意のタイムゾーンへ変換しようとする際に、 実行環境上のタイムゾーンから、変換先のタイムゾーンへの相対的な計算が行われます。

そのため、パブリッククラウドで複数リージョンを使っている場合などには、 プログラムの展開先によって振る舞いが異なる可能性があるため、注意が必要でしょう。

まとめ

今回はPythonで日付文字列からdatetime型に変換するときの方法を書きました。

日付文字列を変換する場合には 文字列の中にタイムゾーン情報が含まれているか を気をつけて処理をすると良いです。

得られるオブジェクトが awarenaive なのかを意識した上でタイムゾーン変換の処理を行わないと、うっかり手痛い変換ミスになってしまうので注意が必要だからです。

私の場合、基本的には「データ仕様を確認して、タイムゾーンあり文字列として変換してしまう」方で処理をしようと思いました。

参考にさせていただいたサイト