pythonでバラバラな日付表現文字列を統一する

[前回まで](https://qiita.com/kirin123kirin/items/1082d3c1564b745dcb4f)分割キーボードを作っていたが完成しやっと手になじんできた。
肩こりも解消して随分生産性が上がったので、久しぶりにpython記事を投稿してみる。

# 自由奔放すぎる日付データをクレンジングする

一個だけ平成元年データがあるのが、全部2001年8月24日 20:10を表す日時データなのだが、表現方法がバラバラである。
よくもまあこれだけバラバラに入力してくれたものだと感心もするが、
この嫌がらせか？と思うクソデータが数十万件あり、時系列分析を行うための日時情報がこのデータにしか存在しないのだ。


## ハラスメント的なクソデータ
|名前|受付年月日|
|----|----------|
|Mr.A|08-24-2001 20:10|
|Mr.B|Friday, August 24th, 2001 20:10|
|Mr.C|Fri Aug. 24, 2001 8:10 p.m.|
|Mr.D|Aug. 24, 2001 20:10|
|E様|2001/08/24 20:10|
|F様|2001/08/24 2010|
|G様|2001年8月24日金曜日 20:10|
|H様|2001年8月24日(金) 20:10|
|I様|平成１３年８月２４日　午後八時十分|
|J様|平成13年08月24日PM 08:10|
|K様|H13年08月24日　PM08:10|
|L様|平13年08月24日　午後8:10|
|M様|平成13年08/24午後08:10|
|N様|平成元年０８月２４日　２０時１０分００秒|


## 僕が欲しいデータ
こういうデータとして欲しい。

|名前|受付年月日|
|----|----------|
|Mr.A|2001-08-24T20:10:00|
|Mr.B|2001-08-24T20:10:00|
|~省略~|
|M様|2001-08-24T20:10:00|
|N様|1989-08-24T20:10:00|

クソデータをpythonを使ってクレンジングしていく。

## [欧米バージョンの解] dateutilライブラリを使えばOK
(僕のpythonバージョンは3.7.7 dateutilライブラリのバージョンは2.8.1)

[python-dateutil](https://pypi.org/project/python-dateutil/)という便利なツールがあるのでこれを使う。
まずは
pip install python-dateutil


```python
from dateutil.parser import parser

def to_datetime(timestr):
    return parser().parse(timestr)

kusodata = """Mr.A	08-24-2001 20:10
Mr.B	Friday, August 24th, 2001 20:10
Mr.C	Fri Aug. 24, 2001 8:10 p.m.
Mr.D	Aug. 24, 2001 20:10
E様	2001/08/24 20:10
F様	2001/08/24 2010
G様	2001年8月24日金曜日 20:10
H様	2001年8月24日(金) 20:10
I様	平成１３年８月２４日　午後八時十分
J様	平成13年08月24日PM 08:10
K様	H13年08月24日　PM08:10
L様	平13年08月24日　午後8:10
M様	平成13年08/24午後08:10
N様	平成元年1月1日　２０時１０分００秒
"""

for line in kusodata.splitlines():
    name, utime = line.split("\t")
    dt = to_datetime(utime)
    print(name, dt.isoformat())

```
なんとなく、to_datetimeという関数でラップしてやって
うりゃーとfor文で全部実行！

途中でエラー、、 あれ？

```python

Mr.A 2001-08-24T20:10:00
Mr.B 2001-08-24T20:10:00
Mr.C 2001-08-24T20:10:00
Mr.D 2001-08-24T20:10:00
E様 2001-08-24T20:10:00
F様 2001-08-24T20:10:00
Traceback (most recent call last):

  File "xxxx.py", line 28, in <module>
    dt = to_datetime(utime)

  File "xxxx.py", line 24, in to_datetime
    return parser().parse(timestr)

  File "xxxx\python\Lib\site-packages\dateutil\parser\_parser.py", line 649, in parse
    raise ParserError("Unknown string format: %s", timestr)

ParserError: Unknown string format: 2001年8月24日金曜日 20:10

```

G様でエラー落ちた。年月日とか全角文字や漢字交じりは扱えないらしい。当然和暦も無理。残念。
全角文字交じりでなければ上記のライブラリ関数一発で行けるが、くそデータは全半角混在の強敵だ。
全角スペースや和暦、省略表記など多種多様だ。

ちょっと処理を追加するぐらいでは、手に負えないので、怒りを覚えながら
dateutilのparserクラスごと継承して大手術したった。

# [欧米+日本バージョンの解] ごった煮日付文字列をdatetimeに変換する関数

```python
import re

from datetime import datetime
from dateutil.parser import parser, parserinfo


## 全角文字列は半角に統一しましょう
ZEN = '！＂＃＄％＆＇（）＊＋，－．／０１２３４５６７８９：；＜＝＞？＠ＡＢＣＤＥＦＧＨＩＪＫＬＭＮＯＰＱＲＳＴＵＶＷＸＹＺ［＼］＾＿｀ａｂｃｄｅｆｇｈｉｊｋｌｍｎｏｐｑｒｓｔｕｖｗｘｙｚ｛｜｝～'
HAN = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
def to_hankaku(s):
    return s.translate(str.maketrans(ZEN, HAN))

## 漢数字は使わずアラビア数字にしましょう
def kanji2int(s,
    kns = str.maketrans('一二三四五六七八九〇壱弐参', '1234567890123'),
    re_kn = re.compile(r'[十拾百千万億兆\d]+'),
    re_ku1 = re.compile(r'[十拾百千]|\d+'),
    re_ku2 = re.compile(r'[万億兆]|[^万億兆]+'),
    UNIT1 = {'十': 10,
            '拾': 10,
            '百': 100,
            '千': 1000},
    UNIT2 = {'万': 10000,
            '億': 100000000,
            '兆': 1000000000000}
    ):
    def _tran(sj, re_obj=re_ku1, transdic=UNIT1):
        unit = 1
        result = 0
        for piece in reversed(re_obj.findall(sj)):
            if piece in transdic:
                if unit > 1:
                    result += unit
                unit = transdic[piece]
            else:
                val = int(piece) if piece.isdecimal() else _tran(piece)
                result += val * unit
                unit = 1

        if unit > 1:
            result += unit

        return result

    ret = s.translate(kns)
    for num in sorted(set(re_kn.findall(ret)), key=lambda s: len(s), reverse=True):
        if not num.isdecimal():
            ar = _tran(num, re_ku2, UNIT2)
            ret = ret.replace(num, str(ar))

    return ret


## 午前午後の位置がおかしい場合もあるので、AM,PMの位置を補正する関数
_ng_ampm = re.compile('(\\s*(?:[AaPp]\\.?[Mm]\\.?|午[前後]))((?:\\s*(?:1[0-9]|2[0-4]||0?[0-9])\\s*?[\\.:時]?\\s*(?:[1-5][0-9]|0?[0-9])\\s*?[\\.:分]?\\s*(?:[1-5][0-9]|0?[0-9])\\s*?(?:秒|[Ss]ec(?:onds)?)??\\s*(?:[,\\.]?\\d+)?(?:\\s*(?:[+\\-]\\d{4})\\s*\\(?(?:[ABCDEFGHIJKLMNOPRSTUVWY][ABCDEFGHIJKLMNOPRSTUVWXYZ][ABCDGHKLMNORSTUVWZ][1DST][T])\\)?|\\s*(?:[+\\-]\\d{4})|\\s*\\(?(?:[ABCDEFGHIJKLMNOPRSTUVWY][ABCDEFGHIJKLMNOPRSTUVWXYZ][ABCDGHKLMNORSTUVWZ][1DST][T])\\)?)?)[^\\s]*)')
def repair_ampm(s):
    return _ng_ampm.sub(" \\2 \\1", s).replace("  ", " ")


## 元号を使った和暦表現はやめて西暦にしましょう
g2d = {
    '令和': datetime(2019, 5, 1),
    'R': datetime(2019, 5, 1),
    '平成': datetime(1989, 1, 8),
    'H': datetime(1989, 1, 8),
    '昭和': datetime(1926, 12, 25),
    'S': datetime(1926, 12, 25),
    '大正': datetime(1912, 7, 30),
    'T': datetime(1912, 7, 30),
    '明治': datetime(1868, 10, 23),
    'M': datetime(1868, 10, 23),
}

## 和暦で1年の時だけ、元年という表現が許されているのは困ります
def gengo2date(timestr):
    g = next((d for d in g2d if d in timestr), None)
    if g is None:
        return timestr

    dy = g2d[g]
    i = dy.year - 1
    pattern = r"(?:" + g + r"[\.,\- ]?)((?:[0-9]{1,2}|元))\s?(年?)"

    reret = re.search(pattern, timestr)
    if reret:
        n = reret.group(1)
        edit = "{}{}".format(int("1" if n == "元" else n) + i, reret.group(2))
        return timestr.replace(reret.group(0), edit)

    return timestr

## dateutilのクラスを一部継承して拡張します
class lazydate(object):
    class jpinfo(parserinfo):
        JUMP = [" ", "　",".", ",", ";", "-", "/", "'",
                "at", "on", "and", "ad", "m", "t", "of",
                "st", "nd", "rd", "th",
                "年", "月", "日",
                ]
        HMS = [("h", "hour", "hours", "時"),
               ("m", "minute", "minutes", "分"),
               ("s", "second", "seconds", "秒")]
        AMPM = [("am", "a", "午前"),
                ("pm", "p", "午後")]

    def __init__(self, timestr, parserinfo=None, **kwargs):
        self._dt = None
        self.timestr = timestr
        self.parserinfo = parserinfo or __class__.jpinfo()
        self._kwargs = kwargs

        if isinstance(timestr, datetime):
            self._dt = timestr
            self.timestr = self.repairstr = str(timestr)

        if not isinstance(timestr, str):
            self.timestr = str(timestr)

        if "fuzzy" not in kwargs:
            self._kwargs["fuzzy"] = True

    def parse(self, timestr=None, **kw):
        if timestr is None and self._dt is not None:
            return self._dt
        if isinstance(timestr, datetime):
            return timestr

        repairstr = to_hankaku(timestr or self.timestr)
        repairstr = kanji2int(repairstr)
        repairstr = gengo2date(repairstr)
        repairstr = repair_ampm(repairstr)

        return parser(self.parserinfo).parse(repairstr, **{**self._kwargs, **kw})


## 細かい表現方法の違いは気にせずとにかく、datetimeオブジェクトが欲しいだけなのです
def to_datetime(timestr):
    return lazydate(timestr).parse(fuzzy_with_tokens=False)

```

# 使い方

もう一回うりゃーと実行

```python

kusodata = """Mr.A	08-24-2001 20:10
Mr.B	Friday, August 24th, 2001 20:10
Mr.C	Fri Aug. 24, 2001 8:10 p.m.
Mr.D	Aug. 24, 2001 20:10
E様	2001/08/24 20:10
F様	2001/08/24 2010
G様	2001年8月24日金曜日 20:10
H様	2001年8月24日(金) 20:10
I様	平成１３年８月２４日　午後八時十分
J様	平成13年08月24日PM 08:10
K様	H13年08月24日　PM08:10
L様	平13年08月24日　午後8:10
M様	平成13年08/24午後08:10
N様	平成元年０８月２４日　２０時１０分００秒
"""

for line in kusodata.splitlines():
    name, utime = line.split("\t")
    dt = to_datetime(utime)
    print(name, dt.isoformat())

```

今度はうまくいったようです！
結構やっつけなところもある、ので詳しい方コメントいただければ幸いです。

```
Mr.A 2001-08-24T20:10:00
Mr.B 2001-08-24T20:10:00
Mr.C 2001-08-24T20:10:00
Mr.D 2001-08-24T20:10:00
E様 2001-08-24T20:10:00
F様 2001-08-24T20:10:00
G様 2001-08-24T20:10:00
H様 2001-08-24T20:10:00
I様 2001-08-24T20:10:00
J様 2001-08-24T20:10:00
K様 2001-08-24T20:10:00
L様 2001-08-24T20:10:00
M様 2001-08-24T20:10:00
N様 1989-08-24T20:10:00

```


# 最後に
今回変換関数を作ったことで、日時についていろいろ勉強になった。

* アメリカ、ヨーロッパ諸国、日本の年月日の「年」を「月日」の前に書くか後に書くか、「月日」の順序も国によって違うらしいので根が深そう。
* 日付入力規則が出来上がったので、データ入力する人には注意しよう。
  * AM,PM入れる場合は時刻の後ろに書きましょう。
  * 全角文字列は使わず半角を使いましょう
  * 漢数字は使わずアラビア数字を使いましょう
  * 元号を使った和暦表現はやめて西暦にしましょう
  * 曜日まで入力する必要はありません。

（入力チェックでエラー出すようにすべきだと分かってますが、こういうデータは無くなりません）


# おまけ
たまに西暦から和暦を求めたい場合もあったので参考に

```python

def to_gengo(timestr, form="%ggg年%m月%d日 %H:%M:%S"):
    dt = to_datetime(timestr)

    gname, gyear = next((g, dt.year - g2d[g].year + 1) for g in g2d if g2d[g] <= dt)
    gengo = "{}{}".format(gname, "元" if gyear == 1 else gyear)
    form = form.replace("%ggg", gengo)
    return dt.strftime(form.encode('unicode-escape').decode()).encode().decode("unicode-escape")

```

```python
In: to_gengo("1989-01-01")
Out: '昭和64年01月01日 00:00:00'
```

和暦文化無くなってくれればいいのに