言語処理100本ノックをやっているとき、正規表現するときにre
に関して新しく学んだこともあるので、ここでまとめておこうと思う。
searchとmatchの違い
- searchはマッチ対象文字列の先頭からマッチしていなくてもマッチする。
- matchは必ずマッチ対象文字列の先頭からマッチしていないとマッチしない。
たとえば、grape
と言う文字列に対してape
というパターンをマッチさせる場合、search
を使った場合はgrape
にはape
が含まれているのでマッチするが、match
を使った場合は、ape
は含まれているが先頭からは始まっていないので、マッチしない。
m = re.match('ape', 'grape') # マッチしない
m = re.search('ape', 'grape') # マッチする
マッチした部分を取り出す
正規表現でマッチした時、そのマッチした部分を取り出したい時がある。
次の例で言うと、'1993'
や'7'
, '2'
を取り出したい場合である。
s = "1993年7月2日生誕"
p = "([0-9]+)年([0-9]+)月([0-9]+)日"
m = re.search(p, s)
マッチ対象文字列(前述のスニペットのs
)のうち、()
でくくられたパターンにマッチした部分は、m.group(n)
メソッドで取り出すことができる。引数のn
は次のようにする。
- パターン全体とマッチした文字列: n=0
- 先頭からi番目の
()
に該当する文字列: n=i
つまり、上記の例で言うとこうなる。
m.group(0) # -> '1993年7月2日'
m.group(1) # -> '1993'
m.group(2) # -> '7'
m.group(3) # -> '2'
注意点として、.group()
の返り値は数値ではなく必ず文字列になるので、数値として扱いたい場合は、適宜キャストして使おう。
マッチさせる部分に名前をつける
前項のケースで、数字ではわかりづらいと言う場合がある。その時は、(?P<name>regex)
のように書くと、先ほど.group()
で引数でどの部分を取り出すか指定したところを、name
という名前で取り出すことができるようになる。
具体的には次のようにする。
s = "1993年7月2日生誕"
p = "(?P<year>[0-9]+)年(?P<month>[0-9]+)月(?P<day>[0-9]+)日"
m = re.search(p, s)
m.group(0) # -> '1993年7月2日'
m.group('year') # -> '1993'
m.group('month') # -> '7'
m.group('day') # -> '2'
マッチした部分を全て取り出す
長い文章で正規表現抽出を行っている時、たとえば「con」で始まる単語を全て取り出したい場合がある。そのような時はre.findall()
を使おう。
s = 'It\'s convenient to conclude you are conservative.'
p = 'con\w+'
m = re.findall(p, s)
m # -> ['convenient', 'conclude', 'conservative']
改行文字にマッチする
パターン文字列に.
を使う場合、任意の文字にマッチできるが、例外として改行文字(\n
)にはマッチできない。
例えば下記の場合、'abc=def\nghi\njkl'
がマッチすると思いきや、'abc=def'
までしかマッチしない。
s = 'abc=def\nghi\njkl'
p = '^abc=.+'
m = re.search(p, s)
m.group() # -> 'abc=def'
これは、'.'
というメタ文字が\n
には例外的にマッチしないことに由来している。こういうときは、search()
の第3引数にre.DOTALL
フラグを立てる。
s = 'abc=def\nghi\njkl'
p = '^abc=.+'
m = re.search(p, s, re.DOTALL)
m.group() # -> 'abc=def\nghi\njkl'
ね?簡単でしょ?
複数行の文章のにマッチする
Webスクレイピングすると、ある特定のタグで始まる行だけ取り出したい、と言うケースもあるのではないか。(やったことないけど)
s = """<p>Pieter Pipar piked a peck of pickled pepers.</p>
<hr>
<p>A pek of pickled pepers Pieter Pipar piked.</p>
<p>If Pieter Pipar piked a pek of pickled pepers,<p>
<hr>
<p>How many pickled pepper did Pieter Pipar picked?</p>"""
p = "^<p>.+$"
適当に<p>
で囲んで、<hr>
で水平線入れてみた。
この状態から、<p>
で始まる行だけ取り出したいとする。
このとき、.findall
を使うが、改行文字が含まれるので、re.MULTILINE
フラグを使うと、改行文字で分割した上で、それぞれの行に対してパターンマッチすることができる。
m = re.findall(p, s, re.MULTILINE)
m
# ['<p>Pieter Pipar piked a peck of pickled pepers.</p>',
# '<p>A pek of pickled pepers Pieter Pipar piked.</p>',
# '<p>If Pieter Pipar piked a pek of pickled pepers,<p>',
# "<p>Where's the pek of pickled pepers that Pieter Pipar picked?</p>"]
最後だけダブルクォーテーションになってるのは謎だが、便利だね。
参考
- re — Regular expression operations — Python 3.9.1 documentation
https://docs.python.org/3/library/re.html - ピーター・パイパー - Wikipedia
https://ja.wikipedia.org/wiki/%E3%83%94%E3%83%BC%E3%82%BF%E3%83%BC%E3%83%BB%E3%83%91%E3%82%A4%E3%83%91%E3%83%BC