2011年12月23日金曜日

◆正規表現の基礎<グループ化構成体>

はっきり言ってちょっと手に余ってきた。(笑)
あくまでも自分なりの解釈をまとめたメモと言う事で・・・。

<グループ化構成体>

(subexpression)

正規表現の部分式を表し、通常は入力文字列の部分文字列をキャプチャする。
括弧で囲まれた部分を、$matches[1]から順に、括弧の数だけ順にキャプチャしてくれる。マッチ全体は$matches[0]となる。

PS>"PowerShell" -match '(S.e)ll';$matches[0,1]
True
Shell
She

‘S.ell’は’Shell’にマッチするので$matches[0]は’Shell’
S.e の部分は括弧で囲まれているのでその部分が$matches[1]となっている。

括弧を2つ使うとこんな感じ。

PS>"PowerShell" -match '(o.e)r(S.e)ll';$matches
True

Name                           Value
----                           -----
2                              She
1                              owe
0                              owerShell

括弧は入れ子にしても使える。

PS>"PowerShell" -match '(o.er(S.e))ll';$matches
True

Name                           Value
----                           -----
2                              She
1                              owerShe
0                              owerShell

 

(?<name>subexpression)

キャプチャした部分式に名前を指定できる。

PS>"PowerShell" -match '(?<first>o.e)r(?<second>S.e)ll';$matches
True

Name                           Value
----                           -----
second                         She
first                          owe
0                              owerShell


PS>$matches.second
She

 

(?<name1-name2>subexpression)

グループ定義を均等化する。

果たして何のことやら。\(ー_ー;)/

どうやらこの機能は.Net独自の機能っぽい。
そのせいかまともに使い方を説明している資料が全く見つけられない。
MSの資料は何を言っているのか全く理解出来ないし・・・。
まぁ、知らなくて困ることはなさそうだ。
一応MSの解説をそのまま載せておく。
グループ化構成体

(?< name1 - name2 > subexpression)

グループ定義を均等化します。既に定義されていたグループ name2 の定義を削除し、既に定義されていた name2 グループと現在のグループの間隔をグループ name1 に格納します。グループ name2 が定義されていない場合、一致はバックトラックされます。name2 の最後の定義を削除すると、name2 の以前の定義がわかるため、この構成体によって、かっこなど入れ子になった構成体を追跡するカウンタとして name2 グループのキャプチャのスタックを使用できます。この構成体では、name1 は省略できます。たとえば (?'name1-name2') のように、山かっこの代わりに一重引用符を使用することもできます。

詳細については、このトピックの「」を参照してください

 

(?:subexpression)

キャプチャを抑止する。

PS>"abcd" -match '((b|a)c)d';$matches
True

Name                           Value
----                           -----
2                              b
1                              bc
0                              bcd


PS>"abcd" -match '((?:b|a)c)d';$matches
True

Name                           Value
----                           -----
1                              bc
0                              bcd

キャプチャしたくないだけであれば括弧を付けなければ良いだけだと思うが、おそらく他の意味での括弧を使いたい場合なのだろう。(ちなみに|は、またはの意)

(?imnsx-imnsx:subexpression)

(?imnsx-imnsx)subexpressionと書いても良いのかな・・・。

指定したオプションを部分式に適用または無効にする。
オプションは以下のとおり。

RegexOption のメンバ インライン文字 説明

None

なし

オプションを設定しないことを指定します。

IgnoreCase

i

大文字小文字を区別せずに照合を行うことを指定します。

Multiline

m

複数行モードを指定します。^ および $ の意味を変更し、文字列の先頭および末尾ではなく、それぞれ、行の先頭および末尾と一致するようにします。

ExplicitCapture

n

(?<name>…) の形式で明示的に名前付けまたは番号付けしたグループだけを有効なキャプチャにするように指定します。これによって、(?:…) という扱いにくい構文を使用せずに、キャプチャしないグループ化を行うときに、かっこを使用できます。

Singleline

s

単一行モードを指定します。ピリオド文字 (.) の意味を変更し、\n 以外のすべての文字ではなく、すべての文字と一致するようにします。

IgnorePatternWhitespace

x

