検索の"前"と"次"で、正規表現"(?>pattern)"(原子的式集合)のヒットする回数が違う

  1. 気にするほどのことでもないのですが、正規表現ライブラリのサポートが増えた記念に些末なことを…

    例文:
    __test__test__test__test__
    __test__test__test__test__

    正規表現:
    (?>__test__)

    * 鬼車 (FindOnig=1)
    + "次を検索(N)"だと全部(8回)ヒットしてしまう
    + "前を検索(P)"だとヒットするのは4回

    * 鬼雲 (FindOnig=0)
    + "次を検索(N)"だと全部(8回)ヒットしてしまう
    + "前を検索(P)"だとヒットするのは4回

    ちなみに他のでは…

    * サクラエディタ (bregonig.dll Ver.4.20 with Onigmo 6.2.0)
    検索方向に関係なくヒットするのは4回

    * .NET Framework
    ヒットするのは4箇所

    * Notepad++
    ヒットするのは4箇所

    個人的には4回に統一していただけると嬉しいのですが…

    少なくとも、検索において、行きと帰りでヒットする回数(箇所)が違うというのは
    如何なものなのかなぁ~と思う次第です。

     |  myupon  |  返信
  2. ご報告ありがとうございます。

    マッチする回数が 8 回か 4 回かという点については、[次を検索] を繰り返す場合に、次の文字列をどこを始点として検索するかという仕様次第で、テキストエディターによってその設計思想は様々です。

    Mery の場合は [次を検索] でなるべく取りこぼしを発生させないよう、マッチした文字列から 1 文字ずつ次を検索する方式を採用しているので 8 回マッチします。

    この仕様については、どのエディターが正解といった答えはないと思っていますが、以前にブログ記事のネタとして調査したことがありますのでご興味がありましたらどうぞ。
    https://www.haijin-boys.com/software/mery/mery-2-2-2

    鬼車 (FindOnig=1) については、確かに [次を検索] は 8 回、[前を検索] は 4 回マッチになってますね。

    鬼雲のほうは [前を検索] でも 8 回マッチするようなので、これは FindOnig=0 の設定ミスではないでしょうか?

    > 少なくとも、検索において、行きと帰りでヒットする回数(箇所)が違うというのは如何なものなのかなぁ~と思う次第です。

    これは私も同感です。(上記の参考リンクの記事にも、そんなことを書いてました)

    そういうわけで、鬼車でも [次を検索]、[前を検索] で同じ回数 (この場合は 8 回マッチ) が正しいと思います。

    鬼車の実装はまだ手探りの状態なのですが、逆方向検索が鬼雲と同じ実装方法だと非常に遅かったので、今回、ちょっと別の方法で実装してみたんです。こんな落とし穴があったとは…。

    もっと研究が必要ですね。

     |  Kuro  |  返信
  3. > Mery の場合は [次を検索] でなるべく取りこぼしを発生させないよう、マッチした文字列から 1 文字ずつ次を検索する方式を採用しているので 8 回マッチします。

    なるほど、では無理ですね。

    > この仕様については、どのエディターが正解といった答えはないと思っていますが、以前にブログ記事のネタとして調査したことがありますのでご興味がありましたらどうぞ。
    > https://www.haijin-boys.com/software/mery/mery-2-2-2

    読ませていただきました。
    私も「正しい」を論ずる気はありません。
    ただ、対応ライブラリが増えたとの事だったので「そういえば…」と思い出し試したところ、8回/4回だった…
    で、手を付けるのであれば、4回の方に統一していただけたら嬉しいなぁ~と…
    すみません、その程度です(^_^ゞ

    > 鬼雲のほうは [前を検索] でも 8 回マッチするようなので、これは FindOnig=0 の設定ミスではないでしょうか?

    ??? FindOnig=0 で8回ヒット ???
    …ということなので確かめてみました。

    現在の私の設定ファイルそのままで…

    FindOnig=0
    Oniguruma → バージョン番号あり ← ??
    Onigumo → バージョン番号あり
    ※両方「あり」です

    FindOnig=1
    Oniguruma → バージョン番号あり
    Onigumo → バージョン番号なし

    なるほど、私が確認したのは鬼車だけだったようです。
    そこで、私の設定ファイルを全部他に移動して確認。

    FindOnig=0
    Oniguruma → バージョン番号なし
    Onigumo → バージョン番号あり

    FindOnig=1
    Oniguruma → バージョン番号あり
    Onigumo → バージョン番号なし

    なるほど、おっしゃる通り、鬼雲は行きも帰りも8回でした。
    隠しオプションとかを気ままに設定している所為かな??
    とはいえ、鬼車で不都合ないので、このまま使わせていただこうと思います。

    もちろん、私の設定のままでも
    FindOnig=0 にして
    onig.dll を外しておけば
    鬼雲も使える(行きも帰りも8回ヒットする)ので、そもそも不都合はありません。

     |  myupon  |  返信
  4. > で、手を付けるのであれば、4回の方に統一していただけたら嬉しいなぁ~と…
    > すみません、その程度です(^_^ゞ

    いえいえ、こちらこそ。貴重なご意見ありがとうございます。

    4 回方式への仕様変更はちょっと、長年この仕様でやってきたこともあって難しいのですが、行きと帰りで同じ回数ヒットしないという問題は気づいてなかったので、とても参考になりました。

    > 隠しオプションとかを気ままに設定している所為かな??

    Mery を起動している状態で Mery.ini を書き換えると反映されないので、もしかしたらそれかも?

    > とはいえ、鬼車で不都合ないので、このまま使わせていただこうと思います。

    ありがとうございます。日常で使う用途なら、Mery でも数年の実績がある鬼雲のほうが安心ですが、検証にご協力いただける場合は鬼車、よろしくお願いします!

    しかし、いただいたご意見を参考に逆方向検索の実装を考え直していたところ、鬼車ではなく「鬼雲」のほうももっと美しく実装できるような気がしてきました。

    この土日は逆方向検索を研究してみたいと思います。

     |  Kuro  |  返信
  5. いつも便利に使わせていただいているのに、報告する手間を惜しむのは如何なものかと思い
    続報というか、参考になるなら(こんなこともあるよ)という思いだけですが

    Mery Version 3.2.8

    行頭に複数の空白文字がある場合…
    正規表現「^\p{Blank}+」で検索すると、行きと帰りでヒットする回数が違います。
    ちなみに、鬼雲でも鬼車でも振る舞いは同じです(行きと帰りでヒットする回数が違います)。

    「じゃあ、強欲「^\p{Blank}++」ならどうなのよ?」と思い、試しましたが、結果は全く同じでした。
    行きは強欲、帰りは謙虚。
    行きに欲をかき過ぎたのを反省して、帰りは謙虚になるのかなと思うと、愛おしくもある。

    前回もそうですが「これの何が問題なんだ?」と言われると
    「正規表現の振る舞いを確認する時、検索開始位置に戻るのに、わずかに手間が増えることぐらいしか思いつかない(;^_^A」

    まぁ
    > Mery の場合は [次を検索] でなるべく取りこぼしを発生させないよう、マッチした文字列から 1 文字ずつ次を検索する方式を採用しているので 8 回マッチします。

    とのことなので「こういう振る舞いは、やむ無しなのかなぁ~」と個人的には納得してます。
    ということで、以上、報告だけでした。

     |  myupon  |  返信
  6. ご報告ありがとうございます。

    いただいた条件で現象を再現することができました。

    調査しましたところ、これは前回「(?>__test__) の件」で逆方向検索の仕様を変更したことによる副作用ということがわかりました。

    逆方向検索を繰り返す場合、再検索の開始位置をマッチした文字列の "左端" を基準にするか、"右端" を基準にするかの違いです。

    従来は逆方向検索でマッチした文字列の "左端" を基準に再検索しており、取りこぼしが発生していたため、「(?>__test__) の件」で修正した際にマッチした文字列の "右端" を基準にするよう、仕様変更しました。

    そのため、正規表現「^\p{Blank}+」で逆方向検索した場合、マッチした文字列の "右端" を基準に再検索をかけるので、マッチした文字列の右端が行頭「^」にマッチしなくなるまで、その行がヒットし続けるというわけです。

    以下はちょっとした実験ですが、長いのでスルーでもかまいません ^^;

    前回、ご紹介したブログ記事の内容と近いものですが、エディターの状況も変わっていることですし、本件はときどきご質問をいただくこともあるので再調査してみました。

    例えば、

    AAAAAAAAAAA
    

    という A が 11 個のテキストを用意します。A の数を 3 で割り切れない数にしておくのがポイントです。

    そして「AAA」や正規表現の「...」など、3 文字の文字列で前後に検索してみると、本件についての挙動が分かりやすいと思います。

    これをもとにいくつかのエディターで実験してみました。

    結果です。

    順方向、逆方向の検索を繰り返すときに "マッチした文字列" の「左端 (開始位置)」を基準に再検索するか、「右端 (終了位置)」を基準に再検索するかにご注目ください。

    もちろん、順方向の検索でマッチした文字列の「左端」をそのまま基準にして再検索すると同じ位置がヒットしてしまうので、順方向の場合の「左端」は「左端 +1」から再検索という意味です。(逆方向の右端の場合も同様)

    --------------------------------
    ① 順方向は左端、逆方向は右端、検索文字列の強調表示は左端
    ・Mery 3.2.8
    ・秀○エディタさん 8.97 (正規表現オフの場合)
    --------------------------------
    検索における取りこぼしはありません。検索結果と検索文字列の強調表示は一致します。

    --------------------------------
    ② 順方向は左端、逆方向は右端、検索文字列の強調表示は右端
    ・Visual Studio 2019
    ・E○Editor さん 20.9.2
    ・秀○エディタさん 8.97 (正規表現オンの場合)
    --------------------------------
    検索における取りこぼしはありませんが、検索文字列の強調表示は最後の「AA」を取りこぼすので、検索結果と検索文字列の強調表示が一致しません。

    Visual Studio 2019 は検索方向を切り替えた一発目だけ③方式になるという謎仕様。

    また、秀○エディタさんは正規表現のオフ・オンによって①方式と②方式に挙動が変わるようです。

    --------------------------------
    ③ 順方向は右端、逆方向は左端、検索文字列の強調表示は右端
    ・Visual Studio Code 1.58.2
    ・Sublime Text 3.2.2
    ・サク○エディタさん 2.4.1.2849
    --------------------------------
    検索、検索文字列の強調表示ともに最後の「AA」を取りこぼしますが、検索結果と検索文字列の強調表示は一致します。

    最後の「AA」は順方向でも逆方向でもどうがんばってもヒットしません。

    しかしながら、これがご希望の仕様なのかもしれません。

    --------------------------------
    ④ 順方向も逆方向も右端
    ・メモ帳
    ・Notepad++ 8.1.2
    --------------------------------
    順方向では最後の「AA」を取りこぼしますが、逆方向では最後の「AA」にヒットします。

    以上です。

    というわけで、今回の正規表現「^\p{Blank}+」で検索して、行きと帰りでヒットする回数が一致するのは③方式のエディターとなります。

    行きと帰りでヒットする回数が一致しないのは、①、②方式で、Visual Studio 2019、E○Editor さんは Mery と同じ挙動でした。

    秀〇エディタさんで正規表現を使うと通常は②方式なのですが、なぜか「^」を使った場合は③方式のように振舞います。例外対応のような処理が組み込まれているのかもしれません。

    どの方式が正解ということはないと思いますが、現在の Mery の仕様としまして、順方向の検索はマッチした文字列の左端を基準に再検索。逆方向の検索はマッチした文字列の右端を基準に再検索、ということで取りこぼしを最小限に抑えつつ、順方向と逆方向で検索開始位置の基準となる条件を合わせて一貫性をもたせています。

    > とのことなので「こういう振る舞いは、やむ無しなのかなぁ~」と個人的には納得してます。

    結果的にはそうですね ^^;

    ③方式を採用したとしてもこれはこれで取りこぼしが発生するため、①、②方式へのご要望が来そうですし、間を取った④方式は一貫性がなくて分かりづらいですし。

    うーん、難しいです。

     |  Kuro  |  返信
  7. 3回「次を」検索した後、2回「前を」検索すれば、最初にヒットした語に戻る。
    (私のように)それを暗黙のお約束と思い込んでいる人は、3回「次を」検索した後、2回で最初の語に戻らないと、ちょっと驚きます。

    一方、正規表現で「前を」検索するなんて正気ではないとも言えますよね(「普通やらねえだろ!」と言いたくなる気持ちもわかる)。
    こう断じた(かどうかは知りませんが)、Notepad++ は正規表現で検索する時「前を」を disable にしますね。
    これはこれで合理的と言うか、正論と言うか…

    でも、「前を」がないのは困る。
    合理的でなくてもいいから、色々あってもいいから、「ナンチャッテ」でもいいから、救済として「前を」はあって欲しい。
    行ったら帰りたいという帰巣本能を満足させて欲しい。
    「前を」がないと帰れない。

    で、1年前の自分に対する回答
    > 少なくとも、検索において、行きと帰りでヒットする回数(箇所)が違うというのは
    > 如何なものなのかなぁ~と思う次第です。

    正規表現では、そういう事もあります。
    だって正規表現なんだもん。
    適宜便利に使ってね。

    > --------------------------------
    > ③ 順方向は右端、逆方向は左端、検索文字列の強調表示は右端
    > ・Visual Studio Code 1.58.2
    > ・Sublime Text 3.2.2
    > ・サク○エディタさん 2.4.1.2849
    > --------------------------------
    > 最後の「AA」は順方向でも逆方向でもどうがんばってもヒットしません。

    本当だ。
    変ですね。
    気にしたことありませんでした(;^_^A

    Meryさんの場合でも、本来の検索や置換で気になったことはありません。
    私が、Meryさんの正規表現対応で気になるのは、色付けの時だけです。

    > しかしながら、これがご希望の仕様なのかもしれません。

    私が言うのも変ですが、どうなっているのが良いのでしょう??
    正規表現ライブラリの目論見と同じ結果になってくれることが理想かな??

    であるならば、予めファイル全体を先頭から走査して置くとか、検索開始位置近辺を先頭から走査して置くとか…
    とにかく、頭から走査した結果を参照するしかないですよね。
    だって正規表現なんだもん。
    で「前を」の場合は、走査結果を逆順で参照するのかな??

    ま、どうであれ、正規表現に合わせた検索方法でなければ対処できませんよね。
    個人的には、色付けの時だけでもそうなっててくれると嬉しいのですが…
    でも、検索/置換と色付けで正規表現の振る舞いが違うと、混乱するだけで、いいことなさそう
    う~~~ん

    という事で、結論「やむ無し」
    ありがとうございました。

     |  myupon  |  返信
  8. 行きと帰りでヒット数が違うと違和感があるというのはごもっともだと思います。

    特に正規表現を使う場合、逆方向検索は「行きすぎちゃった!」ってときに戻る用途で使うことがほとんどだと思いますので。

    Notepad++ の割り切り方にはニヤリとしてしまいました。

    > 正規表現では、そういう事もあります。
    > だって正規表現なんだもん。
    > 適宜便利に使ってね。

    おっしゃるとおり…、正規表現は知れば知るほど闇が深まりますね ^^;

    > とにかく、頭から走査した結果を参照するしかないですよね。
    > 正規表現ライブラリの目論見と同じ結果になってくれることが理想かな??

    正規表現ライブラリにテキスト全体を渡した結果を参照するというのは難しいと思います。

    というのも、テキストエディターは内部で "行ごとにデータを分割" して保持しているものが多いので、正規表現ライブラリにテキスト全体を渡すとなると、すべての行の結合処理が必要となります。

    これが検索の都度、または検索文字列の強調表示の場合は 1 文字を編集する都度発生するわけですから動作速度に大きく影響します。

    そのため、これを実現するためには…

    ①データを分割せずに保持する
    ②正規表現ライブラリをカスタマイズして分割された行に対応させる
    ③正規表現ライブラリを使わず独自の正規表現検索を実装する

    などの仕組みが必要になると思います。

    "正規表現ライブラリ" を使ったエディターですとそこがボトルネックになるので、例えば複数行のテキストに対して「.*?\n.*?\n」のような正規表現で行をまたいだ検索ができないものも多いかと思います。

    こういったエディターは正規表現ライブラリに行単位でデータを渡しているものだと推測します。

    ちなみに Mery は①の仕組みを採用していますが、それでも "検索文字列の強調表示" は編集の都度、正規表現ライブラリにテキスト全体を渡すとなると非常に遅いので、行単位で行っています ^^;

    > 私が言うのも変ですが、どうなっているのが良いのでしょう??
    > 正規表現ライブラリの目論見と同じ結果になってくれることが理想かな??

    正規表現の逆方向検索については難しいところですね。

    その点では、鬼車 (鬼雲) ライブラリには逆方向検索が実装されています。

    Mery はオリジナルの鬼車 (鬼雲) ライブラリを直接呼び出すことができるので、鬼車に本来備わっている逆方向検索の機能を使うかたちにしています。

    間違っていたら申し訳ないのですが、サク〇エディタさんなどで採用されている鬼車のラッパーライブラリ、bregonig.dll には逆方向検索が実装されていなかったと思います。

    そのため、エディター側で検索開始位置を 1 文字ずつ、または 1 行ずつ前に移動しながら順方向検索することで逆方向検索を実現しているのだと思います。

    検索開始位置を 1 行ずつ前に移動しながら順方向検索 (複数ヒットする場合はカーソル位置に到達するまで繰り返す) すれば、あたかも順方向検索を逆方向にたどっているように見えるというわけです。

    また、これを 1 行ずつではなく "1 文字ずつ" にすれば Mery や Visual Studio 2019 のような動作になるはずです。

    1 行ずつ前に移動しながら順方向検索する方式は、行きと帰りでヒット数が一致するという点では良いのですが、動作速度としては検索を何度も繰り返すことになるので遅くなってしまいますね。

    いずれにしても正規表現ライブラリにテキスト全体を渡せない以上、正規表現ライブラリの目論見とまったく同じ、を実現するのは厳しいかと思います。

    > という事で、結論「やむ無し」

    そうですね。こればっかりは誰もが納得できるような正解はなくて、エディターを開発している人の好み次第ということになりそうな気がしますが、いただいたご意見は今後の開発の参考にさせていただきたいと思います。

     |  Kuro  |  返信
  9. > これが検索の都度、または検索文字列の強調表示の場合は 1 文字を編集する都度発生するわけですから動作速度に大きく影響します。

    ですよね。
    「キビキビ動かない」とか「使いものにならない」とか、言いたい放題言われるのは目に見えてますよね。

    今回「^\p{Blank}+」を引き合いに報告させていただいたのは、実際に下記の様なテキストがあって「^\p{Blank}+」で「次を」で5回検索すると最後の行に至ります。
    で、「前を」で戻ろうとすると、52回「前を」検索しなければならない...orz
    --------------------------------
    (検索開始行)
    <- space * 4
    <- space * 4
    :
    :
    :
    <- space * 16
    <- space * 16
    <- space * 16
    --------------------------------

    戻って行く様子を見ながら…
    そうかぁ~~こういう事もあるのかぁ~まぁ仕方ないなぁ~
    滅多にないし、予め分かってれば避けれるし…
    いつも使わせていただいているんだから、報告だけはしておこうと…

    > その点では、鬼車 (鬼雲) ライブラリには逆方向検索が実装されています。
    > Mery はオリジナルの鬼車 (鬼雲) ライブラリを直接呼び出すことができるので、鬼車に本来備わっている逆方向検索の機能を使うかたちにしています。

    逆方向検索が実装されているって、地味に凄いですね。
    今回「そもそも、正規表現で前を検索するってどういうこと?」「前ってどこよ?」「あれ?私は何を期待してるんだ?」って、ちょっと悩んじゃいました(^_^;

    で、逆方向検索が考慮されているのであれば、「^\p{Blank}+(?=.*$)」とすれば?
    あっ、4回で戻れた!!!

    なるほどねぇ~
    「戻りたいなら、戻れる様に書け!」という事なんですね。
    「正規表現で検索する」とは、こういう事なのかもしれません。

    「やむを得ない。諦めよう」と思っていた事が、分かってみれば「良くできてる!」という感動に変わる。
    いやいや何も変わってないんだから、お前がバカなだけだよって話なんですけどね(笑)
    そっかぁ~

    今まで考えたこともありませんでしたが
    「検索に正規表現が使える」と「正規表現で検索できる」は違うんでしょうね。
    勉強になりました。

    > いずれにしても正規表現ライブラリにテキスト全体を渡せない以上、正規表現ライブラリの目論見とまったく同じ、を実現するのは厳しいかと思います。

    ですね。
    行単位でデータを渡していそうなものでは、ブロックコメントの様なものは、正規表現ではどうにもならないし…
    この理想は、ハードの向上により実現可能になるその日まで楽しみに取って置きます(^_^)

     |  myupon  |  返信
スポンサーリンク