蛇ノ目の記

技術のことも。そうでないことも。

正規表現の先読み・後読みを知った話

実に2週間ぶりの更新。転職してそろそろ2ヶ月になる頃だけど、実案件で苦戦しまくっている。今回の話もそこから出てきたネタ。

正規表現には先読みアサーション 後読みアサーション なるものがあることを知ったので、今回はそれについて書く。

公式ドキュメントはこちら

6.2. re — 正規表現操作 — Python 3.6.3 ドキュメント

正規表現 HOWTO — Python 3.6.3 ドキュメント

先読み

先読みアサーション

  • (?=...)

... が続く文字列にマッチする。例えば以下のようになる。

>>> import re
>>> m = re.search(r'Nikon(?=FE2)', 'NikonFE2')
>>> m.group(0)
'Nikon'

マッチしないものも混ぜてみる。

>>> import re
>>> cameras = ['NikonFE', 'NikonFE2', 'NikonFM2']
>>> for camera in cameras:
...     m = re.search(r'Nikon(?=FE2)', camera)
...     if m is not None:
...             print(m.group(0), camera)
...     else:
...             print('None', camera)
...
None NikonFE
Nikon NikonFE2
None NikonFM2

カメラの機種名でやってみたけど、実践的に考えると特定のsuffixのある文字列を引っ掛けるのに使えそう。もちろん (?=...) には正規表現が使える。

>>> m = re.search(r'Nikon(?=[A-Z0-9]{3})', 'NikonFE2')
>>> m
<_sre.SRE_Match object; span=(0, 5), match='Nikon'>
>>> m.group()
'Nikon'

否定先読みアサーション

  • (?!...)

...が続かない文字列にマッチする。例えば以下のようになる。

>>> cameras = ['NikonFE', 'NikonFE2', 'NikonFM2']
>>> for camera in cameras:
...     m = re.search(r'Nikon(?!FE2)', camera)
...     if m is not None:
...             print(m.group(0), camera)
...     else:
...             print('None', camera)
...
Nikon NikonFE
None NikonFE2
Nikon NikonFM2

機種名がFE2でない機種がマッチしている。

特定のsuffixを持たない文字列を引っ掛けるのによさそう。

仕事で<meta property="og:image:type" ~ />ではなく<meta property="og:image" ~ />正規表現で引っ掛ける方法を探していて今回のネタに行き着いた、という裏事情がある。

BeautifulSoupを使って属性を条件にしてタグを取得する、というのが絡んでいる処理なのだけど、その振る舞いを勘違いしており先読みアサーション氏には出る幕がなかったというオチがつく。無念。この反省もブログのネタとして活かしたい。

後読み

後読みアサーション

  • (?<=...)

文字列内の現在位置の前に、現在位置で終わる ... とのマッチがあれば、マッチします。

とある。

(?<=abc)def は abcdef にマッチを見つけます。

>>> import re
>>> m = re.search('(?<=abc)def', 'abcdef')
>>> m.group(0)
'def'

含まれるパターンは、固定長の文字列にのみマッチしなければなりません。

先読みアサーションとは異なって、パターンに正規表現を使うことはできない。

シャーロック・ホームズの相棒だけを探す。

>>> import re
>>> watsons = ['JamesWatson', 'JohnHWatson', 'IBMWatson']
>>> for watson in watsons:
...     m = re.search(r'(?<=JohnH)Watson', watson)
...     if m is not None:
...             print(m.group(0), watson)
...     else:
...             print('None', watson)
...
None JamesWatson
Watson JohnHWatson
None IBMWatson

同じファミリーネームの人物がなかなか思いつかなくて苦労した。一つだけ人間じゃないのが混ざってるけど気にしない。

否定後読みアサーション

  • (?<!...)

文字列内の現在位置の前に ... とのマッチがない場合に、マッチします。

とある。また後読みアサーションと同様にパターンに正規表現は使えない。

今度はシャーロック・ホームズの相棒じゃない人たちを探す。

>>> for watson in watsons:
...     m = re.search(r'(?<!JohnH)Watson', watson)
...     if m is not None:
...             print(m.group(0), watson)
...     else:
...             print('None', watson)
...
Watson JamesWatson
None JohnHWatson
Watson IBMWatson

DNAの二重螺旋構造を見つけてノーベル賞取った人とIBMのすごいやつがマッチした。

後読みアサーションは特定のprefixを持つ(あるいは持たない)文字列をマッチさせる、というような使い方ができそう。

そういう感じで正規表現の先読み・後読みアサーションについてわかったことを書いてみた。