前の記事で予告した通り、今度はNSRegularExpressionの話。

正規表現でマッチした部分文字列を取得する

まずNSRegularExpressionオブジェクトを作って、それのメソッドにNSStringのオブジェクトを渡す、という形で使う。まぁ説明するよりコード見た方が早い。

NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
NSError *error   = nil;
NSRegularExpression *regexp =
  [NSRegularExpression regularExpressionWithPattern:@"「そんな(.+)で大丈夫か?」「(.+)」"
                                            options:0
                                              error:&error];
if (error != nil) {
  NSLog(@"%@", error);
} else {
  NSTextCheckingResult *match =
    [regexp firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
  NSLog(@"%d", match.numberOfRanges); // 3のはず
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]); // マッチした文字列全部
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]); // "正規表現"
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]); // "大丈夫だ、問題ない"
}

地味にややこしい。Rubyで書いたらこんなんで済むのに。

# coding: utf-8
if "「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」" =~ /「そんな(.+)で大丈夫か?」「(.+)」/
  puts $&
  puts $1
  puts $2
end

まぁRubyやPerlと比べるのは(少なくとも文字列操作や正規表現に関して言えば)フェアじゃないですけど!とにかくこれで正規表現で部分文字列を探せるようになりました、と。

ちなみに、-firstMatchInString:options:range:というメソッド名で分かると思うけど、これは最初にマッチした箇所しか取ってこない。マッチした箇所全て欲しければ、-matchesInString:options:range:を使えば、NSTextCheckingResultが入ったNSArrayが返ってくる。別に返り値はずっと取っておく必要はなくて、単にマッチする毎になんか処理をしたいんだよ、ってときは、-enumerateMatchesInString:options:range:usingBlock:が使える。さっきの-firstMatchInString:options:range:を書き換えるとこんな感じになる。

NSRegularExpressionOptions options = 0;
NSRange range = NSMakeRange(0, string.length);
id block = ^(NSTextCheckingResult *match, NSMatchingFlags flag, BOOL *stop){
  NSLog(@"%d", match.numberOfRanges);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]);
};
[regexp enumerateMatchesInString:string options:options range:range usingBlock:block];

Blocksの使い方は以前書いた記事とか読んでもらえると分かるかもしれない。あの記事を書いた時点ではiOS4.0を想定してアプリ作れなかったので実質まともに使えるのがSnow Leopardだけだったのだけど、今ならiPhone/iPadともに4系前提で作れるし、そもそもNSRegularExpression自体がiOS4.0以降にしか無いのでNSRegularExpressionを使える環境ならBlockも使えるので問題ない。

置換する

正規表現が使えるなら一番やりたいのは置換だろう、ということでもちろん置換もできる。-stringByReplacingMatchesInString:options:range:withTemplate:というのがそれ。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"$0\n→($2砕け散る)\n→「神は言っている、ここで死ぬ運命ではないと」\n→「$1」「一番いいのを頼む」";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"「(そんな(.+)で大丈夫か?)」「.+」"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

最初は話を聞かなかったあいつもちゃんと一番いいのを頼んできたので、今度は大丈夫だろう。しれっと$0とか$1とか使ってるけど、もちろんちゃんと置換文字列の中でキャプチャした部分文字列を参照したりできてるはず。

ただ、-stringByReplacingMatchesInString:options:range:withTemplate:は文字列そのものを置換してるわけじゃなくて、引数のNSStringのオブジェクトをcopyして置換したものを返してくる。なので、元のstringは何も変わってないので変わったつもりで使おうとしたらアレ?ってなるし、毎回文字列のコピーをするので場合によっては無駄になる。その場合は-replaceMatchesInString:options:range:withTemplate:の方を使う。基本的には-stringByReplacingMatchesInString:options:range:withTemplate:と同じなんだけど、以下の点が違う。

  • 引数にNSStringでは無くNSMutableStringを取る
  • 引数のオブジェクトのコピーではなく引数のオブジェクト自体を置換する
  • 返り値は置換後の文字列ではなく整数値で、置換箇所の数を返す

というわけで、ある正規表現で置換した文字列をさらに別な正規表現で置換して、みたいなことをやる場合はこっちのメソッドを使うべき。

ちなみに、上記二つのメソッドはマッチした箇所を全部置換する。例えば下のようなコードだと「大丈夫か」と「大丈夫だ、」が両方置換される。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"チョ☆チョニッシーナ☆まっソコぶれっシュ☆エスボグリバンバーベーコンさんだね!";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"大丈夫(か|だ)、?"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

もしマッチした箇所の内特定の部分だけを置換したい場合は、-firstMatchInString:options:range:とか-matchesInString:options:range:でNSTextCheckingResultのオブジェクトを取得しておいてから、-replacementStringForResult:inString:offset:template:を使う、みたいな感じになるかしら。ちょっと面倒な気もするけど。

RegexKitLite or NSRegularExpression

両方書いてみた感想で言うと、個人的にはRegexKitLiteのNSStringにメソッド生やしてくアプローチのAPIのが使い易いと思った。CoreFoundation使ってごりごり書いてるのでパフォーマンスも悪くないし、割と早い段階からBlocksに対応してたりとアクティブに開発されてるし、その気になればソース読めるし(まぁ、チラ見しては見たもののあんまり読む気にはならないのだけども)…とか考えると、既にRegexKitLiteを使ってるなら別に無理にNSRegularExpressionに乗り換える必要は無い気がしてくる。iOS4.0以前のバージョンもターゲットにするなら他に選択肢はないし、あと何故かNSRegularExpressionクラスはiOSにしか無くてMacOSXでは使えないという面白いことになってるので、iOSでもMacでも動くようなコードを書く場合もやっぱりNSRegularExpressionは使えない。

とは言えNSRegularExpressionの方はFoundationの一部なので、数カ所正規表現での置換を使いたいが為に外部のコード落としてきてプロジェクトに組み込んでlibicucoreに忘れずにリンクして…ってしないで済むなぁとか、万が一iOSの内部の実装が変わったりなんかの規約が変わったりしてもおそらく書き換えないで済むだろうなぁという多少の安心感とかはある。ので、これから作るアプリで、4.0以降のみをターゲットにしてる場合は、NSRegularExpressionを使って書こうかなぁなんて思ったりしてたり。