iOS アプリ開発での正規表現を使った文字列処理がややこしい
iOS での正規表現をつかった文字列処理、Ruby や Perl はおろか、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 のプログラムで、外部ライブラリとか無しで同じことをしようとすると、次の手順が必要になる。ようだ。
- 正規表現オブジェクトを生成する: NSRegularExpression オブジェクト
- マッチする「範囲」のデータを配列状に書き出す: -matchesInString:options:range: メソッドを使う
- 元の文字列と「範囲」のデータを使ってマッチした文字列を取りだす: 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 でコンパイルした限りでは動いている感じ。ただし、無意味にコピペに失敗とかもあるので油断は禁物。