背景
先日、「閏秒(うるう秒:以下、すべて漢字表記します)」のレコードを持つデータを扱う機会があり、その際の対処について調べた結果をまとめておきます。
タイトルは、映画『桐島、部活やめるってよ』のパロディです Pythonなので大蛇⇒ニシキヘビ⇒毒⇒毒島...すみません...
先人たちの知恵をお借りするなどして解決できたことを、この場をお借りして感謝するとともに、大変恐縮ですが自分のメモとしても、こちらへまとめておきます。
環境
- Windows 10 Pro
- Python 3.9.0, 3.8.5
- Django 3.1.4
- PostgreSQL 13.1
- Nginx 1.95.1
- Gunicorn
- Putty 0.74
0.閏秒とは
Wikipedia - 閏秒 より引用:
閏秒は、現行の協定世界時 において、世界時のUT1との差を調整するために追加もしくは削除される秒である 。この現行方式のUTCは1972年に始まった。2019年までに実施された計27回の閏秒(★注1)は、いずれも1秒追加による調整であった。 直近の閏秒の挿入は、2017年1月1日午前9時直前に行われた。
(★注1) 末尾に実施日の一覧も掲載しています。
1.現象
nofrag, frag = t.split('.')
nofrag_dt = datetime.datetime.strptime(nofrag, "%Y-%m-%dT%H:%M:%S")
dt = nofrag_dt.replace(microsecond=int(frag))
ValueError: second must be in 0..59
『値エラー:秒は0~59であるべき』が発生。
↓
当然じゃん……ん!?で、フリーズ……恐る恐るデータを見てみると…
『なんということでしょう~ データを開けるとそこには、2016-12-31 23:59:60.202915
というレコードが隠されていました~』(大改造!!劇的ビフォーアフター風に)
という結果です。
その文字列を解析してdatetimeオブジェクトに変換するために、上記のコードを使用したところで値エラーに...
2.原因
Python公式ドキュメント - time --- 時刻データへのアクセスと変換 -- time.strftime では、うるう秒をサポートしていると記載あり
⇒ ”値60は閏秒を表すタイムスタンプで有効で、値61は歴史的な理由からサポートされています” との注釈入り
上述の通り、Python のドキュメントでは、%S
は [0, 61] を受け入れるので、これは問題ではないと思いますね。
しかし、上記のタイムスタンプでこのエラーが発生します。
なんと、秒数が59を超える場合、ここでの問題は .strftime での扱いではなく、datetime.datetime へ変換できないことだったようです。
%S
のドキュメントには、次のように記載あり:
time モジュールとは異なり、datetime モジュールは閏秒をサポートしていません。
上述の時間文字列 "2016-12-31T23:59:60.202915"
は、時刻が UTC(協定世界時)における(現時点では最後の(直近の)閏秒)であることを意味します。
3.対処
以下の変換論理を用いて、datetime.datetime で扱えるようにします。参考元コードは某所よりお借りしてきました。
【注意】:現地時間での変換では失敗する可能性があります。(例:DST(夏時間)への遷移中)
import time
from calendar import timegm
from datetime import datetime, timedelta
time_string = '2016-12-31T23:59:60.202915'
time_string, dot, us = time_string.partition('.')
utc_time_tuple = time.strptime(time_string, "%Y-%m-%dT%H:%M:%S")
dt = datetime(1970, 1, 1) + timedelta(seconds=timegm(utc_time_tuple))
if dot:
dt = dt.replace(microsecond=datetime.strptime(us, '%f').microsecond)
print(dt)
4.結果
2017-01-01 00:00:00.202915
閏秒の表記が、2017-01-01 00:00:00.202915
へ変換されました。
⇒ 上記例 2016-12-31 23:59:60.202915
は、JST(日本標準時)における 2017-01-01 08:59:60.202915
に相当します。※JST=UTC+9時間
参考
閏秒の実施:
(編集後記)
データ分析・解析の前処理では、例外レコードを扱う機会も多いかと思いますが、こういった歴史的にも特殊な事柄に関連する事象を、つい忘れがちです。
(00~59に当てはまらないため)例外だからエラーデータとして弾いてしまおう…とすると、正しい分析・解析に至らない可能性もあります。
何が原因なのか、このレコードが発生した背景・理由や、レコードの必要性や妥当性を、今一度考えるきっかけにしたいと思います。