正規表記とは
正規表記を使えば、複雑なパターンや文字列など検索したり置換したりできます。今回はこの正規表記を使って、入力されたデータから日付を表現する文字列だけを読み取り、抽出してみようと思います。
基礎なので詳細は割愛します。下記の参考文献に公式ドキュメントを張り付けていますのでそちらを読んでみてください。
個人的に正規表記は何だか複雑でとらえにくい感じがありますが徐々に慣れていこうと思っています。
標準ライブラリのre
を使います
import re
入力データの用意
まずは日付のデータを用意します。
色々なデータを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",
]
日付を表現する正規表記
例えば、スマホやパソコンで「今日」と入力すると予測変換で「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 = 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()
のメソッドを使って正規表記で一部マッチする表記を出力してみます。
for date in DATE:
# Hit data to "hit_date"
hit_date = date_type.search(date)
print(hit_date)
<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()
でタプル型を返してあげましょう。さきほどのスクリプトを少し改良します。
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)
('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以上はありえません。そんな感じでやっていきます。
細かくやるとうるう年も考えなきゃならないので、ここの判定はご自由に変えてやってみてください。
以上を考慮するとこんな感じになります。
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)
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
これより日付だと思われる表現だけを抽出することができたと思います。
完成したサンプルコード
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)
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はこちら