Y&S SoftWareのホームページ
使いかってのいいソフトウェアを開発します

先読みの正規表現について

(?ms) は Groups(n) には含まれません。
これは 正規表現の「フラグ設定」 であって、キャプチャ対象(グループ)ではないからです。

(?ms) の意味
これは正規表現の モード修飾子(オプション) で、以下の2つを有効にします

フラグ 意味
m (multi-line) ^ や $ を行単位で働かせる(各行の先頭/末尾)
s (single-line) . が改行にもマッチする(通常は改行にマッチしない)

つまり、(?ms) は解析のモードを変えるだけで、Match.Groups(n) の対象には一切影響を与えません。

Dim pattern = "(?ms)^(\d+)\s*\r?\n..."
上記のように先頭に (?ms) を置いても、


Dim m As Match = Regex.Match(s, pattern)
Console.WriteLine(m.Groups(0).Value) '→ 全体マッチ
Console.WriteLine(m.Groups(1).Value) '→ 番号部分
となり、(?ms) が Groups(0) や Groups(1) に入ることは絶対にありません。

補足:モード修飾子をあとから与える方法(VB.NET)

RegexOptions options = RegexOptions.Multiline Or RegexOptions.Singleline
Regex.Match(text, pattern, options)

これと (?ms) の効果は同じです。どちらの書き方でも構いません。


(?=\r?\n\r?\n|\z) のような 先読み(lookahead) は、Groups(n) には含まれません。


(?=...) という構文は、
「この位置のあとに◯◯が続いていればマッチとみなすが、実際にはその◯◯を消費しない(取り込まない)」
という正規表現の機能です。

マッチの判定には使う
結果としてのキャプチャ(Groups)には入らない


Dim pattern As String = "(\d+)(?=\s+abc)"
Dim input As String = "123 abc"

Dim match = Regex.Match(input, pattern)
Console.WriteLine(match.Groups(0).Value) ' → "123"
Console.WriteLine(match.Groups(1).Value) ' → "123"
この例で、(?=\s+abc) の部分は:

" abc" という文字列が 後ろにあるかどうかをチェック

でも実際に " abc" は 取り込まれない

Groups にも入らない

(?=...) の先読み部分は「次のマッチのパートの一部として使う」ことができます。
ただし、先読み自体は取り込まれないので、それに続くマッチで実際にその文字列を取り込む必要があります。


Dim pattern As String = "(?=abc)abc"
Dim input As String = "abc123"
この場合:

(?=abc) で "abc" がそこにあることを確認するだけ(取り込まない)

続く abc が 実際に取り込まれる部分


たとえば、次のように「ある語で始まり、続く特定のパターンを取り込む」ことができます:


Dim pattern As String = "(?=word)(word\s+\w+)"
Dim input As String = "word apple banana"
この場合:

(?=word) → word がこの位置にあることを確認(ゼロ幅、取り込まない)

(word\s+\w+) → word とその後の1単語を取り込む

Groups(0) → "word apple" がマッチ

Groups(1) → "word apple"

次のマッチの開始位置に影響
正規表現のエンジンは、先読みによってマッチの終了位置をずらすことなく条件チェックだけを行い、その後の正規表現で本体を取り込むという動作をします。

注意点
(?=...) だけでは マッチの一部として取り込めない

取り込むには、その文字列を後続のパターンで明示的に指定する必要がある




Dim pattern As String = "(?=123)456"
Dim input As String = "123456"
では、マッチしません。


(?=123) は先読み
123 がその位置にあることを確認しますが、何も取り込まない(ゼロ幅)

つまり、現在の位置に 123 が あることを確認するだけ

456 を次にマッチさせようとするが…
先読みの位置の直後(つまり "1" の位置)から 456 を探すことになります。
しかし "123456" の "1" の位置からは "456" ではなく "123" が始まっているため…
この正規表現は マッチしません。

正しい例:順番を変える
Dim pattern As String = "123(?=456)"
Dim input As String = "123456"
この場合:

123 を取り込む(マッチ)

(?=456) はゼロ幅で 456 の存在を確認

Groups(0) → "123"

(?=123)123456 なら?

Dim pattern As String = "(?=123)123456"
Dim input As String = "123456"
この場合は:

(?=123) で 123 の存在を確認(ゼロ幅)

123456 が続けてマッチ
Groups(0) = "123456"

(?=123)456 のように書いても、先読みの「確認位置」には 456 は来ないのでマッチしない。
必ず、先読みの後ろの部分が、その位置からマッチ可能なものでなければならない。




Dim pattern As String = "(?=123)456"
Dim input As String = "123456"
先読みとは?
(?=123) は「今からの位置に 123 があるか?」を確認するだけです。

しかし 確認後も文字位置は進まず、そのままの位置で次の 456 にマッチを試みます。
入力 "123456" の 最初の位置(文字 "1" の位置) で、

(?=123) → 成功("123" がそこにある)

しかし、その同じ位置(まだ "1" の位置)から、

"456" にマッチしようとする → "123456" の先頭からは "456" ではなく "123" なので マッチ失敗

確かに "456" は文字列の中にある
ただし、正規表現エンジンは (?=123) が成功した位置(ここでは "1" の位置)から 456 を直接マッチさせようとします。
でも "456" は "4" の位置から始まります。つまり マッチ位置がズレてるので、結果として失敗します。

だから、位置Aでは 456 は始まってないので失敗します。

マッチさせたい場合の正しい書き方:
次のようにすれば 456 にマッチできます:

Dim pattern As String = "123(?=456)"
これなら123を取り込んで
その直後の位置に 456 があるかを確認する(ゼロ幅)
この場合、Groups(0) → "123"


Dim pattern As String = "123456"
これなら当然 "123456" にマッチします。


(?=xxx)yyyyyy のように文字列の前方に先読みを置いたら、文字列yyyyyyはxxxで始まらなければマッチしない。と同意
もう少し正確に言うと:

(?=xxx)yyyyyy という正規表現は、
現在の位置から xxx が見える場合にだけ yyyyyy にマッチを試みる。

だからこう言えます:
yyyyyy がマッチするには、xxx がその位置から始まっていなければならない。

これは「yyyyyy の直前に xxx があることが条件」ではありません。

「yyyyyy をマッチさせたい位置から見て xxx が見えなければならない」という意味です。


結論
(?=xxx)yyyyyy の形では、文字列 yyyyyy がその位置から xxx で始まっていなければマッチしない
→ yyyyyy の先頭が xxx に一致する必要がある。



Dim pattern As String = ".+(123)(?=456)"
Dim input As String = "000123456"

Groups(0) 000123
Groups(1) 123


Dim pattern As String = ".+(123)(?=456)"
Dim input As String = "000123456"

解釈:
.+ …… 1文字以上の任意の文字(貪欲)

(123) …… グループ1:文字列 "123"

(?=456) …… 先読み:直後に "456" があることを条件にする(マッチには含まれない)

実際のマッチ結果
入力文字列:"000123456"

正規表現エンジンは、次のように処理します:

. + が貪欲に動作して、可能な限り多くの文字を取り込もうとする。

ただし、後続の (123)(?=456) にマッチするように調整する必要がある。

結果として、"000123" の位置で 123 にマッチし、その直後に 456 があるので条件を満たす。

マッチ結果
グループ 値 備考
Groups(0) 000123 マッチ全体(先読みは含まれない)
Groups(1) 123 明示的なキャプチャグループ