2018/12/03

[Python] datetime オブジェクト、Unix time、文字列の相互変換まとめ

Python で [datetime オブジェクト]() 、 [Unix time](https://ja.wikipedia.org/wiki/UNIX%E6%99%82%E9%96%93)、文字列を相互変換する方法はいろいろあります。
中には直感に反するものもありnative になりそうなのに、ローカルのタイムゾーンを適用しちゃうとか、
過去に1度間違えたのに、再度間違えるということもしばしば。

そこで、2度と間違えないように、個人的にベストと思う方法をまとめてみました。



変換には様々な方法があるので、上記が唯一の方法というわけではありませんが、
なるべく、安全かつシンプルになるものを選んだつもりです。

また、変換可能でも、安全では無いと判断したものは矢印を引いていません安全な方法をご存じの方は教えていただけると幸いです

以下2点は Python のバージョンによっては使用できないので、代替案を掲載しておきました。
- `datetime.timestamp()` (3.3 以降で使用可)
- `datetime.fromisoformat()` (3.7 以降で使用可)

なお、掲載コードは以下のように import 済みとします。
```python
from datetime import datetime
import pytz
```

## 1. Unix time と datetime オブジェクトの相互変換
### Unix time → datetime (aware)
Unix time から datetime オブジェクトを生成するには、[fromtimestamp()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.fromtimestamp) を使用します。

この時、`tz` は必ず指定するようにしましょう。そうしないと、**ローカルのタイムゾーンを適用した native オブジェクト** が生成されてしまいます。
例外的に、[datetime.now()](https://docs.python.org/3/library/datetime.html#datetime.datetime.now) 等でローカル時間を取得して処理する時は `tz` を指定しなくても良いかもしれません。
```python
datetime.fromtimestamp(1541030400, tz=pytz.utc)
# 2018-11-01 00:00:00+00:00

datetime.fromtimestamp(1541030400, tz=pytz.timezone('Asia/Tokyo'))
# 2018-11-01 09:00:00+09:00

datetime.fromtimestamp(1541030400)  # native
# 2018-11-01 09:00:00  (+09:00 のタイムゾーン設定されているマシンで実行した場合)
# 2018-10-31 20:00:00  (-05:00 のタイムゾーン設定されているマシンで実行した場合)
```

### Unix time → datetime (native)
Unix Time から native オブジェクトを生成したい場合は、[utcfromtimestamp()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.utcfromtimestamp) が使えます。
```python
datetime.utcfromtimestamp(1541030400)
# 2018-11-01 00:00:00
```

直感的には、UTC な aware オブジェクトが生成されて欲しいところ。
間違えないように気をつけましょう。

### datetime (aware) → Unix time
datetime オブジェクトから Unix time に変換する場合は、[timestamp()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.timestamp) を使用します。

この時、native オブジェクトから変換すると、**ローカルのタイムゾーンを適用して変換**されてしまいます。
こちらも、[fromtimestamp()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.fromtimestamp) と同様、[datetime.now()](https://docs.python.org/3/library/datetime.html#datetime.datetime.now) 等で取得したローカル時間の native オブジェクトを処理する時は良いかもしれませんが、基本的には使わないようにした方が良いでしょう。
```python
dt = datetime.fromtimestamp(1541030400, tz=pytz.utc)
int(dt.timestamp())
# 1541030400

dt = datetime.fromtimestamp(1541030400, tz=pytz.timezone('Asia/Tokyo'))
int(dt.timestamp())
# 1541030400

dt = datetime.utcfromtimestamp(1541030400)  # native
int(dt.timestamp())
# 1540998000  (+09:00 のタイムゾーン設定されているマシンで実行した場合)
# 1541044800  (-05:00 のタイムゾーン設定されているマシンで実行した場合)
```

### timestamp() が使えない時
[timestamp()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.timestamp) は Python 3.3 以降で導入されました。
それ以前のバージョンで Unix time に変換したい場合は、以下のように calendar を使うと良いようです。
この時、UTC の aware に変換しないと想定外の値に変換されてしまうので注意が必要です。
```python
import calendar
dt = datetime.fromtimestamp(1541030400, tz=pytz.timezone('Asia/Tokyo'))
calendar.timegm(dt.astimezone(pytz.utc).timetuple())
# 1541030400
```

> 参考
>
> [Pythonの日付処理とTimeZone | Nekoya press](http://nekoya.github.io/blog/2013/06/21/python-datetime/)


## 2. aware と native の相互変換
### datetime (aware) → datetime (native)
aware から native にする場合は `replace(tzinfo=None)` を使用します。
```python
dt = datetime.fromtimestamp(1541030400, tz=pytz.timezone('Asia/Tokyo'))
# 2018-11-01 09:00:00+09:00

dt.replace(tzinfo=None)
# 2018-11-01 09:00:00
```

### datetime (native) → datetime (aware)
native から aware に変換する時は `replace(tzinfo=xxx)` を**使ってはいけません**。
代わりに `pytz.localize()` を使用します。
```python
dt = datetime.utcfromtimestamp(1541030400)
# 2018-11-01 00:00:00

pytz.timezone('Asia/Tokyo').localize(dt)
# 2018-11-01 00:00:00+09:00
```

タイムゾーンは日付によってオフセットが変わるのでサマータイムのある地域だと特に、このような仕様になっているのですが、
かなり紛らわしく間違いやすいので注意が必要です。

> 参考
> - [Pythonでdatetimeにtzinfoを付与するのにreplaceを使ってはいけない | Nekoya press](http://nekoya.github.io/blog/2013/07/05/python-datetime-with-jst/)
> - [pytzの仕様が変わっている - Qiita](https://qiita.com/higitune/items/0ca244373d380cf1c060)


## 3. aware のタイムゾーンを変更する
同じ Unix time を持つ別のタイムゾーンに変換するには [astimezone()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.astimezone) を使用します。
```python
dt = datetime.fromtimestamp(1541030400, tz=pytz.timezone('Asia/Tokyo'))
# 2018-11-01 09:00:00+09:00

dt.astimezone(pytz.timezone('America/New_York'))
# 2018-10-31 20:00:00-04:00
```

なお、`astimezone(None)` は native に変換するのではなく、**ローカルのタイムゾーンに変更**します。
最初、メリットがわからなかったのですが、これを使ってローカルタイムゾーンを取得するという技が紹介されていましたかなりトリッキー。普通にローカルタイムゾーンを取得出来ればそれで良いのだが…。

> 参考
>
> [datetime - Python: Figure out local timezone - Stack Overflow](https://stackoverflow.com/a/39079819)


## 4. 文字列と datetime オブジェクトの相互変換
書式文字列を使用して、任意の文字列と datetime を相互変換することが出来ます。

> 書式文字列のディレクティブ一覧は以下
>
> [strftime() and strptime() Behavior](https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior)


### 文字列 → datetime
文字列から datetime オブジェクトに変換する場合は [strptime()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.strptime) を使用します。

`%z` を含む場合は aware になり、含まない場合は native になります。
なお、`%Z`(大文字)は `strptime()` では使用できないようです。

```python
datetime.strptime("2018/11/01 09:00", "%Y/%m/%d %H:%M")
# 2018-11-01 09:00:00

datetime.strptime("2018/11/01 09:00+0900", "%Y/%m/%d %H:%M%z")
# 2018-11-01 09:00:00+09:00
```

### datetime → 文字列
datetime から文字列に変換するには [strftime()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.strftime) を使用します。

native に対し `%z` を指定しても何も表示されません。
```python
dt = datetime.utcfromtimestamp(1541030400)
# 2018-11-01 00:00:00
dt.strftime("%Y/%m/%d %H:%M%z")
# 2018/11/01 00:00

dt = datetime.fromtimestamp(1541030400, tz=pytz.timezone('Asia/Tokyo'))
# 2018-11-01 09:00:00+09:00
dt.strftime("%Y/%m/%d %H:%M%z")
# 2018/11/01 09:00+0900
dt.strftime("%Y/%m/%d %H:%M(%Z)")
# 2018/11/01 09:00(JST)
```


## 5. ISO フォーマット文字列と datetime オブジェクトの相互変換
[ISO 8601 形式](https://ja.wikipedia.org/wiki/ISO_8601) と datetime の相互変換も提供されています。

ただ、ISO 8601 にも基本と拡張が存在し、拡張形式にしか対応していない様子。
さらに UTC を表す "Z" には対応していないなど、使い勝手はイマイチです。

個人的には [strptime()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.strptime) と [strftime()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.strftime) を使用した方が柔軟性が高く、便利かと思います。

### ISO フォーマット文字列 → datetime オブジェクト
ISO フォーマット文字列から datetime に変換するには [fromisoformat()](https://docs.python.org/3.7/library/datetime.html#datetime.datetime.fromisoformat) を使用します。

上述のように、拡張形式にしか対応していませんセパレータは "T" に限定されず、任意の1文字で良い。
```python
iso_string = "2018-11-01T09:00:00+09:00"
datetime.fromisoformat(iso_string)
# 2018-11-01 09:00:00+09:00
```

[fromisoformat()](https://docs.python.org/3.7/library/datetime.html#datetime.datetime.fromisoformat) は Python 3.7 で導入されたので、
それ以前の場合は [strptime()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.strptime) で代用することになります。

厄介なのは ISO フォーマットと `strptime()` のタイムゾーンの記述方法が異なる点です。
ISO は `+09:00` なのに対し、 `strptime()` は `+0900` でコロンがありません。

あまり良い方法は提供されていないようなので、愚直に置換してやるのがよさそうです

> 参考
>
> [python - parsing timezone with colon - Stack Overflow](https://stackoverflow.com/a/45300534)

```python
iso_string = "2018-11-01T09:00:00+09:00"

# タイムゾーンのフォーマットを変更
dt_string = iso_string if ":" != iso_string[-3:-2] else iso_string[:-3]+iso_string[-2:]
# 2018-11-01T09:00:00+0900

datetime.strptime(dt_string, "%Y-%m-%dT%H:%M:%S%z")
# 2018-11-01 09:00:00+09:00
```

### datetime オブジェクト → ISO フォーマット文字列
datetime から IOSフォーマット文字列に変換するには [isoformat()](https://docs.python.org/3.6/library/datetime.html#datetime.datetime.isoformat) を使用します。

```python
dt = datetime.fromtimestamp(1541030400, tz=pytz.timezone('Asia/Tokyo'))
# 2018-11-01 09:00:00+09:00

dt.isoformat()
# 2018-11-01T09:00:00+09:00

dt.isoformat(timespec='hours')
# 2018-11-01T09+09:00
```

`timespec` や `sep` を用いて多少フォーマットを変更できますが、上述のように全ての ISO 8601 形式をカバーしているわけではないので、使用できるシーンは限られそうです。


## 6. コンストラクタ
コンストラクタでタイムゾーンを指定すると、native に対し `replace()` したのと同等になってしまいます。

コンストラクタで `tzinfo` を指定するのではなく、native で作成してから `pytz.localize()` で aware に変換するようにしましょう。

```python
# こちらが正解
dt = datetime(2018, 9, 1)  # native
pytz.timezone('Asia/Tokyo').localize(dt)
# 2018-09-01 00:00:00+09:00

# 以下のようにコンストラクタで tzinfo を指定すると19分というずれが発生
datetime.datetime(2018, 9, 1, tzinfo=pytz.timezone('Asia/Tokyo'))
# 2018-09-01 00:00:00+09:19
```

> 参考
>
> - [[Python] datetime のコンストラクタでタイムゾーンは指定しない方がよい](https://kokufu.blogspot.com/2018/11/python-datetime.html)
> - [pytzの仕様が変わっている - Qiita](https://qiita.com/higitune/items/0ca244373d380cf1c060)
> - [Pythonでdatetimeにtzinfoを付与するのにreplaceを使ってはいけない | Nekoya press](http://nekoya.github.io/blog/2013/07/05/python-datetime-with-jst/)

0 件のコメント: