目次
datetime, pytz, dateutil
Python3の日付処理関連モジュールについての覚え書き。
- 公式ドキュメント
- 日本語でのまとまった記事
日付関連処理は「月によって日数が違う」「閏年の有無」など統一的な処理がしにくいことに加え、 国際的にはタイムゾーンやサマータイムを考慮する必要があるなど、とにかくプログラムで扱いづらい。
- datetime
- 標準モジュールの1つで、日付や時刻周りを扱う
- これ1つでも、基本的なことならさほど困らずに出来る
- pytz
- タイムゾーンが設定しやすくなる
- python-dateutil
- あれば便利な機能を拡張してくれる
- サードパーティ製だが、上記Python公式ドキュメントでも推薦されている
Python3での日付処理は、この3つがあれば基本は足りるはず。
なお、datetime
モジュールには、日時を同時に扱う datetime.datetime
の他に、日付のみ、時刻のみを扱う datetime.date, datetime.time
も存在する。
要所で使い分ければ良いが、ここでは datetime.datetime
を取り上げる。
Aware, Naive
datetime.datetime
オブジェクトには、この2種類の“状態”がある。
タイムゾーン情報を持つか持たないかでオブジェクトの挙動が異なる。公式でも真っ先に説明されている重要な概念。
- Aware: タイムゾーン(UTCとの時差情報)を持つ
- Naive: タイムゾーン(UTCとの時差情報)を持たない
基本的にAwareで扱う意識を持っておく方がよい。
時刻の差分を取ったり統計処理するような場合はUNIX時刻に変換することが多いと思うが、 UNIX時刻の定義は「UTCで 1970/01/01 00:00:00 からの経過時間」なので、Awareと似た概念となる。
Naiveなオブジェクトは、PCのロケールに従って扱われる。
現在の状態の判定は、datetime.datetime
オブジェクト dt
が以下を両方満たす場合がAware、それ以外がNaiveとされる。
dt.tzinfo
がNone以外dt.tzinfo.utcoffset(dt)
がNone以外
簡易使い方
import datetime # タイムゾーン jst = datetime.timezone(datetime.timedelta(hours=9)) # 現在時刻 (PC時刻) dt = datetime.datetime.now() # Naive: 2019-01-01 12:34:56.789012 dt = datetime.datetime.now(jst) # Aware: 2019-01-01 12:34:56.789012+09:00 # パース(文字列 => datetimeオブジェクト) dt = datetime.datetime.strptime('2019-01-01 12:34:56.789012', '%Y-%m-%d %H:%M:%S.%f') # Naive dt = datetime.datetime.strptime('2019-01-01 12:34:56.789012+0900', '%Y-%m-%d %H:%M:%S.%f%z') # Aware # フォーマット(datetimeオブジェクト => 文字列) date_str = dt.strftime('%Y/%m/%d %H:%M:%S') # Unix時刻 => datetimeオブジェクト dt = datetime.datetime.fromtimestamp(1546313696.789012) # Naive dt = datetime.datetime.fromtimestamp(1546313696.789012, tz=jst) # Aware # datetimeオブジェクト => Unix時刻 unix = dt.timestamp()
タイムゾーンを扱うオブジェクト
主として、「pytz」と「datetime.timezone」の2つがある。
- datetime.timezoneは機能的にシンプルだが、記述がやや冗長、サマータイムなどは非対応
- pytzは多機能だが、注意を要する挙動がある
より正確に言うと、datetime.tzinfo
抽象クラスというのがあり(ドキュメント参照)、
これが求める各種メソッドを具象化したものならタイムゾーンのオブジェクトとして扱える。
datetime.timezone
datetime.timezoneは、JapanやTokyoなどの文字列での対応辞書は持ってなくて、UTCとの差分をtimedeltaで直接指定する形となる。
jst = datetime.timezone(datetime.timedelta(hours=9)) d = datetime.datetime(2000, 1, 1, tzinfo=jst) # => 2000-01-01 00:00:00+09:00
また、tzinfo
抽象クラス自体にはサマータイムを扱う仕組みはあるものの、
datetime.timezone
は未実装なのか常にNoneを返すようになっているので、サマータイム等の考慮はできない。
pytz
pytzは、文字列での指定も出来るし、サマータイムも考慮できる。便利。
ただし、以下に示す注意点がある。
日本時間(Japan または Asia/Tokyo)を扱うと +09:19
と、19分ズレることがある。
これは何故かというと、明治政府により日本標準時が1888年に適用開始されるまでは、
事実上の日本時刻は東京の現地時刻(LMT)が使われていて、それがUTCとは9時間19分の時差だった(と考えられる)ため。
いや、現代のデータなんだから日本標準時の方を使ってよ……。
ズレるのは、datetime()
や replace()
等の tzinfo=
引数にpytzオブジェクトを指定したとき。
pythonの datetime.tzinfo
は法令ががらっと変わるような変化は対応していない。
一方、pytzは同じ地域でも時代によって時差が異なることが考慮され、同じAsia/Tokyoでも複数のtzinfoの実装が定義されている。
pytzオブジェクトが tzinfo
の引数として渡されたとき、とりあえずその地域の tzinfo から1つを渡さないといけないが、
その時点ではpytzはどの時刻を処理しようとしているかが見えないので、とりあえず最初に定義されているものを渡す。
それが日本時刻ではたまたま旧い方だった、ということらしい。
pytz.timezone.localize()
なら、データの時刻が考慮された“正しい”タイムゾーンが選択されるのでこちらを使う。
# 非推奨 jst = pytz.timezone('Asia/Tokyo') d_aware = datetime.datetime(2000, 1, 1, tzinfo=jst) # => 2000-01-01 00:00:00+09:19 d_naive = datetime.datetime.strptime('2000-01-01 00:00:00', '%Y-%m-%d %H:%M:%S') d_aware = d_naive.replace(tzinfo=jst) # => 2000-01-01 00:00:00+09:19
# 推奨 jst = pytz.timezone('Asia/Tokyo') d_naive = datetime.datetime(2000, 1, 1) d_aware = jst.localize(d_naive) # => 2000-01-01 00:00:00+09:00 d_naive = datetime.datetime.strptime('2000-01-01 00:00:00', '%Y-%m-%d %H:%M:%S') d_aware = jst.localize(d_naive) # => 2000-01-01 00:00:00+09:00
なお、それでいうと1888/01/01は +09:00
が正しいはずなのだが、localizeしても +09:19
となってしまう。
調べると 1901/12/14 05:45:51~52 の境目で +09:19
から +09:00
に切り替わるが、
これはちょうどUNIX時刻が INT_MIN である -2147483648 を超える境界なので、その辺が関係してそう。
いずれにしろ、あまり古い時刻はdatetime型で扱うべきでない。
2038年問題にあたる 2038/01/19 03:14:08 でも同様の問題が起こるかと思ったが、こちらは大丈夫(+09:00
)だった。
速度
以下の記事ではpytzの処理は遅いとの検証結果が書かれている。大量の日時を扱う際にはdatetime.timezoneの方がよいか。
dateutil
- 明日、来週、来月、などの加減算
- 時間毎、日毎、などのイテレート
- 明示的にフォーマット指定しなくてもよしなに推定解釈してくれるパーサ
などの機能がある。痒いところに手が届く。
パーサに関しては、まぁdatetimeにもあるし、思わぬ変換をされないとも限らないのでフォーマットは明示しといた方が間違いが無いかなとは思う。
pipやcondaでインストールする際のパッケージは「python-dateutil」だが、import時は「dateutil」である点に注意。
from dateutil.rrule import rrule, MONTHLY start = datetime.datetime.strptime('2019/01/29', '%Y/%m/%d') until = datetime.datetime.strptime('2019/06/05', '%Y/%m/%d') # startからuntilを超えるまで、1ヶ月ごとにイテレート for dt in rrule(MONTHLY, dtstart=start, until=until): print(dt) # ==== # 2019-01-29 00:00:00 # 2019-03-29 00:00:00 (存在しない日付は飛ばされる) # 2019-04-29 00:00:00 # 2019-05-29 00:00:00
from dateutil.relativedelta import relativedelta dt = datetime.datetime.strptime('20190129', '%Y%m%d') # 5週間後の1日前を計算 dt += relativedelta(weeks=5, days=-1) print(dt) # ==== # 2019-03-04 00:00:00
ハマりどころ・トラブルシューティング
UNIX時刻が小さい時の timestamp 取得時エラー
- Windows 7
- Python 3.7.5
Windows依存らしい。
1970/01/01 など、UNIX時刻が小さいNaiveなdatetimeオブジェクトから、d.timestamp()
でUNIX時刻を取り出そうとすると、以下のエラーが発生する。
OSError: [Errno 22] Invalid argument
具体的には、UNIX時刻にした結果が86400(1日の秒数)未満になる場合に発生するらしい。
これは、タイムゾーンを与えてAwareにしてやると解消する。 よくわからんが、NaiveなdatetimeをUNIX時刻にする際、PCのAPIを使おうとするが、Windowsからlocaltimeを読み出す際の挙動がそうなっているかららしい。
また、他の記事を見るに、datetime.fromtimestamp()
に小さい値を与えても発生していたようだが、こちらは報告されて3.7時点では解消している(多分)。