倭マン's BLOG

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

Groovy in Addiction (4) : 正規表現

今回は正規表現について。 『Groovyイン・アクション』Chapter3 を参考にしています(一覧)。

Groovyイン・アクション

Groovyイン・アクション

パターンの定義


まずはパターンの定義方法を。 パターンは単なる文字列なので、前回見た文字列の5つの定義方法のいずれかで、普通に文字列を定義すればいいだけです。 ただし、「スラッシュ構文」を用いて定義するのが通例のようです(というか、スラッシュ構文は正規表現のパターンを定義するためにある)。

スラッシュ構文はほとんどエスケープがいらないので、一度慣れると Java には戻れなくなる恐れがあります:~)

assert "abc" == /abc/;
assert "\\d" == /\d/;

また、スラッシュ構文は GString 対応なので、「ドル構文」も使えます:

def reference = "hello";
assert reference == /$reference/;

「パターンの終わりを意味しないドル記号」はエスケープの必要なし。

assert "\$" == /$/;

パターン・マッチ


パターン・マッチ」とは、ある文字列が指定されたパターンに(全体として)一致するかどうかを評価する操作。

パターン・マッチ演算子 : ==~

パターン・マッチ演算子 (==~, !=~) は、(非)等値演算子 ( ==, != ) と同じように、評価結果は Boolean 値。

twister = 'she sells sea shells at the sea shore of seychelles';

value = (twister ==~ /\w+( \w+)*/);
assert value instanceof java.lang.Boolean;    // 「パターン・マッチ」の評価結果は Boolean

assert twister ==~ /\w+( \w+)*/;    // twister がパターンに一致するかどうか
assert twister !=~ /s.e/;    // twister がパターンに一致しないかどうか

switch-case, grep()

「パターン・マッチ」は、上記の他に

  • switch-case 文
  • Collection に対する grep()

でも使われます。 注意が必要なのは、これらに使用されるのは(パターンを表す)文字列ではなく、java.util.regex.Pattern オブジェクトだということです。 で少し詳しくやりますが、文字列を Pattern オブジェクトに変換するには、単にその文字列の前にパターン演算子 (~)*1 を付けるだけです。

assert (~/..../ ).isCase('bear');   // 通常、こういう使い方はしない

//***** switch-case *****
switch('bear'){
    case '....' :           // ここに一致するのは switch('....') のとき
        assert false; 
        break;
    case ~/..../ :       // 4文字の文字列に一致する
        assert true; 
        break;
    default: 
        assert false;
}

//***** grep() *****
beasts = ['bear', 'wolf', 'tiger', 'regex'];
assert beasts.grep(~/..../) == ['bear', 'wolf'];


パターン検索」とは、ある文字列に対して指定されたパターンに一致する部分を検索する操作。 検索結果の文字列に対して置換などの操作も行うことが可能。

パターン検索 : =~

パターン検索を行うための「パターン検索演算子 (=~)」の基本的な使い方。 Matcher オブジェクトが生成されているのですが、そんなことより、文の形を覚えてしまった方が良いかと。 Matcher クラスの使い方については後述

myFairStringy = 'The rain in Spain stays mainly in the plain!';
rhyme = /\b\w*ain\b/;

//***** パターン検索 (1) *****
found = '';
(myFairStringy =~ rhyme).each{ match ->
    found += match + ' ';
}
assert found == 'rain Spain plain ';

//***** パターン検索 (2) : グループを使う *****
('xy' =~ /(.)(.)/).each{ all, x, y ->
  assert all == 'xy';
  assert x == 'x';
  assert y == 'y';
}


Groovy では、正規表現関連のメソッドがいくつか String クラスに定義されてます(Groovy JDK 参照)。 ここでは、その中から Pattern クラスに関係しないメソッドを見ていきます(Pattern クラス関連のメソッドは後述):

メソッド 戻り値
eachMatch(String, Closure) String
splitEachLine(String, Closure) Object
find(String)
find(String, Closure)
String
findAll(String)
findAll(String, Closure)
List
replaceAll(String, Closure) String

