📝

正規表現のメリットは分かるけど、使い方(勉強の仕方)が分からなかったので私なりの独学の方法を公開

2023/08/29に公開

https://qiita.com/items/6bf93e01af0a99d4f019


お詫び

Qiitaの元記事にて、区切り線を「---」で書いている場所があり、これがZennの記法に干渉して一部うまく表示できない記事がある事を認識しています。
全ての記事を精査しきれていないため、お手数ですがお見かけの際は教えていただけると大変喜びます。


もっといい方法はあると思うけど、正規表現だけに限らず、みんながどうやって勉強しているのか分からないので、自分から率先して公開するようにしてみる。
こんなに悩んだのは、はじめてJavaを勉強した時に、一緒にオブジェクト指向や変数のスコープについてウンウン唸っていた頃以来。
こちらについては勉強する方法とか教える側とかも必死だけど、正規表現はみんな楽勝なのかな?

私がハマったんで、他の人もハマってんじゃね?と思って公開する。

プラクティス:シミュレーション

日付とIPがあるログを正規表現で取得したい場合を想定

基本

「python re 使い方 or 正規表現」あたりでgoogle先生にお尋ねしつつ、いつも何となく使ってるreについて、きちんと復習しておく。

import re

# 参考:Python2のlambda式でprintを使う: https://qiita.com/Go-zen-chu/items/135bab27cea5ef27e1f8
# より、python2でもprint文を使いたかったので関数を作って対応
def_pattern_print(mes):
    print 'pattern: ',mes

re_search = lambda x,y:pattern_print(re.compile(y).search(x).groups())

log = '123.45.67.89 http://qiita.com 2017:12:20'  # IPv4 URL date
pattern1 = r'.*?'
pattern2 = r'(.*?)'
pattern3 = r'(.*?) +.*$'
pattern4 = r'(.*?) +(.*$)'

re_search(line, pattern1)
re_search(line, pattern2)
re_search(line, pattern3)
re_search(line, pattern4)

#出力
#->pattern:  ()
#->pattern:  ('',)
#->pattern:  ('123.45.67.89',)
#->pattern:  ('123.45.67.89','http://qiita.com 2017:12:20')

まずはパターンの書き方について。
https://qiita.com/supersaiakujin/items/8d2b318b36da27dca16a#正規表現
を参照して、やりたいイメージを膨らませるのが一番良いが、ここではそういうのは特に考えず、全パターンマッチを採用する。

なお、全パターンマッチを使う場合、取得条件を明示的にしておく必要がある。
ここではログファイルを半角スペースで区切りたい想定なので、半角スペースをつけて次のパターンマッチで全件取得するようにしている。
ちなみに、re.searchメソッドを説明しやすいfindallに変えると、

#出力
#->pattern:  ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '','', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '','', '', '', '', '', '']
#->pattern:  ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '','', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '','', '', '', '', '', '']
#->pattern:  ['123.45.67.89']
#->pattern:  [('123.45.67.89', 'http://qiita.com 2017:12:20')]

のように出力が得られる。
ここでは空白のリストだと分かりにくいのでfindallとしていないが、メソッドや内容が何であれ上記を理解しておかないと、これ以降が(正規表現に強い苦手意識のある、来年の自分に向けて)説明しにくいので、ここで明記しておく。

なお、

pattern_all = r'.*'

#出力
#->pattern:  ['123.45.67.89', 'http://qiita.com 2017:12:20']

とすると、logの内容を見ることが出来る。
が、正規表現の活用としてはこれ単体で実行する事は勉強とかデバッグぐらいしかないと思う。

文字列切り出し

とりあえず何も考えずに先頭から4文字切り出してみる。

pattern_splitchar4 = r'(.{4}) +(.*$)'
#->pattern:  ['123.','']

*が任意の数の文字・数字など。
これを4と指定すると、4文字切り取ることが出来る。
これだけで見ると、こんな事をしなくてもlog[:4]で同じものが取れるし、楽。

日付(年)を取る

pattern_yyyy = r'([\d]{4})'
#->pattern:  ['2017','']

上記パターンに「数値である」という条件を[\d]とする事で指定したもので、厳密には日付を取るためのパターンではない
このようなログの場合、[\d]=数字であること、{4}=4桁(YYYY:年)であることを指定すると、たまたまdateのYYYYが最初にヒットするので、そこが参照されただけである。

また、たまたま[\d]{4}でマッチングした時に、この場所以外になかっただけで、複数マッチする場合は.*$で締める必要がある。

日付(月)を取る

pattern_mm = r'[\d]{4}:([\d]{2})'
#->pattern:  ['12','']

日付(年)を取るパターンで場所を決めて、その後ろにある:(nn)に該当する場合はこれとする。
なお、日付(日)を取る場合はpatternに:([\d]{2})を増やしてやるとよい。
月のパターンにある()を外さない場合は月,日のように取得される。

日付(年月日)を取る

単純に日付(日)でやったように(この中)が取れるので、([\d]{4}):([\d]{2}):([\d]{2})とすると良い。
フォーマットにこだわらないなら、([\d]{4}:[\d]{2}:[\d]{2})で一括で取れる。

ちょっとイケてると感じたやり方が、([\d:]{8})とすると、いっきに取れる。
[2(HH)] + [1(:)] + [2(MM)] + [1(:)] +
[2(SS)]で8である。
数字を下げると、先頭から数えてヒットする数だけ拾う。

応用

この説明がしたかった。

IPアドレスも同様の方法で取れる。
1.2.3.4の場合、[\d.]{7}
111.222.333.444の場合、[\d.]{15}

これをいい感じにやる場合、単純にくっつけてしまえばよい
[\d.]{7,15}
これでIPアドレスの範囲だけ取得できる
ポートも含めたい場合は[\d.:]{9,21}が妥当か。
ポートは:X~XXXXXの範囲を想定している。

まずは日付とIPの処理をしっかり学ぶ

ここにCSVとかでなんちゃってログファイルを作って,や"をパターンマッチングに使わない、というルールを自身に課して、こういう場合はどうなるの?というケースを幾つか作ってやってみると、何となくだが「正規表現ってこういうもの」と思うようになった。
かなり苦手意識があったし、今でも使いこなせているとは思わないが毛嫌いする事はなくなったように思う。
今まで決め打ちでstring[N1:N2]とかstring.find()を複数回とかで逃げていたが、適切な書き方は可読性を上げる事につながるので、必要であれば正規表現をしっかり使えるようにしておくとpython以外にも使える範囲は広い。

文字列(小文字・大文字)や記号も扱ってみる

日付・IPが分かれば、この辺からは調べて自分で解決できるようになっているはず。
ちなみに、IPでやってる.(ドット)も記号扱いだが、ここでは決め打ちで書いているのであまり気にしていない。

注意

正規表現を使うよりfindなどの方が早いので、単純なものにまで正規表現にする必要はない。
何事も適材適所が大事。

参考

今回使ったのがpythonだったので参考がpythonを使用するものに偏っている…

[Python]Regular Expressions 正規表現
https://qiita.com/supersaiakujin/items/8d2b318b36da27dca16a

pythonの正規表現メモ
https://qiita.com/kaeruair/items/747518c116c85a88ee21

GitHubで編集を提案

Discussion