« ヒラヒラ | トップページ | tkinterdnd2で、ドラッグ&ドロップしたファイルのフルパスの取得 »

2023年6月30日 (金)

正規表現の先読みと後読み

夜中に少し雨が降っていたようで、日中は曇りが多かったけれど、雨は降ったり止んだり。

 

なんだかわけわからない記号だらけの正規表現、ずーっと避けていたんだよね。(苦笑)

何年前だろう?
ひょんなことから、正規表現を勉強してみようと思って、やってみた。

色んな情報を見て、試行錯誤して、なんとかそれなりに出来るようになり、検索や置換でかなり重宝するようになった。

もし、あの時、正規表現を勉強していなかったら、今手元にあるプログラムのいくつかは実現出来ていなかったかもしれないし、検索や置換の時間効率はかなり変わっていただろう。

それだけ、正規表現って、「画期的」な検索/置換方法。

よくもまぁ、こんなのを考えたな、と思うよ。(笑)

でも、正規表現の記述を考えている時間の間に、せっせと普通に検索・置換をした方が速かったというのは、何度もあった。(苦笑)

 

やっていて、ほとんどの人が引っ掛かるのではないだろうか?

最長一致と最短一致。

今回は、これについては述べない。

これも、何度も試行錯誤して、あぁ、そういうことかぁ、というのがわかった。

そして、最後の砦と言ってもいいか?

正規表現の先読みと後読み!

これも勉強はしたんだけれど、なんか飲み込めなくて...

でも、ひょんなことから、それなりに理解出来てしまった。

 

切っ掛けは、ある文字列を含む行「以外」を削除したい、というものだった。

ネットで検索すると、「^(?!.*文字列).*$\r\n」で、その文字列が含まれていない行すべてを削除出来るというのを知った。

これは無茶苦茶便利!

この中に、「?!」というのが使われていて、これは、否定先読みというもの。

じゃぁ、改めて勉強してみるか、と思って、調べてみると、ああ、そういう考え方なんだ、というのがあったので、それで理解が深まった。

ここでは、それについて、書いてみたい。

 

そもそも、先読み、後読み、という名付けが誤解の元なのかもしれない。

その通り理解しようとすると、こんがらがる。

こう理解すればいい。

  • 先読み = 検索文字列の直後に何かがある/ない
  • 後読み = 検索文字列の直前に何かがある/ない

という感じ。

これを、別の書き方にすると、

  • (後読み)検索文字(先読み)

という感じになる。

イメージとは逆の場所に、それぞれがある、という感じかな?
イメージであれば、(先読み)検索文字(後読み)になると思う。

これを頭に入れて、それぞれの肯定/否定も含めた記号を覚えればいい。

  • 肯定先読み => 検索文字(?=何か)
  • 否定先読み => 検索文字(?!何か)
  • 肯定後読み => (?<=何か)検索文字
  • 否定後読み => (?<!何か) 検索文字

何か、というのは、検索文字の前後にある、何らかの文字や正規表現の記号のこと。

 

では、具体的に。

aaa県aaa市
aaa県bbb市
aaa県ccc市
bbb県ddd市
bbb県eee市
bbb県aaa市
ccc県bbb市
ccc県ccc市
ccc県ddd市

aaa(?=市)(?<=県)aaa => 1行目と6行目のaaa(市)
aaa(?!市)aaa(?=県)(?<!県)aaa => 1~3行目のaaa(県)

1行目と6行目のaaa(市)に一致させたいなら、書き方は、その左辺の2通り。

まず、aaaを検索し、その直後に「市」があるか?肯定先読み。
もう一つは、aaaを検索し、その直前に「県」があるか?肯定後読み。

先読みであれ、後読みであれ、肯定であれば、こういう書き方が出来る、という一例。

次に、1~3行目のaaa(県)に一致させたいなら、書き方は、その左辺の3通り。

