Pythonでサーバーから返されたGMT(UTC)時間を日本時間に変換する

# はじめに

以前投稿した記事[「Twitterでの情報収集を RSS へ（一部）移行」](https://qiita.com/penguinprogrammer/items/ef7b9d8455d3bce5c0ed)のプログラムを改良してたのだが、RSSフィードのなかの日時の変換ではまった。

インターネットにもいろいろな情報があるにはあるのだが、Pythonのバージョンだったり、使うライブラリだったり、OS等によっていろいろあるらしく混乱した。自分の頭の整理のためにもまとめた。

# 環境
+ NanoPi NEO2[^NanoPiNEO2とは]
+ Armbian 22.11.4 （debian 11 (bullseye) ベース）
+ Python V3.9.2
+ dateutil v2.8.1
+ pytz 2022.7.1

:::note warn
Pythonのライブラリは一部OSに依存する（タイムゾーン名、等）。OSによって動作が若干異なる事があるので注意
:::

[^NanoPiNEO2とは]: 中国FriendlyELEC社のARM搭載SBC（シングルボードコンピュータ）。分かりやすく書けば「ラズパイ（Raspberry Pi）みたいなもの」

# やりたかったこと
やりたいことは単純で、サーバーから取得したRSSフィードに含まれている日時の文字列、今回の場合は`'Mon, 13 Feb 2023 10:40:59 GMT'`という形式、を日本時間（＝ローカル時間[^ローカル時間の定義]）に変換すること。

[^ローカル時間の定義]: この記事では「操作者」と「稼働するコンピュータ」が日本でコンピューター上の時間設定も日本時間で、これをローカル時間とする。

# ハマった点(1) 
## 問題点：Awareなdatetimeオブジェクトにならない 

:::note info
PythonのdatetimeオブジェクトにはAware（タイムゾーン情報あり）とNaive（タイムゾーン情報なし）の2種類がある
:::

タイムゾーン情報が付与された日時文字列`'Mon, 13 Feb 2023 10:40:59 GMT'`をdatetimeオブジェクトにしようとすると`datetime.strptime()`では`%Z`で指定しているタイムゾーン情報`GMT`がなくなってしまう。（タイムゾーン情報が欠落したNaiveなdatetimeオブジェクトになってしまう。）

:::note info
日時文字列が`'Mon, 13 Feb 2023 10:40:59+00:00'`のような時差形式で`%z`を使用する場合このようにはならない
:::

```py
import datetime
 
s   = 'Mon, 13 Feb 2023 10:40:59 GMT'
fmt = '%a, %d %b %Y %H:%M:%S %Z'

dt = datetime.datetime.strptime(s, fmt)
print(dt)
#2023-02-13 10:40:59          # タイムゾーン情報がなくなる
print(dt.tzinfo)
#None                         # タイムゾーン情報がなくなる
```

`GMT`の代わりに`UTC`や`JST`が使われていても同じ結果でタイムゾーンの情報がなくなってしまう

```py
import datetime

fmt = '%a, %d %b %Y %H:%M:%S %Z'

dt = datetime.datetime.strptime('Mon, 13 Feb 2023 10:40:59 GMT', fmt)
print(dt, dt.tzinfo)
#2023-02-13 10:40:59 None       # タイムゾーン情報（GMT）がなくなる

dt = datetime.datetime.strptime('Mon, 13 Feb 2023 10:40:59 UTC', fmt)
print(dt, dt.tzinfo)
#2023-02-13 10:40:59 None       # タイムゾーン情報（UTC）がなくなる

dt = datetime.datetime.strptime('Mon, 13 Feb 2023 10:40:59 JST', fmt)
print(dt, dt.tzinfo)
#2023-02-13 10:40:59 None       # タイムゾーン情報（JST）がなくなる
```
しかし、上の例の`datetime.strptime()`の第2引数の`%Z`の部分が無視されているわけではなく`%Z`を省略するとエラーになってしまう。また、不適切なタイムゾーン名、例えば`UTC+9`を指定すると`datetime.strptime()`がエラーになってしまう。

```py

# フォーマットに%Zを指定しない場合
dt = datetime.datetime.strptime('Mon, 13 Feb 2023 10:40:59 GMT',   '%a, %d %b %Y %H:%M:%S')
#エラー

# タイムゾーンとして'UTC+9'を使用した場合
dt = datetime.datetime.strptime('Mon, 13 Feb 2023 10:40:59 UTC+9', '%a, %d %b %Y %H:%M:%S %Z')
#エラー

```
[Pythonの公式ドキュメント「strftime() と strptime() の書式コード」](https://docs.python.org/ja/3/library/datetime.html#strftime-and-strptime-format-codes)の注釈(6)によれば`datetime.strptime()`の第2引数のタイムゾーン(`%Z`)には（日本で使用する場合）'UTC'、'GMT'、'JST'しかダメらしい。[^Windows]

[^Windows]: Windows環境（Windows 10(22H2) 64bit、Python 3.9.13）では`JST`を指定したらエラーになった

## 解決策：dateutilライブラリを使用する

`datetime.strptime()`の代わりに`dateutil.parser.parse()`を使用する。

:::note info
python-dateutilモジュールをインストールする必要がある[^pythonコマンドをつかうべき理由][^Armbianの場合]

[^Armbianの場合]: `#apt install python3-dateutil`でもインストールできたがバージョンが異なっていた（古かった）。システム（周り）で使用されている可能性があるので`apt`コマンドは使用しない方が無難か？

```
python3 -m pip install python-dateutil
```
:::

[^pythonコマンドをつかうべき理由]: 複数バージョンのPythonがインストールされている環境の事を考えると`pip`コマンドよりも`python`コマンドを使用するのが望ましい。([参考ホームページ](https://daeudaeu.com/python-pip-difference/))


```py
import dateutil.parser 

s   = 'Mon, 13 Feb 2023 10:40:59 GMT'

dt = dateutil.parser.parse(s)
print(dt)
#2023-02-13 10:40:59+00:00      # タイムゾーン情報(+00:00)が残っている
print(dt.tzinfo)
# UTC                           # タイムゾーン(UTC)が残っている
```

もちろん`JST`も使用できる。
```py
import dateutil.parser 

s   = 'Mon, 13 Feb 2023 10:40:59 JST'

dt = dateutil.parser.parse(s)
print(dt)
#2023-02-13 10:40:59+09:00        # タイムゾーン情報(+09:00)が残っている
print(dt.tzinfo)
# JST                             # タイムゾーン(JST)が残っている
```



# ハマった点(2)

## 疑問：日本時間に変換する方法がたくさんあるがどれがいい？

タイムゾーン情報付きのdatetimeオブジェクト（Awareなdatetimeオブジェクト）から日本時間（正確にはローカルタイム[^ローカル時間の定義]）に変換する方法がいろいろあってどの方法を選択すればいいか分からない。 

## (1)標準ライブラリを使用する方法
```py
import datetime

# 変換前の時間(UTC)
dt = datetime.datetime(2023, 2, 13, 10, 40, 59, tzinfo=datetime.timezone.utc)
print(dt)
#2023-02-13 10:40:59+00:00

# 方法1：システム設定にまかせる
print(dt.astimezone())
#2023-02-13 19:40:59+09:00

# 方法2：時差を指定する（欧米等、夏時間がある地域は難しい）
td = datetime.timedelta(hours=9)
tz = datetime.timezone(td)
print(dt.astimezone(tz)
#2023-02-13 19:40:59+09:00

# 方法3：zoneinfo.ZoneInfoを使用（Python V3.9以降）(OSまたは、tzdataパッケージ依存)
import zoneinfo
print(dt.asttimezone(zoneinfo.ZoneInfo('Asia/Tokyo')))
#2023-02-13 19:40:59+09:00
print(dt.astimezone(zoneinfo.ZoneInfo('Japan')))
#2023-02-13 19:40:59+09:00
print(dt.astimezone(zoneinfo.ZoneInfo('localtime')))
#2023-02-13 19:40:59+09:00
```

## (2)dateutil.tzを使用する方法

:::note info
標準ライブラリではないのでdateutilモジュールをインストールする必要がある[^pythonコマンドをつかうべき理由][^Armbianの場合]

```
python3 -m pip install python-dateutil
```
:::

```py
import datetime
from dateutil import tz

# 変換前の時間(UTC)
dt = datetime.datetime(2023, 2, 13, 10, 40, 59, tzinfo=datetime.timezone.utc)
print(dt)
#2023-02-13 10:40:59+00:00

# 方法1：タイムゾーン名(OS依存)を指定する
print(dt.astimezone(tz.gettz('Asia/Tokyo')))
#2023-02-13 19:40:59+09:00
print(dt.astimezone(tz.gettz('Japan')))
#2023-02-13 19:40:59+09:00
print(dt.astimezone(tz.gettz('localtime')))
#2023-02-13 19:40:59+09:00
print(dt.astimezone(tz.gettz('')))
#エラー
print(dt.astimezone(tz.gettz()))
#2023-02-13 19:40:59+09:00
```


## (3) pytzライブラリを使用する方法
[pytz](https://pypi.org/project/pytz/)はtimezoneの処理をするためのライブラリ。


:::note info
標準ライブラリではないのでpytzモジュールをインストールする必要がある[^pythonコマンドをつかうべき理由][^Armbianの場合]
```
python3 -m pip install pyt
```
:::

```py
import datetimme
import pytz

# 変換前の時間(UTC)
dt = datetime.datetime(2023, 2, 13, 10, 40, 59, tzinfo=datetime.timezone.utc)
print(dt)
#2023-02-13 10:40:59+00:00

# 方法1：タイムゾーン名を指定する
print(dt.astimezone(pytz.timezone('Asia/Tokyo')))
#2023-02-13 19:40:59+09:00
print(dt.astimezone(pytz.timezone('Japan')))
#2023-02-13 19:40:59+09:00
print(dt.astimezone(pytz.timezone('localtime')))
#エラー
print(dt.astimezone(pytz.timezone('')))
#エラー
print(dt.astimezone(pytz.timezone()))
#エラー

# 方法2：localize()はnaiveなdatetimeではないため使えない
print(pytz.timezone('Asia/Tokyo').localize(dt)))
#エラー
```
なお、`pytz`はPython2系の時からある超メジャーなライブラリらしいが、過去（1888年より前とか）で時差が19分ずれたり、`pytz.timezone()`と`datetime.tzinfo`との互換性に癖があったりするらしい[^pytzの問題点]ので避けた方がいいかもしれない。（特にzoneinfoモジュールが追加されたPython V3.9以降の場合）

[^pytzの問題点]: 「[Pythonのタイムゾーンはpytzよりzoneinfoかdateutils.tzを使おう、という話](https://zenn.dev/atsuoishimoto/articles/505b3f4831443a1b63ad)」

:::note warn

標準ライブラリの`datetime.timezone`と`pytz.timezone`には**完全な互換性はない**
:::

## 答え：標準ライブラリ

今回やりたいことは標準ライブラリだけで十分できる。

# まとめ

今回の目的、サーバーから取得したRSSフィールドに含まれている日時の文字列を日本時間（＝ローカル時間[^ローカル時間の定義]）に変換したいだけなら

+ `dateutil.parser.parse()`を使用してAwareな`datetime`オブジェクトを取得
+ `datetime.astimezone()`を使用してローカル時間に変換

でできる。

# 参考情報

+ dateutilライブラリ [[ホームページ]](https://github.com/dateutil/dateutil) [[PyPI]](https://pypi.org/project/python-dateutil/) [[ドキュメント]](https://dateutil.readthedocs.io/en/stable/)
+ pytzライブラリ [[ホームページ]](https://pythonhosted.org/pytz/) [[PyPI]](https://pypi.org/project/pytz/)

+ [「datetime の UTC / JSTの変換についてまとめ」](https://python.civic-apps.com/datetime-utc-jst-convert/)
+ [Qiita記事「pytzを使ってpythonでタイムゾーンを扱う"正しい"方法」](https://qiita.com/non_cal/items/2927fbdfa1d1f355b89d)
+ [Qiita記事「Python3のdatetimeはタイムゾーンを指定するだけで高速になる」](https://qiita.com/mattsu6/items/5511c5631e7a54550f7f)
+ [Zenn記事「Pythonのタイムゾーンはpytzよりzoneinfoかdateutils.tzを使おう、という話」](https://zenn.dev/atsuoishimoto/articles/505b3f4831443a1b63ad)