datetime
Python3
ISO8601
Arrow

ISO 8601 string to datetime without arrow

More than 1 year has passed since last update.


背景・動機

Pythonで時刻を扱うにはArrowが便利だが、依存しすぎると戻り値がarrowオブジェクトなのかdatetimeオブジェクトなのか統一が取れない事態に陥る可能性がある(あった)。

混ぜ過ぎ注意。

「よし、arrowから脱却しよう」と意気込んでみても、arrow.get()の壁が立ちはだかる。何でも引数に投げとけば、よしなに時刻オブジェクトに変換してくれる人をダメにする関数である。しかも副作用として、arrowオブジェクトを返す。arrow.get()を求めてarrowを使い、arrow.get()に依存しすぎて破滅する。

arrow.get(string)は、ISO 8601拡張形式1の文字列をarrowオブジェクトに変換してくれる。

これに習ってdatetimeオブジェクトへ変換する実装を行い、arrow.get()脱却のための代替関数を実装してみる。


ISO 8601

ISO(International Organization for Standardization, 国際標準化機構)の規格8601

https://www.iso.org/iso-8601-date-and-time-format.html

タイトルは"Date and time format"

最新改訂版は2004年のISO 8601:2004でタイトルは"Data elements and interchange formats -- Information interchange -- Representation of dates and times"

わざわざ「Information interchange(=情報交換のための)」とあるのが感慨深い。

DISとは国際規格原案(Draft International Standard)のことで、照会段階であることを意味する。

つまりこの先変わる可能性があり、今も新規格が吟味中のようだ。

仕様は必ず公式を参照すべきだが、Previewを押すと何と有料ぽい。

非公式だが日本語の有用なリンクを載せておく。

https://ja.wikipedia.org/wiki/ISO_8601

http://www.coppermine.jp/docs/notepad/2016/12/iso-8601.html


拡張形式の仕様

詳細は先述のリンクを参照。要点のみまとめる。




セパレイター



マイクロ秒
タイムゾーン

例1
2018
02
26
T
01
02
34
.123450
+09:00


Pythonでの実装紹介

typingを利用しているのでpython >= 3.5が必要だが、不要なら2系でも動くかもしれない)


ISO 8601拡張形式の文字列をdatetimeに


iso8601_to_datetime.py

from datetime import datetime

import re

iso_format=re.compile('^\d{4}-\d{2}-\d{2}(( |T)\d{2}:\d{2}:\d{2}(([.,])\d{1,6})?([+-]\d{2}(:)?\d{2})?)?$')

def iso8601_to_datetime(string: str) -> datetime:
m=iso_format.match(string)
if m is None:
return None
iso='%Y-%m-%d'
if m.group(1) is not None:
iso+=m.group(2)+'%H:%M:%S'
if m.group(3) is not None:
iso+=m.group(4)+'%f'
if m.group(5) is not None:
iso+='%z'
if m.group(6) is not None:
string=string[:-3]+string[-2:]
return datetime.strptime(string, iso)


$python -i iso8601_to_datetime.py

>>> iso8601_to_datetime('2018-02-25T00:00:00.123450+09:00')
2018-02-25 00:00:00.123450+09:00

仕様に100%沿っているわけではないので、必要に応じて手を入れて見てください(YYYY-MM未対応)

%zが曲者で、拡張形式を認めてくれない

%fはよしな

基本形式も含めるなら、正規表現で0回または1回の繰り返しを表す?をつけてください

m.group()がNoneか、文字列の足し算をするときに確認しないとエラー


arrow.get()代替

arrowの内部実装では、一度文字列をtimestampに変換して再出力していた。

正規表現のみで代替してみる。ただし後述の通り、100%代替ではないので注意。


arrow_get.py

from datetime import datetime, timezone, timedelta

from typing import TypeVar
import .iso8601_to_datetime

UTC=timezone(timedelta(hours=0))
ARROW = TypeVar('ARROW', str, int, float, datetime, None)
def arrow_get(obj: ARROW) -> datetime:
if obj is None:
return datetime.now(tz=UTC)
elif isinstance(obj, str):
dt=iso8601_to_datetime(obj)
if dt is None:
obj=float(obj)
else:
obj=dt
if isinstance(obj, datetime) and obj.tzinfo is not None:
return obj
elif isinstance(obj, int) or isinstance(obj, float):
obj=datetime.fromtimestamp(float(obj))
return obj.replace(tzinfo=UTC)



  • 仕様に沿った部分


    • 引数がないときは現在のUTC時刻

    • 入力がawareなdatetimeオブジェクトならそのtimezoneを保持、naiveならUTCとして返す

    • etc...



  • 未実装箇所


    • 引数がarrow, date, tzinfo, time.structの入力は未実装




なぜarrowでは基本形式を入力としていないか

arrow.get()で引数に基本形式を入れると、エラーか、例えば'20180225'はtimestampとして扱われる

timestamp文字列も引数に認めているため、基本形式は入力としていないのだと思う

実際に実装を見ると、パース(文字列マッチング)時の優先順位が高かった


timezone

標準ライブラリだけで対応したければ上記実装の通りUTCを定めるが、ズレを数値のみで表現する必要がある。

実用的なのはdateutilで、IANA databaseを持っているから文字列で表現できる。

"powerful extensions to datetime"とあるように純粋なdatetime拡張で、arrowみたく独自オブジェクトが混ざり込んだりはしてない。公式ドキュメント内でも紹介されている。


よしな対応

$python

>>> arrow.get('2018/02/25').datetime
2018-02-25 00:00:00+00:00
>>> arrow.get('2018-02-25+09:00').datetime
2018-02-25 00:00:00+00:00

正規表現を変える

^\d{4}([-/])\d{2}([-/]\d{2}(( |T)\d{2}:\d{2}:\d{2}(([.,])\d{1,6})?([+-]\d{2}(:)?\d{2})?)?)?$

iso8601_to_datetime()の最初のデリミタを正規表現のマッチから判別する

iso='%Y'+m.group(1)+'%m'+m.group(1)+'%d'

m.group()の数値が変わるので注意


その他

iso8601というpip pkgを見つけた

こちらは基本形式にも対応してる様子





  1. arrow.get()のドキュメントにはISO-8601-formatted strとあるが、後述の通り基本形式は対応していない