aaaを検索し、その直後は「市ではない」もの。否定先読み。
aaaを検索し、その直後は「県である」もの。肯定先読み。
aaaを検索し、その直前は「県ではない」もの。否定後読み。

正規表現検索が出来る、テキスト・エディターで確認するといいでしょう。

先読み、後読みというより、直後の何か、直前の何か、という風に理解した方がわかりやすいように思う。

 

次に、よくある置き換え処理。

<div class="aaa">bbbbb</div>
/?div => spanに置き換え
(?<=/|<)div(?<=/|)div => spanに置き換え

「div」を「span」に置き換えしたい、なんていう場合。

一般的には、1行目に書いている通りで、「/?div」の方がわかりやすい。

「?」は、その直前のものがあってもいいし、なくてもいい、という記号。

だから、最初の「div」と「/」のある「div」両方が対象となる。

これを、肯定後読みで表現すると、2行目になる。

一応、2通り書いた。

「/|<」と「/|(なし)」。

「|」は、orなので、どちらかがあれば、ということ。

後者の方は、何も書かなくても、「div」そのものが対象となるようだ。

こういう風に検索すれば、「span」に置き換え出来る。

このように、正規表現の先読みと後読みが理解出来るようになると、更に、検索の幅が広がるね。

 

次は、へー、そんなことも出来るんだ、という事例。

aaa-bbb-ccc-ddd-111
111-bbb-aaa-ccc-ddd
ddd-bbb-ccc-111-aaa
zzz-bbb-aaa-111-ttt

1~3行目にマッチ
(?=.*aaa.*)(?=.*bbb.*)(?=.*ccc.*).*

こういう書き方をすれば、4行目以外をマッチさせることが出来るのだそうだ。

こういう書き方をしなければ、「|」を使って、全ての組み合わせで書かないといけないのだけれど、それがこんな短い1行で表現出来る。

 

最後は、これが本来の言葉通りの意味、使い方なのかな?と思ったもの。

本来の言葉の使い方ではないかもしれないけれど、あくまで、解釈ということで。

book
hook
look
nook

(?=book)bo => 1行目の「bo」に一致
k(?<=nook) => 4行目の最後の文字「k」に一致

「(?=book)bo」というのは、まず、「book」という文字を探し、あれば、その中の「先頭」から見て、「bo」という文字があるか?

今回の場合であれば、「book」の先頭の2文字「bo」が、それに当たる。

「k(?<=nook)」というのは、「nook」という文字を探し、あれば、その中の「後方」から見て、「k」という文字があるか?

今回の場合であれば、4行目の「nook」の最後の文字「k」が、それに当たる。

前者は、検索した文字の「先」頭から読んで、後者は、検索した文字の「後」方から読んで、ということで、先読み、後読み、なんていう風に繋げて覚えるといいかも。
あくまで、覚え方として、ね。

ちなみに、最初に紹介した「ある文字列を含む行以外を削除  ^(?!.*文字列).*$\r\n」は、このやり方に当たり、否定先読みになると思う、多分。(苦笑)

 

同じ先読み、後読みであっても、先述のものとこれでは、ちょっと使い方が違っているよね?

以前勉強した時、理解出来なかったのは、これらがごっちゃになって、何が何やら、ということで、理解出来なかったのかもしれない。

ということで、同じ先読み、後読みでも、2種類の検索の仕方がある、というのは知っておきたい。
そうすれば、こんがらないですむと思う。

多分、だけれど、どちらの使用頻度が高そうか?と考えると、前者の、直後・直前の説明で書いた方が多いような気がする。

 

まさか、ある単語が含まれていない行をすべて削除することから、正規表現の先読みと後読みのことを、ある程度理解できるようになるとは思わなかったな。(笑)

|

« ヒラヒラ | トップページ | tkinterdnd2で、ドラッグ&ドロップしたファイルのフルパスの取得 »

パソコン・インターネット」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« ヒラヒラ | トップページ | tkinterdnd2で、ドラッグ&ドロップしたファイルのフルパスの取得 »