目次

datetime, pytz, dateutil

Python3の日付処理関連モジュールについての覚え書き。

日付関連処理は「月によって日数が違う」「閏年の有無」など統一的な処理がしにくいことに加え、 国際的にはタイムゾーンやサマータイムを考慮する必要があるなど、とにかくプログラムで扱いづらい。

Python3での日付処理は、この3つがあれば基本は足りるはず。

なお、datetime モジュールには、日時を同時に扱う datetime.datetime の他に、日付のみ、時刻のみを扱う datetime.date, datetime.time も存在する。 要所で使い分ければ良いが、ここでは datetime.datetime を取り上げる。

Aware, Naive

datetime.datetime オブジェクトには、この2種類の“状態”がある。 タイムゾーン情報を持つか持たないかでオブジェクトの挙動が異なる。公式でも真っ先に説明されている重要な概念。

基本的にAwareで扱う意識を持っておく方がよい。

時刻の差分を取ったり統計処理するような場合はUNIX時刻に変換することが多いと思うが、 UNIX時刻の定義は「UTCで 1970/01/01 00:00:00 からの経過時間」なので、Awareと似た概念となる。

Naiveなオブジェクトは、PCのロケールに従って扱われる。

現在の状態の判定は、datetime.datetime オブジェクト dt が以下を両方満たす場合がAware、それ以外がNaiveとされる。

簡易使い方

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.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依存らしい。

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時点では解消している(多分)。