サンプル・コードをいくらか:

myFairStringy = 'The rain in Spain stays mainly in the plain!';
rhyme = /\b\w*ain\b/;

//***** eachMatch(String, Closure) *****
found = '';
myFairStringy.eachMatch(rhyme){ match ->
    found += match + ' ';
}
assert found == 'rain Spain plain ';

//***** split(String) *****
words = myFairStringy.split(rhyme);
assert words == ['The ', ' in ', ' stays mainly in the ', '!'];    // 返り値は String[]

//***** find(String), find(String, Closure) *****
xain = myFairStringy.find(rhyme);
assert xain == 'rain';

xain = myFairStringy.find(rhyme){
    it - 'ain' + '___';
}
assert xain == 'r___';

//***** findAll(String), findAll(String, Closure) *****
xains = myFairStringy.findAll(rhyme);
assert xains == ['rain', 'Spain', 'plain'];

xains = myFairStringy.findAll(rhyme){
    it - 'ain' + '___';
}
assert xains == ['r___', 'Sp___', 'pl___'];

//***** replaceAll(String), replaceAll(String, Closure) *****
result = myFairStringy.replaceAll(rhyme, '???');
assert result == 'The ??? in ??? stays mainly in the ???!';

result = myFairStringy.replaceAll(rhyme){
    it - 'ain' + '___';
}
assert result == 'The r___ in Sp___ stays mainly in the pl___!';

Pattern クラスと Matcher クラス


ここでは、Pattern クラスと Matcher クラスの Groovy 拡張を見ていきます。

Pattern クラス

String オブジェクトに対してパターン演算子 (~) を適用すると、Pattern オブジェクトを取得することができます。 Java と同様に、パターンを使い回すときはこの Pattern オブジェクトを保持して使用すると効率的です。

twister = 'she sells sea shells at the sea shore of seychelles';

//***** パターン・コンパイル : ~ / bitwiseNegate() *****
pattern = ~/\w+( \w+)*/;
assert pattern instanceof java.util.regex.Pattern;

//***** パターン・マッチ : String#matches(Pattern) *****
assert twister.matches(pattern);

//***** パターン検索 : String#replaceAll(Pattern, String) *****
pattern = ~/\w+/;
wordsByX = twister.replaceAll(pattern, 'x');
assert wordsByX == 'x x x x x x x x x x';

String に定義されているメソッドで、Pattern オブジェクトを使用するものを列挙しておきます(Groovy JDK より):

メソッド 戻り値
matches(Pattern) boolean
eachMatch(Pattern, Closure) String
splitEachLine(Pattern, Closure) Object
find(Pattern)
find(Pattern, Closure)
String
findAll(Pattern)
findAll(Pattern, Closure)
List
replaceAll(Pattern, String)
replaceAll(Pattern, Closure)
String
replaceFirst(Pattern, String) String

Matcher クラス

Java では、Matcher クラスはかなり使いにくかったイメージがありますが(個人の感想です)、Groovy では getAt() メソッドによって配列っぽくアクセスできるので、だいぶ使いやすくなってる感が。

matcher = ('a b c' =~ /\S/);  // 括弧なくても OK
assert matcher instanceof java.util.regex.Matcher;

//***** asBoolean() *****
assert matcher;

//***** getAt(), getCount() *****
assert matcher[0] instanceof String    // グループがないときは String
assert matcher[1] == 'b';
assert matcher[0] == 'a';   // アクセスの順番は任意
assert matcher[2] == 'c';

assert matcher[1..2] == ['b' , 'c'];
assert matcher.count == 3;

//***** グループの使用 *****
matcher = 'a:1 b:2 c:3' =~ /(\S+):(\S+)/;    // () を使ってグループを2つ定義

assert matcher.hasGroup();
assert matcher[0] instanceof List;    // グループがあるときは List<String>
assert matcher[0] == ['a:1', 'a', '1'];
assert matcher[1] == ['b:2', 'b', '2'];
assert matcher[2] == ['c:3', 'c', '3'];

*1:「パターン・コンパイル演算子」の方が良いような気が・・・