簡単な肯定の先読みの例を見てみましょう。
正規表現牛(?=丼)
は、牛
の右側に丼
がある場合、牛
にマッチします。丼
は先読み(?=丼)
によってマッチに必要な条件ですが、ゼロ幅マッチなのでマッチ文字列には入りません(丼
という文字を消費しません)。そのため、マッチ文字列すなわち置換対象文字列は牛
となり、結果的に単語牛丼
のみ豚丼
に置換されます。
この例だとs/牛丼/豚丼/g
でも代用できます。しかし、複数の正規表現演算子や文字列関数を駆使しないと実現できなさそうな要望も、先後読みを使うと単一の正規表現で簡潔に書ける場合があります。
先や後はマッチカーソルの動き
初学者が先後読みを学んだ際、「先や後の意味がわからない」と訴える場面をしばしば見かけます。
前項のs/牛(?=丼)/豚/g
のサンプルは、「牛
の後に丼
がある場合」といった説明もできるでしょう。先読みを使用したのに、その文字の後と取れる説明もできるわけです。ここでの「後」は、文字の登場順、つまり時間軸における「後」ととらえています。
しかし、先後読みの先や後は、時間軸のことではなくマッチ文字列を取り込んでいく先や後と位置関係でとらえるのがお勧めです。言い換えると、文章を読む目線の移動方向、すなわち書字方向の向かう先が先読み、その逆側が後読みです。日本語や英語を含む多くの言語の書字方向は左から右へ向かうので、右側を対象とするのが先読み、左側を対象とするのが後読みと考えて差し支えありません。
時間軸で考える場合も、「牛
をマッチ文字列候補に取り込んだけれど、次の文字をマッチ文字列候補として取り込む前に、先に次の文字を偵察する」だと先読みの意味を正しく反映した説明となります。ただ、時間軸よりも位置関係でとらえるほうが誤解が起こりづらいと筆者は考えています。
「後」が時間軸を想像させるからか、「後読み」の代わりに「戻り読み」「後ろ読み」といった位置関係を強調した用語を使う人もいます[2]。また、先読みと後読みの英語での表現は、それぞれlook aheadとlook behindです[3]。戻り読みや後ろ読み、または英単語aheadやbehindがしっくり来る人は、そちらで覚えるのもよいでしょう。
ゼロ幅マッチは一段下げて読む
先読みや後読みはゼロ幅マッチであると解説しましたが、ゼロ幅マッチを含んだ正規表現を読むときに筆者が頭の中で想像するのが、1行にした正規表現からゼロ幅マッチ部分だけを下の行に移す方法です。
図1では(1)の正規表現牛(?=丼)
を例として、(2)で肯定の先読み(?=丼)
を下の行に移しています。ゼロ幅マッチが位置にマッチすることと、先読みの方向がわかりやすいよう、(3)のように正規表現内の位置を線で示して、ゼロ幅マッチの指示を方向がわかりやすい図形にすると、視覚的にわかりやすくなります。
このように、通常の幅のある文字列マッチと、ゼロ幅マッチを上下に分けて書き、下の行にあるゼロ幅マッチの情報を上の行にある幅のある文字列マッチへの指示として書くことで、見通しが良くなります。
隣接する複数のゼロ幅マッチはAND条件
ゼロ幅マッチは位置にマッチしますが、複数のゼロ幅マッチが隣接している場合はAND条件になります。
(1)は、冒頭^
かつ否定の先読み(?!\d)
によって数値が右側に存在しない位置に文字列WARNING:
を挿入するサンプルです。正規表現がゼロ幅マッチのみで構成されており、文字を消費しないため、置換がゼロ幅マッチで確定した位置への文字列挿入として機能します。出力結果は下記になります。
上記で「かつ」を使って説明したことからわかるように、^(?!\d)
は位置に関する2個の条件のAND条件となっており、順序を逆にして(?!\d)^
と書いても同じ意味です。しかしこの場合は、左端に書かれることが自然に感じる冒頭^を、わざわざ否定の先読み(?!\d)
の右側に書く意義はないでしょう。ただ、複数のゼロ幅マッチの正規表現が隣接している場合、それらはその位置に関するAND条件になっていて、かつその順序の入れ替えで意味が変わらないことを理解しておくと、ゼロ幅マッチへの理解が深まります。
(1)で否定の先読みを使わず、1つのs///
に収めようとすると、幅のある1文字をキャプチャした結果によって条件分岐させることになるでしょう。
しかし、数値に関する検査のため、幅のある1文字をキャプチャする必要があり、上記の例では$line
が空文字の場合に対応できていません。また、s{●}{■}e
の置換文字列を得るためのPerlコード■
の中にm//
があることが読みやすいかどうかは、議論が分かれるでしょう。
幅のある1文字をキャプチャする例と比較すると、否定の先後読みは、左右にそもそも文字列が存在しない冒頭や末尾で効果を発揮することがわかります。
先後読みを使うか、それとも複数のステップで文字列を分解および検査していくかは、開発現場ごとの方針もあるでしょう。しかし上記の例でも、先後読みの恩恵の一端が垣間見えます。
否定の先後読みの実践
前項で否定の先読みの例が登場しましたが、引き続き、否定の先後読みの実践的な活用例を見ていきましょう。
紙幅の都合上、肯定の先後読みの実践的な活用例の解説は割愛します。s/牛(?=丼)/豚/g
とs/牛丼/豚丼/g
の例で見たとおり、肯定の先後読みは幅のあるマッチで代用しやすい傾向にあるためです。ただし、肯定の先後読みは、文字を消費しないメリットが置換で活きる場面もあります。
否定の先後読みを日本語の単語境界の代用として使う
筆者は東京在住なのですが、プライベートで定期的に京都を訪れています。過去の京都訪問時のことを調べようと、移動の記録から「京都」を検索すると、「東京都」を含む大量のエントリがヒットしてしまう課題がありました。
検索に正規表現を使えるのであれば、否定の先後読みがこの課題を解決してくれます。
ちなみに、単語境界\b
は日本語文字列に対応していない[4]ので、\b京都\b
ではうまくいきません[5]。
否定の後読みで単語マッチを限定する
「東京都」ではない「京都」にマッチする、否定の後読みを使った正規表現の例を見てみましょう。
(3)の正規表現(?<!東)京都
によって、文字列「京都」があり、かつ「京」の左側に「東」がない場合に限り、「京都」にマッチします。これによって、(2)だけでなく(1)の文字列にもマッチしません。
否定の先読みで単語マッチを限定する
本項では、「東京都」だけでなく「京都府」や「京都市」にもマッチしない例を題材に、マッチ文字列の右側に条件を課す否定の先読みを見ていきます。
(3)の正規表現(?<!東)京都(?!府)(?!市)
は、否定の後読み(?<!東)
のほか、2つの否定の先読み(?!府)(?!市)
によって、「東京都」「京都府」「京都市」の一部ではない「京都」にマッチします。この例では、(1)(2)が表示されます。
ゼロ幅マッチを一段下げて読む例で表すと、図2となります。正規表現(1)から否定の先後読みを下の行に移して(2)とし、視覚的にわかりやすいよう(3)で否定の先後読みを表した図形が正規表現内の位置に指示を与えています。AND条件となる(?!府)
と(?!市)
の関係も把握しやすくなります。
まとめ
本稿では、Perlの正規表現の話題の中から、初学者向けの丁寧な解説が少ないと筆者が感じる事項について解説しました。
Perlの正規表現は、それ自体が小さなプログラミング言語と思えるほどの機能を有しています。今回解説した内容が、みなさんの一歩進んだ正規表現の理解と活用の一助となれば幸いです。
さて、次回の執筆者は白方健太郎さんで、テーマは「Perlで作る非中央集権型ソーシャルネットワークサーバ」です。お楽しみに。