Pythonの正規表記：入力データから妥当な日付表現だけを判定して抽出する方法

# 正規表記とは

正規表記を使えば、複雑なパターンや文字列など検索したり置換したりできます。今回はこの正規表記を使って、**入力されたデータから日付を表現する文字列だけを読み取り、抽出してみよう**と思います。

基礎なので詳細は割愛します。下記の参考文献に公式ドキュメントを張り付けていますのでそちらを読んでみてください。

個人的に正規表記は何だか複雑でとらえにくい感じがありますが徐々に慣れていこうと思っています。

標準ライブラリの`re`を使います

```
import re
```


# 入力データの用意

まずは日付のデータを用意します。
色々なデータを`DATE`としてリストに格納しました。日付とはまったく関係のないものや惜しいもの、区切りだけが違うものなど様々ありますね。

```date.py
DATE = ["2020/01/05",
        "2020/1/5",
        "2020年1月5日",
        "2020-1-5",
        "2020/1/5",
        "2020.1.5",
        "2020/20/20",
        "2020 1 5",
        "2020 01 05",
        "1995w44w47",
        "Thank you",
        "1998/33/52",
        "3020/1/1",
        ]
```


# 日付を表現する正規表記

例えば、スマホやパソコンで「今日」と入力すると予測変換で「2020年1月5日」や「2020/01/05」、「令和2年1月5日」といった表現が出てくるとおもいます。今回は西暦を使い、`YYYY-MM-DD`の表記を取り扱います。

よく使用される日付の正規表現式をサンプルとして書くと`^\d{4}-\d{1,2}-\d{1,2}$`こんな風に書けます。
※記号の表現をみてわからない場合は公式ドキュメントをよくみてみてください。


しかし、これではまだ甘いです。対応している表記は`-`で区切られた文字列のみになっています。そこで使うのが`\D`なんです。

`\D`は任意の非数字文字を表しています。`[^0-9]` と等価ですね。したがって、これを使えばハイフンや文字列、スペース、ドットなど数字以外の何かを判定できるようになります。

さっそく作ってみます。

```date_type.py
date_type = re.compile(r"""(
    (^\d{4})        # First 4 digits number
    (\D)            # Something other than numbers
    (\d{1,2})       # 1 or 2 digits number
    (\D)            # Something other than numbers
    (\d{1,2})       # 1 or 2 digits number
    )""",re.VERBOSE)
```
できました。メソッドは`re.compile()`を使います。

先に示した日付と比べると`$`がなくなっています。`$`は文字列の末尾がマッチしているのかを調べるものですが、今回は必ずしも末端が`\d{1,2}`=`MM`ではありません。それは入力データに`2020年1月5日`が存在するからです。末端に`日`があったり、何か他の文字列がくることを考えるとおいそれと末端固定の`$`を使うことはできません。

# 日付を抽出する

準備が整ったので日付を抽出を考えてみましょう。
まず`.search()`のメソッドを使って正規表記で一部マッチする表記を出力してみます。

```hit_data_1.py
for date in DATE:
    # Hit data to "hit_date"
    hit_date = date_type.search(date)
    print(hit_date)
```

```出力結果_1.py
<re.Match object; span=(0, 10), match='2020/01/05'>
<re.Match object; span=(0, 8), match='2020/1/5'>
<re.Match object; span=(0, 8), match='2020年1月5'>
<re.Match object; span=(0, 8), match='2020-1-5'>
<re.Match object; span=(0, 8), match='2020/1/5'>
<re.Match object; span=(0, 8), match='2020.1.5'>
<re.Match object; span=(0, 10), match='2020/20/20'>
<re.Match object; span=(0, 8), match='2020 1 5'>
<re.Match object; span=(0, 10), match='2020 01 05'>
<re.Match object; span=(0, 10), match='1995w44w47'>
None
<re.Match object; span=(0, 10), match='1998/33/52'>
<re.Match object; span=(0, 8), match='3020/1/1'>
```
当然、`Thank you`には`None`が返ってきました。それ以外の表記はまだ元気そうですね。

次にbool型で`None`を省き、もし`True`ならば`.groups()`でタプル型を返してあげましょう。さきほどのスクリプトを少し改良します。

