Pythonで正規表現:先読みアサーション(再論)

具体的な例から考察しよう。ファイル名は基幹名(basename)と拡張子(extension)をドット(.)で区切って表現する。例えば、ファイル名abc.txtはabcが基幹名で、txtが拡張子である。

例えば多くのファイル名のマッチングのタスクで、特定の拡張子を持つファイル名をマッチングから外したいとする(例えば拡張子bat)。

この逆、つまり特定の拡張子を持つファイル名のみマッチングさせたいという処理は極めて簡単である。正規表現で書くと

.*[.]bat$

区別に関心のない基幹名(ドットを含めて)はドットで終わる任意の長さの任意の文字列とのマッチだから.*[.]となる。
この処理の逆、つまり特定の拡張子を持つファイル名をマッチングから除外しそれ以外の多くの拡張子を持つファイル名をマッチさせたいというのが本来のタスクである。このようなばあいに使えるのが「先読みアサーション」である。「先読みアサーション」には否定と肯定とがあるが、ここでは否定を使う。

パターンAパターンB

パターンAまでマッチングが進んだ時点でこれ以降がパターンBであるかどうかをその時点で検証し、であるとマッチングは失敗とする。ないとマッチングは継続する。問題に即して正規表現で書くと

.*[.](?!bat$)

先読みアサーション(否定)は(?!正規表現)と書く。この時点以降の文字列がこの正規表現とマッチするか検証する。それ以降で例えばbatchではマッチしない。rebatもマッチしない。マッチするものはbatのみである。これがマッチするとマッチング作業は失敗である。それがマッチしないと、後はなんの制限もなしにマッチさせればよいので最終的な正規表現は以下のようになる:

.*[.](?!bat$).*$

後読みアサーションというものもある。これも否定と肯定がある。この否定は(?<!..)と書く。例えば「ファイル名の基幹名がabc以外であるとマッチする」というような処理で使える。ファイルの基幹名がabc以外の全てのファイル名にマッチする正規表現は以下のようになる。

(?<!^abc)[.].*$

これは左からドットの位置まで探索を進め、そこで振り返って先頭からドットまでの間の文字列がabcであるとマッチングは失敗とする。それ以外では探査を右に進め通常のマッチングを続ける。この例では.*$となっているので文字列の最後まで任意の長さの任意の文字列がマッチされる。しかし結果としてマッチされる文字列は[.].*$の部分のみである。基幹名もマッチさせるには以下のようにする:

.*(?<!^abc)[.].*$

これで基幹名がabc以外の全てのファイル名がマッチされる。拡張子がなんであれ、基幹名がabcのファイル名はマッチに失敗する。

先読み(後読み)アサーションはこのように上手く使うと大変の便利な機能である。