倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Groovy でグループを使った正規表現マッチを試す

なんとなく今まで正規表現を回避してたんだけど、定型文字列を String のメソッドだけで解析するの面倒すぎるので、Groovy パワーであれこれ試してみました。 Groovy 使うと、グループを使った正規表現でマッチしたグループの内容を List の要素アクセスのように取得できるので楽そう。

正規表現と Matcher の基本
まずは簡単なサンプルで動作を確認してみましょう。 対象とする文字列は

def method0(String s)

のような Groovy のメソッド宣言(ただしメソッド本体無し)。 この文字列に対して正規表現でメソッド名と引数の型、名前を取得します。 正規表現をあまり複雑にしないために、空白文字の部分は1つの空白のみ許すことにします(その他、余計な空白はないとします)。 あと、メソッド名などの最初に数字が来てもマッチします。

では実際にサンプルを書いてみましょう。 Groovy では正規表現を表す文字列は /.../ で書くとエスケープが少なく済んで楽ですね。

// 対象文字列
def s0 = 'def method0(String s)'

// 正規表現
def ARG = /(\w+) (\w+)/    // 引数部分の正規表現。 グループ2つ
def regex0 = /def (\w+)\($ARG\)/

// マッチの実行
def matcher0 = s0 =~ regex0    // =~ は正規表現検索演算子

// Matcher のあれこれ
assert matcher0 instanceof java.util.regex.Matcher

assert matcher0.hasGroup()
assert matcher0.groupCount() == 3  // 正規表現に含まれるグループの数

assert matcher0.size() == 1    // マッチした部分の数を取得
assert matcher0.size() == matcher0.count  // size() と count は同じ

assert matcher0[0] instanceof List    // Matcer から要素風に取得したオブジェクトは List
assert matcher0[0] == ['def method0(String s)', 'method0', 'String', 's']
    // 1つ目の要素はマッチした文字列全体、後の要素は各グループにマッチした文字列

GDK の機能によって Matcher に matcher0[0] のような方法(getAt メソッド)でマッチ結果を取得することができます。 その取得結果は List オブジェクトで、その要素は

  • 最初の要素はマッチした文字列全体
  • それ以降の要素はグループにマッチした文字列

となっています。 ここでの正規表現は

def (\w+)\((\w+) (\w+)\)

ですが*1、これを「def (A)\((B) (C)\)」としたとき、matcher0[0] で返される List には

  • 0. def (A)\((B) (C)\)
  • 1. (A)
  • 2. (B)
  • 3. (C)

にマッチする文字列(グループ化の丸括弧 () は含まれません)が返されます。 グループが入れ子になっていないと簡単。 なってても別に難しくないけど。 で、今の場合には以下の文字列が割り当てられます:

  • 0. def (A)\((B) (C)\) ・・・ def method0(String s)
  • 1. (A) ・・・ method0
  • 2. (B) ・・・ String
  • 3. (C) ・・・ s

最初がマッチした文字列全体だって以外は特に注意が必要なところはないですかね。

複数のマッチがある場合
上記では「matcher0[0]」でマッチ結果の List を取得できることを見ましたが、例えばメソッド名を取得したい場合には「matcher0[0][1]」のように2重配列であるかのようにアクセスする必要があります。 先ほどの例ではこの1つ目の [0] の必要性がよく分からないので、これが意味のある場合のサンプルを見ておきましょう。 正規表現は先ほどと同じものを使っています:

// 対象文字列
def s1 = '''
  def method1(String s)
  def method2(int i)
  def method3(double d)
'''

// マッチの実行(正規表現はさっきといっしょ)
def matcher1 = s1 =~ regex0

// マッチ結果
assert matcher1.size() == 3
assert matcher1[0] == ['def method1(String s)', 'method1', 'String', 's']
assert matcher1[1] == ['def method2(int i)',    'method2', 'int',    'i']
assert matcher1[2] == ['def method3(double d)', 'method3', 'double', 'd']

Groovy の正規表現検索演算子「=~」は対象文字列にマッチした部分が複数あってもきちんと抜き出してくれるんですね。 ちなみに正規表現マッチ演算子「==~」は対象文字列全体が正規表現にマッチしているかどうかを検証します。

Matcher オブジェクトの各要素(matcher1[0] などで返される List オブジェクト)がどのような要素からなるかは、先ほどのサンプルと同じです。

グループに数量子をつけると・・・
さて、対象文字列のメソッド宣言が引数1つというのは融通が利かないので、複数の引数でもいいように拡張してみましょう。 引数がない場合もできますが、正規表現が複雑になるのでここでは割愛。 で、2つ目以降の引数は数量子 * を使って書きましょう:

// 対象文字列
def s2 = '''
  def method4(String s, int i, double d)
  def method5(long l)
'''

// 正規表現(ARG は上記のものと同じ)
def regex2 = /def (\w+)\($ARG(, $ARG)*\)/

// マッチの実行
def matcher2 = s2 =~ regex2

// マッチ結果
assert matcher2[0] == ['def method4(String s, int i, double d)', 'method4',
                       'String', 's', ', double d', 'double', 'd']
assert matcher2[1] == ['def method5(long l)', 'method5', 'long', 'l', null, null, null]

さて、マッチ結果を見てみると正規表現初心者には少々腑に落ちないところがいくつか。

  • matcher2[0] に引数「int i」に関連する部分が抜け落ちている
  • 数量子「*」を付けるための丸括弧 () がグループにされて「, double d」が結果に入れられている(まぁ、仕方ない?)
  • matcher2[1] にやたら null がある

最初、正規表現の書き方がおかしいのかと悩んでたんだけど、どうもこれが(Java 正規表現の)通常の動作のようで。 これがどういうことかと言うと、今の正規表現

def (\w+)\((\w+) (\w+)(, (\w+) (\w+))*\)

を「def (A)\((B) (C)(, (D) (E))*\)」とすると、定義されるグループは

  • 0. def (A)\((B) (C)(, (D) (E))*\)
  • 1. (A)
  • 2. (B)
  • 3. (C)
  • 4. (, (D) (E))
  • 5. (D)
  • 6. (E)

となります。 ちょっと直感とのズレがあるのはグループ4で、数量子「*」が付いていても(というか数量子が付いているかどうかに関係なく)グループは1つになり、マッチする文字列は1つしか返されないということです。 返される文字列はマッチした最後の文字列となります。 もう少し具体的に見てみると(0番目のマッチした文字列全体は長くなるので省略して)

(A) (B) (C) (, (D) (E)) (D) (E)
1行目1つ目 method4 String s , int i int i
1行目2つ目 , double d double d
1行目の結果 method4 String s , double d double d
2行目 method5 long l
2行目の結果 method5 long l null null null

のように int 関連の引数が double のもので上書きされます。 グループはどんな数量子が付いても1つのグループってことですな。 この動作ってどう使ったらいいのかよくわからん。 そもそも groupCount() メソッド (Java) とか hasGroup() メソッド (GDK) とかは Pattern オブジェクトに定義した方がいいんじゃないか?って気もするし。

引数の個数が分からないなら別途処理するしか・・・
では、引数の部分が複数個ある場合にはどうしたらいいのかと考えると、引数部分だけ抜き出して別途処理するしかなさそうですな。 ARG にあたる部分のグループ化を解いて、引数部分を1つのグループにしてマッチさせましょう。 数量子を付けるために丸括弧 () を付けざるを得ないところがありますが、「(?:」によってグループと認識されないようにしておきましょう*2

//対象文字列
def s4 = 'def method6(String s, int i, double d)'

// 正規表現
def ARG = /\w+ \w+/    // 最初のサンプルと変わってます
def ARGS = /$ARG(?:, $ARG)*/    // (?: でグループにしない括弧
def regex3 = /def (\w+)\(($ARGS)\)/

// マッチの実行
def matcher3 = s3 =~ regex3
assert matcher3[0] == ['def method6(String s, int i, double d)', 'method6',
                       'String s, int i, double d']
assert matcher3[0][2].split(/, /) == ['String s', 'int i', 'double d']
    // String#split(regex) で引数部分を分離

引数部分全体(部分なのに全体w)を取得できたら、String#split() メソッドでさらに分離すれば1つ1つの引数部分が得られます。 この split メソッドの引数は正規表現として扱われるので、/,\s+/ などを渡せば複数の空白文字で区切られている場合でもキチンと処理してくれます。 めでたしめでたし。

上記と同じことを Java だけでやろうと思ったんだけど、思いの外大変そう。

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

詳説 正規表現 第3版

詳説 正規表現 第3版

  • 作者: Jeffrey E.F. Friedl,株式会社ロングテール,長尾高弘
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/04/26
  • メディア: 大型本
  • 購入: 24人 クリック: 754回
  • この商品を含むブログ (85件) を見る
正規表現クックブック

正規表現クックブック

*1:エスケープされた「\(」と「\)」は文字列としての丸括弧でグループ化とは無関係。

*2:グループと認識されないようにする丸括弧にもいくつか種類があるようだけど、イマイチ違いがわからん。