エスケープされない空白をパターンから除外し、シャープ記号 (#) の後ろのコメントを有効にするように指定します。エスケープされる空白文字の一覧については、「文字のエスケープ」を参照してください。文字クラスからは空白が削除されません。

<IgnoreCase>

matchはそもそも大文字小文字を区別しないのでここではその効果を見るためにcmatchを使っている。

PS>"abc" -cmatch 'AB'
False
PS>"abc" -cmatch '(?i:AB)'
True

<Multiline>

^は入力文字列の先頭を表すので2行目以降にはマッチしない。
Multilineオプションを付けることで入力文字列の先頭ではなく行頭の意味に変化する。

PS>"ab`ncd"
ab
cd
PS>"ab`ncd" -match 'ab'
True
PS>"ab`ncd" -match 'cd'
True
PS>"ab`ncd" -match '^ab'
True
PS>"ab`ncd" -match '^cd'
False
PS>"ab`ncd" -match '(?m:^cd)'
True

<ExplicitCapture>

名前付したグループだけをキャプチャする。

PS>"bcd" -match '(a|b)cd';$matches
True

Name                           Value
----                           -----
1                              b
0                              bcd


PS>"bcd" -match '(?n:a|b)cd';$matches
True

Name                           Value
----                           -----
0                              bcd

match演算子では(?:)との違いは見出しづらいように思う。
RegExクラスの様に通常のオプションとインラインでのオプションの両方が使える場合に意味が出てくるのでは無いだろうか・・・・。

<Singleline>

通常ピリオドは改行以外のすべての文字にマッチするが、Singlelineオプションをつけると改行にもマッチする。

PS>"ab`ncd" -match 'a.*d'
False
PS>"ab`ncd" -match '(?s:a.*d)'
True

<IgnorePatternWhitespace>

パターンの中のスペースを無視する。

PS>"abcde" -match "ab cd"
False
PS>"abcde" -match "(?x:ab cd)"
True

加えて、#以降行末までをコメントとして扱ってくれるようになる。
とあるが、これがちょっと分かりづらく以下のような指定ではエラーになる。

PS>"abcd" -match '(?x:ab cd-#コメント)'
演算子 '-match ' の引数 解析中 "(?x:ab cd-#コメント)" - ) が足りません。 が正しくありません。
発生場所 行:1 文字:14
+ "abcd" -match <<<<  '(?x:ab cd-#コメント)'
    + CategoryInfo          : InvalidOperation: (:) []、RuntimeException
    + FullyQualifiedErrorId : BadOperatorArgument

試行錯誤した結果、どうやら行末が必要なようです。
こんな感じ。

PS>"abcd" -match '(?x:ab cd#コメント
>> )'
>>
True
PS>"abcde" -match '(?x:ab cd#コメント
>> e)'
>>
True

実際には、ヒア文字列とかの行末にコメントを付けるときなどに使うのかと思います。

<マイナス付きオプション>

これまでのオプションはそれぞれマイナスが付いた指定もできて、打ち消しの意味になります。
RegExオプションとインラインで相反する指定をする時に有効なものかと思いますのでRegExを使ったサンプルを1つ。

インラインでオプションを打ち消すサンプル
  1. $opt = [System.Text.RegularExpressions.RegexOptions]
  2. $opt_x = $opt::IgnorePatternWhitespace
  3. $opt_i = $opt::IgnoreCase
  4.  
  5. "(1)"
  6. [RegEx]::Match("abcde","aBc De",$opt_x -bor $opt_i)
  7. "(2)"
  8. [RegEx]::Match("abcde","(?-i-x:aBc De)",$opt_x -bor $opt_i)
  9. "(3)"
  10. [RegEx]::Match("abcde","(?-i-x:aBcDe)",$opt_x -bor $opt_i)
  11. "(4)"
  12. [RegEx]::Match("abcde","(?-i-x:abcde)",$opt_x -bor $opt_i)

結果はこんな感じ。
image

(1)は「スペースを無視する」と「大文字小文字を区別しない」オプションが付いているのでマッチ。
(2)と(3)はそれをインラインで打ち消しているのでアンマッチ。
結局スペースを削除し、大文字小文字をあわせて、(4)でマッチとなっている。

(?=subexpression)

ゼロ幅の正の先読みアサーション。
なんのこっちゃ(笑)

自分なりに解釈すると、このsubexpressionはマッチ条件には使われるがキャプチャはされないものって感じかなぁ。先読みってのは右側ってことかな。

PS>"12345-6789" -match '(?=...-)..';$matches
True

Name                           Value
----                           -----
0                              34

(?=...-)は、345- とマッチするので3が実際のパターンの判定開始位置。
単純に任意の2文字をパターン指定しているので3から2文字、即ち34がキャプチャされている。

PS>"123a456b789c" -match  '\d{3}(?=c)';$matches
True

