iOS アプリ開発での正規表現を使った文字列処理がややこしい

iOS での正規表現をつかった文字列処理、RubyPerl はおろか、C# なんぞに比べても一段とわかりにくい気がする。どうしてこうなった。

Ruby で考える

buf には次のような文字列が入っているとする。ファイルをごっそり読みこんだ状態で、改行も含まれている状態になっている。

1324650815.dat::【大阪】 ほげほげ
1324392193.dat::【電力】 ふにふに
1324640842.dat::【国際】 もこもこ
1324650659.dat::【社会】 どきどき
...

このとき、'xxxxx.dat' というデータの部分の一覧を取り出したいとき、Ruby なら下のように書ける。

dat = buf.scan(/(\d+.dat)::/)

これで dat に 'xxxx.dat' という文字列が配列状に詰めこまれる。とっても簡単。

Objective-C (iOS) で考える

iOS のプログラムで、外部ライブラリとか無しで同じことをしようとすると、次の手順が必要になる。ようだ。

  1. 正規表現オブジェクトを生成する: NSRegularExpression オブジェクト
  2. マッチする「範囲」のデータを配列状に書き出す: -matchesInString:options:range: メソッドを使う
  3. 元の文字列と「範囲」のデータを使ってマッチした文字列を取りだす: NSTextCheckingResult

とても素直に書くと以下のような感じ。

	// NSString *body; に文字列が入っているものとする。
	NSError *error   = nil;
        NSRegularExpression *regexp = 
	[NSRegularExpression regularExpressionWithPattern:@"(\\d+.dat)::" 
                             options:0 error:&error]; 

        NSArray *dat = [regexp matchesInString:body options:0 range:NSMakeRange(0, body.length)];
        NSMutableArray *dats = [[NSMutableArray alloc] init];
        
        for ( int i = 0; i < [dat count]; i++ )
        {
            NSTextCheckingResult *res = [dat objectAtIndex: i];
            if (res != nil ){
                [dats addObject: [body substringWithRange:[res rangeAtIndex:1]]];
            }

        }

dat に入ってくる NSTextCheckingResult には、マッチした文字列が直接入っているのではなく、マッチした「範囲」の情報が入ってくる。実際に、マッチした文字列そのものを取り出すには、マッチをかけた文字列である body と subStringWithRange: メソッドを使って取りだす操作が必要になる。

このとき、NSTextCheckingResult の -rangeAtIndex: メソッドを使うことで、範囲オブジェクトを取りだせる。-rangeAtIndes: に与える文字の意味は、

  • 0: 与えた正規表現全体にマッチした範囲
  • 1: 与えた正規表現に含まれる () で囲まれた部分のうち、最初のひとつめにマッチした部分
  • 2: 与えた正規表現に含まれる () で囲まれた部分のうち、ふたつめにマッチした部分
  • 3: 以下同様

となっている。上の例だと正規表現が @"(\\d+.dat)::" で与えられており、括弧で囲まれた部分が1箇所だけなので、rangeAtIndex: メソッドは、それぞれ

  • 0: "xxxxx.dat::" という文字列の「範囲」(文字列そのものじゃない)
  • 1: "xxxxx.dat" という文字列の範囲
  • 2: nil

をかえす。3 以降でも nil になる。数字の上限は与えた正規表現によって決まる。このへんが分かりにくいと思った。ちなみに enumerateMatchesInString:options:range:usingBlock: メソッドを使うと NSArray* dat なしでも書ける。

	// NSString *body; に文字列が入っているものとする。
	NSError *error   = nil;
        NSRegularExpression *regexp = 
	[NSRegularExpression regularExpressionWithPattern:@"(\\d+.dat)::" 
                             options:0 error:&error]; 

        NSMutableArray *dats = [[NSMutableArray alloc] init];

        id proc = ^(NSTextCheckingResult *arr, NSMatchingFlags flag, BOOL *stop) {
            [dats addObject: [body substringWithRange:[arr rangeAtIndex:1]]];
        };
        
        [regexp enumerateMatchesInString:body options:0 
                range:NSMakeRange(0, body.length) usingBlock:proc];

これで dats にマッチした文字列の一覧がずらずら入ってくる。とりあえず Xcode 4.2 でコンパイルした限りでは動いている感じ。ただし、無意味にコピペに失敗とかもあるので油断は禁物。

バックスラッシュの罠

正規表現中にある '\' はバックスラッシュなんだけど、環境によっては円記号に見えてるかもしれない。日本語キーボードで Xcode で入力するときは Alt + \ として、ちゃんとバックスラッシュを入力してやらないと、マッチのときに失敗する。円記号では代用にならない。US キーボードなら問題なくバックスラッシュになるはず・・・