Python2.7(Lambda)でタイムゾーンの略称(JST, PDT等)をパースする方法

こんにちは、臼田です。

「Tue, 14 Mar 2017 00:24:08 PDT」みたいな文字列をdatetime型にして保存したいケースが有ると思います。

私は、AWSのRSSからpubDateを取得した際に上記のようになっていたため、これをLambda(Python2.7)上で扱いたいと考えました。

タイムゾーンの略称を扱う難しさ

時間の文字列からdatetimeを取得するには、datetime.datetime.strptime()が利用できます。

しかし、Python2.7ではstrptime()でタイムゾーンの略称を扱う%Zが利用できません。逆にdatetime型からstrに変換するstrftime()では利用できるのに、です。

これはどういうことかと調べてみると、ここで情報を見つけました。

要約すると、タイムゾーンの略称は重複している名称がある為、strptime()では%Zが利用できないとのこと。

これをタイムゾーンを扱うライブラリであるpytzで確かめてみました。

# -*- coding: utf-8 -*-
# pytz_test.py
import inspect
import itertools
from datetime import datetime, timedelta
import pytz


alltz = map(pytz.timezone, pytz.all_timezones)

normal = datetime(2009, 9, 1)
ambiguous = datetime(2009, 10, 31, 23, 30)

tz_dtlist = []
for tz in alltz:
	# print tz
	if 'is_dst' in inspect.getargspec(tz.tzname).args:
		tz_dtlist.append({'tzname': tz.tzname(normal, is_dst=False), 'offset': tz.utcoffset(normal, is_dst=False)})
		tz_dtlist.append({'tzname': tz.tzname(ambiguous, is_dst=False), 'offset': tz.utcoffset(ambiguous, is_dst=False)})
	else:
		tz_dtlist.append({'tzname': tz.tzname(normal), 'offset': tz.utcoffset(normal)})

settz_dtlist = []
for tz in tz_dtlist:
	if tz not in settz_dtlist: settz_dtlist.append(tz)

print 'all tz: ' + str(len(tz_dtlist))
print 'unique tz: ' + str(len(settz_dtlist))

# get unique tznames
func = lambda x:x["tzname"]
data = sorted(settz_dtlist, key=func)

unique_data = []
for k, g in itertools.groupby(data, func):
	unique_data.append(g.next())

print 'duplicate tz name: ' + str(len(settz_dtlist) - len(unique_data))

出力結果は以下のとおりです。

python pytz_test.py
all tz: 1155
unique tz: 225
duplicate tz name: 17

pytzの中にも、重複したタイムゾーンの略称を利用していて時間が違う地域が17箇所あることが確認できました。(pytzのバージョンにより結果は異なるかと思います)

strptime()でタイムゾーンの略称を扱うことが出来ないため、違う方法を検討する必要があります。

自由フォーマットのパースが可能なdateutil

strptime()よりも使いやすいdatetimeのパーサにdateutil.parser.parseがあります。

dateutil - powerful extensions to datetime

こちらは外部ライブラリですが、strptime()のようにフォーマットを厳密に指定しなくてもdatetime型に変換してくれる大変優秀なものです。

少し動かしてみます。

from datetime import datetime, timedelta
from dateutil.parser import parse

t = '2017-03-01T16:29:18+09:00'
print parse(t)
t2 = 'Tue, 14 Mar 2017 00:24:08 PDT'
print parse(t2)
2017-03-01 16:29:18+09:00
2017-03-14 00:24:08

+HH:MM表記のものは認識されますが、PDTのものは認識されませんでした。

dateutilのパーサをそのまま使用しても、タイムゾーンの略語を扱うことが出来ません。

※正確には、環境によってはローカルのタイムゾーンを読み込んでいる場合があります。例えば私の環境では「JST」のみは+09:00でパースすることが出来ました。

dateutilでタイムゾーンの略語を使用する

この解決は非常に簡単で、dateutilにはタイムゾーン名を利用するための引数(tzinfos)を取っています。

詳細はdateutil.parser.parseに書かれています。

問題は、その引数をどうやって作成するかです。

しかし、これも先程のstack overflowにいい方法が書かれていたので引用します。


tz_str = '''-12 Y
-11 X NUT SST
-10 W CKT HAST HST TAHT TKT
-9 V AKST GAMT GIT HADT HNY
-8 U AKDT CIST HAY HNP PST PT
-7 T HAP HNR MST PDT
-6 S CST EAST GALT HAR HNC MDT
-5 R CDT COT EASST ECT EST ET HAC HNE PET
-4 Q AST BOT CLT COST EDT FKT GYT HAE HNA PYT
-3 P ADT ART BRT CLST FKST GFT HAA PMST PYST SRT UYT WGT
-2 O BRST FNT PMDT UYST WGST
-1 N AZOT CVT EGT
0 Z EGST GMT UTC WET WT
1 A CET DFT WAT WEDT WEST
2 B CAT CEDT CEST EET SAST WAST
3 C EAT EEDT EEST IDT MSK
4 D AMT AZT GET GST KUYT MSD MUT RET SAMT SCT
5 E AMST AQTT AZST HMT MAWT MVT PKT TFT TJT TMT UZT YEKT
6 F ALMT BIOT BTT IOT KGT NOVT OMST YEKST
7 G CXT DAVT HOVT ICT KRAT NOVST OMSST THA WIB
8 H ACT AWST BDT BNT CAST HKT IRKT KRAST MYT PHT SGT ULAT WITA WST
9 I AWDT IRKST JST KST PWT TLT WDT WIT YAKT
10 K AEST ChST PGT VLAT YAKST YAPT
11 L AEDT LHDT MAGT NCT PONT SBT VLAST VUT
12 M ANAST ANAT FJT GILT MAGST MHT NZST PETST PETT TVT WFT
13 FJST NZDT
11.5 NFT
10.5 ACDT LHST
9.5 ACST
6.5 CCT MMT
5.75 NPT
5.5 SLT
4.5 AFT IRDT
3.5 IRST
-2.5 HAT NDT
-3.5 HNT NST NT
-4.5 HLV VET
-9.5 MART MIT'''

tzd = {}
for tz_descr in map(str.split, tz_str.split('\n')):
 tz_offset = int(float(tz_descr[0]) * 3600)
 for tz_code in tz_descr[1:]:
 tzd[tz_code] = tz_offset

実際に使ってみます。

print parse(t2, tzinfos=tzd)
2017-03-14 00:24:08-07:00

綺麗にパースできました!

まとめ

タイムゾーンが決まっている場合には、何も気にしなくていいので上記のようなtzinfoを作成する必要は無いかと思います。

また、フォーマットが+-HH:MMならdateutilにおまかせで大丈夫です。

しかし、どうしようもないときは上記を利用してみてはいかがでしょうか?