Name                           Value
----                           -----
0                              789

こちらは、後ろに「c」の文字が続く数字3桁という事で789がキャプチャされている。

(?!subexpression)

ゼロ幅の負の先読みアサーション。
上記の否定形。

PS>"123c456b789a" -match  '\d{3}(?!c)';$matches
True

Name                           Value
----                           -----
0                              456

後ろに「c」の続かない数字3桁という事で456がキャプチャされている。

(?<=subexpression)

ゼロ幅の負の先読みアサーション。
今度は負なので左側。

PS>"public static int  private static string" -match '(?<=private )static.*';$matches
True

Name                           Value
----                           -----
0                              static string

頭に「private」が付いた「static」という事で「string」のほうがマッチ。

(?<!subexpression)

上記の否定形。

(?>subexpression)

MSDNによると、

(?> subexpression)

非バックトラッキング部分式です。"最長" 部分式とも呼ばれます。部分式は 1 回完全に一致し、バックトラッキングの断片には参加しません。つまり、部分式は、その部分式単独で一致する文字列にだけ一致します。

既定では、一致しなかった場合に、バックトラッキングによって他の一致が検索されます。バックトラッキングが成功しないことがわかっている場合は、非バックトラッキング部分式を使用して不要な検索を回避し、パフォーマンスを高めることができます。

果たして何のことやら。\(ー_ー;)/

っとパスしようかと思ったのだが、なんとか自分なりに適当に纏めてみた。

基本的に量指定子「*」、「+」のように長さの決まっていないものについては最長(一番広い範囲)でのマッチが採用される。
以下の例ではpencilではなくpenとマッチしているのが判る。

PS>"This is not a pencil. This is a pen." -match '(This.+pen)';$matches
True

Name                           Value
----                           -----
1                              This is not a pencil. This is a pen
0                              This is not a pencil. This is a pen

次に、パターン文字列の後ろにcilを追加してみる。

PS>"This is not a pencil. This is a pen." -match '(This.+pen)cil';$matches
True

Name                           Value
----                           -----
1                              This is not a pen
0                              This is not a pencil

今度は当然「pencil」にマッチするわけであるが、部分式は前回と同じなのでまずは、「pen」とマッチする。
しかし、「cil」が見つからないのでそこから範囲を狭めてというか左側に向かって次のpenを探すという段階を踏むことになる。(この動作をバックトラッキングと呼ぶのだと思う)

さて、ここで以下のようなスクリプトがあったとすると、

正規表現でフィルターするサンプル
  1. $arr = ,"This is not a pen.  This is a ballpint pen"
  2. $arr += "This is not a pen. This is a pencil"
  3. $arr += "This is not a pen. This is a highlighter pen"
  4. $arr += "This is not a pen. This is a pencil"
  5. $arr += "This is not a pen. This is a highlighter pen"
  6.  
  7. $arr | ?{$_ -match '(This.+pen)cil'}

これは明らかに以下のように書いても同じ結果が得られる。

  1. $arr | ?{$_ -match '(?>This.+pen)cil'}

このように明らかにバックトラッキングが不要な局面では非バックトラッキング指定をしたほうが性能的に有利ということなのだと思う。

対象の文字列を大きくして性能を比較してみたのが以下のサンプルである。

バックトラッキングあり
  1. $arr = ,"This is not a pen.  This is a ballpint pen"
  2. $arr += "This is not a pen. This is a pencil"
  3. $arr += "This is not a pen. This is a highlighter pen"
  4. $arr += "This is not a pen. This is a pencil"
  5. $arr += "This is not a pen." + ("hoge" * 10000) +
  6.     " This is a highlighter pen"
  7.  
  8. $arrLarge = $arr * 1000
  9. Measure-Command{
  10.     $arrLarge | ?{$_ -match '(This.+pen)cil'} | Out-Null
  11. }

image
バックトラッキング無し
  1. $arr = ,"This is not a pen.  This is a ballpint pen"
  2. $arr += "This is not a pen. This is a pencil"
  3. $arr += "This is not a pen. This is a highlighter pen"
  4. $arr += "This is not a pen. This is a pencil"
  5. $arr += "This is not a pen." + ("hoge" * 10000) +
  6.     " This is a highlighter pen"
  7.  
  8. $arrLarge = $arr * 1000
  9. Measure-Command{
  10.     $arrLarge | ?{$_ -match '(?>This.+pen)cil'} | Out-Null
  11. }

image

0 件のコメント:

コメントを投稿