```hit_data_2.py
for date in DATE:
    # Hit data to "hit_date"
    hit_date = date_type.search(date)
    bool_value = bool(hit_date)
    if bool_value is True:
        split = hit_date.groups()
        print(split)
```

```出力結果_2.py
('2020/01/05', '2020', '/', '01', '/', '05')
('2020/1/5', '2020', '/', '1', '/', '5')
('2020年1月5', '2020', '年', '1', '月', '5')
('2020-1-5', '2020', '-', '1', '-', '5')
('2020/1/5', '2020', '/', '1', '/', '5')
('2020.1.5', '2020', '.', '1', '.', '5')
('2020/20/20', '2020', '/', '20', '/', '20')
('2020 1 5', '2020', ' ', '1', ' ', '5')
('2020 01 05', '2020', ' ', '01', ' ', '05')
('1995w44w47', '1995', 'w', '44', 'w', '47')
('1998/33/52', '1998', '/', '33', '/', '52')
('3020/1/1', '3020', '/', '1', '/', '1')
```

はい！ここまで来たらもう少しです。
欲しい情報が格納されているのは、`[1]`と`[3]`と`[5]`でそれぞれ西暦、月、日です。これを分類するにはタプルのアンパッキングを使います。

さらにタプル内のタイプは`<class 'str'>`なのでint型に変えてあげましょう。そうすることで判定しやすくなりますね。

次にint型になった西暦と月と日がぶっとんだ数値になっていないかどうかを判定します。3000年はほとんど日常的に使わないため省きます。月も13以上はありませんし、日も32以上はありえません。そんな感じでやっていきます。
細かくやるとうるう年も考えなきゃならないので、ここの判定はご自由に変えてやってみてください。

以上を考慮するとこんな感じになります。

```hit_data_3.py
for date in DATE:
    # Hit data to "hit_date"
    hit_date = date_type.search(date)
    bool_value = bool(hit_date)
    if bool_value is True:
        split = hit_date.groups()

        # Tuple unpacking
        year, month, day = int(split[1]),int(split[3]),int(split[5])

        if year>3000 or month >12 or day > 31:
            print("False")
        else:
            print(year, month, day)
```

```出力結果_3.py
2020 1 5
2020 1 5
2020 1 5
2020 1 5
2020 1 5
2020 1 5
False
2020 1 5
2020 1 5
False
False
False
```
これより日付だと思われる表現だけを抽出することができたと思います。


# 完成したサンプルコード
```main.py
import re

# data of date
DATE = ["2020/01/05",
        "2020/1/5",
        "2020年1月5日",
        "2020-1-5",
        "2020/1/5",
        "2020.1.5",
        "2020/20/20",
        "2020 1 5",
        "2020 01 05",
        "1995w44w47",
        "Thank you",
        "1998/33/52",
        "3020/1/1",
        ]

# date :sample of Regular expression operations
date_type = re.compile(r"""(
    (^\d{4})        # First 4 digits number
    (\D)            # Something other than numbers
    (\d{1,2})       # 1 or 2 digits number
    (\D)            # Something other than numbers
    (\d{1,2})       # 1 or 2 digits number
    )""",re.VERBOSE)

for date in DATE:
    # Hit data to "hit_date"
    hit_date = date_type.search(date)
    bool_value = bool(hit_date)
    if bool_value is True:
        split = hit_date.groups()

        # Tuple unpacking
        year, month, day = int(split[1]),int(split[3]),int(split[5])

        if year>3000 or month >12 or day > 31:
            print("False")
        else:
            print(year, month, day)
```

```出力結果.py
2020 1 5
2020 1 5
2020 1 5
2020 1 5
2020 1 5
2020 1 5
False
2020 1 5
2020 1 5
False
False
False
```

# まとめ
いかがでしたか。もっと他に良いやり方があるのかもしれませんが、これで精一杯です。

日常的な作業を効率化させるためにツールを開発しようと思ってやっていたら、ここにぶち当たったのでついでにQiitaにも書いときました。

何かの役に立てれば幸いです。[Githubはこちら](https://github.com/yuki-imamura-all/Date_Extraction_v1.0.0)

# 参考文献
- [Python 標準ライブラリre --- 正規表現操作](https://docs.python.org/ja/3/library/re.html)
- [よく使う正規表現はもうググりたくない！](https://qiita.com/dongri/items/2a0a18e253eb5bf9edba)