2011年4月17日日曜日

Pythonの時刻処理(時計、タイムゾーン変換)

Pythonでの時間の扱いがわけわかんないので、少しまとめておく
(Python 2.6で動作確認)

標準モジュール

o time

低レベルの時刻処理。Cの関数呼び出し。
時刻はタイムスタンプ(整数型、time_t)、または構造体(struct_time)で表現。

o datetime

高レベルの時刻処理。
オブジェクトとして時刻を扱える。

o calendar

カレンダーを扱うためのライブラリ。

拡張モジュール

拡張パッケージをいくつか導入(pip install)しておくと便利。

o pytz

タイムゾーンの一覧。

o python-dateutil

日付関連の便利なユーティリティ。

タイムゾーンの扱い

アプリケーション内での時刻処理には、datetimeオブジェクトを使う。datetimeは、明示的に指定しない限りは「タイムゾーンなし」になる。このオブジェクトが示すのは「システムの時計」であり、地球上のどこかの「現地時間」を表してはいない。(Pythonでは、前者を"native object"、後者を"aware object"と呼ぶ)

>>> datetime.datetime.now()
datetime.datetime(2011, 4, 17, 19, 15, 50, 533798)

タイムゾーンを明示的に指定したければ、pytzを利用する。

>>> datetime.datetime.now(tz=pytz.timezone('Asia/Tokyo'))
datetime.datetime(2011, 4, 17, 19, 15, 52, 863696, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)
または
>>> pytz.timezone('Asia/Tokyo').localize(datetime.datetime.now())
datetime.datetime(2011, 4, 17, 19, 15, 52, 863696, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)

時刻表現の標準化

タイムゾーンの変換を考える前に、時刻の表現を標準化したい。

まず、時刻はすべてUTCで持つことにする。タイムスタンプ(整数)で現在時刻を得るには次のようにする。

>>> int(time.time())
1303036188

datetimeで現在時刻を得るには次のようにする。

>>> datetime.datetime.utcnow()
datetime.datetime(2011, 4, 17, 10, 30, 50, 193172)

タイムスタンプとdatetimeの相互変換は次のようにする。

>>> dt = datetime.datetime.utcnow()
>>> calendar.timegm(dt.timetuple())
1303036386
>>> datetime.datetime.utcfromtimestamp(1303036386)
datetime.datetime(2011, 4, 17, 10, 33, 6)

datetimeはバイト表現に向いていないので、ファイル等に格納するときは64ビット整数のタイムスタンプとし、アプリケーションでdatetimeに変換する。ただし、DBに格納するときはdatetimeのままO/Rマッパーに渡した方が扱いやすい。

タイムゾーンの変換

上記のように標準化された時刻は、暗黙的にUTCでの時刻を示しているが、オブジェクトとしてはタイムゾーンの情報を持っていない。タイムゾーンを扱うときには、それがUTCであることを明示的に示してやる。

>>> pytz.utc.localize(dt)
datetime.datetime(2011, 4, 17, 10, 33, 6, 575205, tzinfo=<UTC>)

その上で、目的のタイムゾーンへと変換を行う。

>>> pytz.utc.localize(dt).astimezone(pytz.timezone('Asia/Tokyo'))
datetime.datetime(2011, 4, 17, 19, 33, 6, 575205, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)

一連の処理を簡単にするためにクラス化を行う。例えば、次のようなクラスを考える。

class User(object):
    def __init__(self):
        self.timezone = 'Asia/Tokyo'

標準化された時刻dtを、オブジェクトのタイムゾーンに変換するためのメソッドをlocalizeとする。

    def localize(self, dt):
        return pytz.utc.localize(dt).astimezone(pytz.timezone(self.timezone))

同様に、標準化されたタイムスタンプを、オブジェクトのタイムゾーンに変換するためのメソッドをlocaltimeとする。

    def localtime(self, timestamp):
        return self.localize(datetime.datetime.utcfromtimestamp(timestamp))

時刻の表示

ローカル時間へと変換された時刻は、通常のdatetimeオブジェクトとしてフォーマットできる。

>>> u = User()
>>> t = int(time.time())
>>> print u.localtime(t)
2011-04-17 20:26:14+09:00
>>> print u.localtime(t).ctime()
Sun Apr 17 20:26:14 2011
>>> print u.localtime(t).isoformat()
2011-04-17T20:26:14+09:00
>>> print u.localtime(t).strftime('%Y-%m-%d %H:%M:%S %Z')
2011-04-17 20:26:14 JST

TODO: ロケールを指定した時刻の出力について。

時刻の読み込み

フォーマットされた時刻を読み取るにはstrptimeが使える。

>>> datetime.datetime.strptime('2011-04-18 06:02:32 GMT', '%Y-%m-%d %H:%M:%S %Z')
datetime.datetime(2011, 4, 18, 6, 2, 32)

ただ、文字列に含まれるタイムゾーンは、うまく読み取ってくれないみたい。あらかじめ"GMT"のようにわかっているならそれを前提としてもいいが、そうでなければ自前で処理する必要がありそう。(探せばライブラリがあるかも)

python-dateutilという外部モジュールを使うと、明示的にフォーマットを指定せずとも、ライブラリ側でよきにはからってくれるみたい。(タイムゾーンもある程度みてくれるようだ)

>>> dateutil.parser.parse('Sun, 17 Apr 2011 21:32:07 JST')
datetime.datetime(2011, 4, 17, 21, 32, 7, tzinfo=tzlocal())

タイムゾーン付きで読んだ時刻は、次のようにして標準化する。

>>> dt = dateutil.parser.parse('Sun, 17 Apr 2011 21:32:07 JST')
>>> dt.astimezone(pytz.utc).replace(tzinfo=None)
datetime.datetime(2011, 4, 17, 12, 32, 7)
>>> calendar.timegm(dt.astimezone(pytz.utc).timetuple())
1303043527

日付の計算

将来の時間を計算するときは、datetime.timedeltaを用いる。

>>> dt = datetime.datetime.utcnow()
>>> dt
datetime.datetime(2011, 4, 17, 12, 36, 18, 792871)
>>> dt + datetime.timedelta(seconds=60)
datetime.datetime(2011, 4, 17, 12, 37, 18, 792871)
>>> dt + datetime.timedelta(days=30)
datetime.datetime(2011, 5, 17, 12, 36, 18, 792871)

ただ、datetime.timedeltaは秒単位、日単位の指定しか出来ないので、「半年後」などを指定するのは難しい。そのようなときは、dateutil.relativedelta.relativedeltaが使える。

>>> dt + dateutil.relativedelta.relativedelta(months=6)
datetime.datetime(2011, 10, 17, 12, 36, 18, 792871)

時間の差分

「〇〇分前」とか「残り○時間○分」とか表示したいときは、Djangoであればdjango.utils.timesinceが使える。

>>> dt = datetime.datetime.now()
>>> django.utils.timesince.timesince(dt - datetime.timedelta(seconds=300))
u'5 minutes'
>>> django.utils.timesince.timeuntil(dt + datetime.timedelta(seconds=300))
u'4 minutes'

これらは標準のフィルタとして次のように使える。

{{ blog_date|timesince:comment_date }}

TODO: タイムゾーンを考慮した記述。

その他

datetime.datetime.utcnow()を使うとマイクロ秒まで返されてしまう。秒単位の方がよければ次のようにする。

>>> datetime.datetime.utcnow().replace(microsecond=0)
datetime.datetime(2011, 4, 17, 13, 6, 35)


0 件のコメント:

コメントを投稿