正規表現re.findall()と重複の落とし穴【Python】

Oct. 16, 2019, 6:02 a.m. edited Dec. 21, 2019, 5:19 a.m.

#正規表現  #Python 

前回(正規表現のre.search().groups()とre.findall()の違い【Python】)の続きです。引き続きPythonと正規表現のreモジュールでやっていきます。

まずは問題(という落とし穴)

sssabcssssssdefsssdefsss

という文字列からabcから始まりdefで終わる3の倍数の長さでかつ最長の文字列を抽出したいとします(つまりabcssssssdefsssdef)。

これは正規表現を使って、

>>> re.search('abc(...)*def', 'sssabcssssssdefsssdefsss').group()
'abcssssssdefsssdef'

でできます。結果がabcssssssdefではなくabcssssssdefsssdefとなるのは、正規表現が最長一致をとってくるからです。

では、

sssabcsabcssdefsssdefsssssssssssssssssssdefs

という文字列であった場合はどうなるでしょう。最長のabcssdefsssdefsssssssssssssssssssdefをとってきてほしいところですが、果たして...!

>>> re.search('abc(...)*def', 'sssabcsabcssdefsssdefsssssssssssssssssssdefs').group()
'abcsabcssdefsssdef'

はい、そうなる気はしていました。というのも、前回触れたように、

re.search()は最初にマッチした文字列全体を返す

ためです。一方、re.findall()

マッチした文字列全体をすべて見つけて返します。

そこで、re.findall()を使ってみる1と、

>>> re.findall('(abc(...)*def)', 'sssabcsabcssdefsssdefsssssssssssssssssssdefs')
[('abcsabcssdefsssdef', 'sss')]

1つのマッチした文字列全体(のグループ)しか出てこないじゃんっ!!!

原因

どうやらre.findall()はマッチした文字列全体を1つ見つけると、次のマッチした文字列全体をそれ以降の文字列から探すようです。つまり、2つのマッチした文字列全体が重複していると、最初の方しか抽出されないことになります。つまり、今回の例でいうと、

sssabcsabcssdefsssdefsssssssssssssssssssdefs

から最初のマッチした文字列全体abcsabcssdefsssdefを取り出す。すると、残りの文字列は

sssssssssssssssssssdefs

となるので、ここから次のマッチした文字列全体を探す(が、もちろん存在しないので、探索は終了)。ということになります。

対策

肯定的先読み?=を使います。

>>> re.findall('(?=(abc(...)*def))', 'sssabcsabcssdefsssdefsssssssssssssssssssdefs')
[('abcsabcssdefsssdef', 'sss'), ('abcssdefsssdefsssssssssssssssssssdef', 'sss')]

これは?=の後ろにパターン(今回は'(abc(...)*def)')がある場合に?=の手前のパターン(今回は''つまり空文字列)に一致します。したがって、re.findall()の挙動としては、

sssabcsabcssdefsssdefsssssssssssssssssssdefs

から最初のマッチしたパターン''を取り出す。すると、残りの文字列は

bcsabcssdefsssdefsssssssssssssssssssdefs

となるので、次に再びマッチしたパターンを取り出すことができます2

あとは、問題の目的としては最長の文字列を得ることなので、re.findall()で得られた文字列の中で最長のものをとってくれば良いです。例えば、

>>> all_groups = re.findall('(?=(abc(...)*def))', 'sssabcsabcssdefsssdefsssssssssssssssssssdefs')
>>> max(all_groups, key=lambda x: len(x[0]))[0]
'abcssdefsssdefsssssssssssssssssssdef'

という感じで。


  1. 全体に括弧をつけてグループ化しないと、真ん中の括弧で包まれた3文字しか得られずわかりづらいので、パターン文字列を(abc(...)*def)とした。 

  2. あくまでもマッチした文字列全体としてはただの空文字列''なので、re.search()の挙動としては、

    >>> re.search('(?=(abc(...)def))', 'sssabcsabcssdefsssdefsssssssssssssssssssdefs').group()
    ''
    >>> re.search('(?=(abc(...)def))', 'sssabcsabcssdefsssdefsssssssssssssssssssdefs').group(1)
    'abcsabcssdefsssdef'
    >>> re.search('(?=(abc(...)def))', 'sssabcsabcssdefsssdefsssssssssssssssssssdefs').group(2)
    'sss'
    >>> re.search('(?=(abc(...)def))', 'sssabcsabcssdefsssdefsssssssssssssssssssdefs').groups()
    ('abcsabcssdefsssdef', 'sss')
    となる。