株式会社エス・スリー・フォー

ICU 2.4 の正規表現

IBMのOpenSource: ICU はUnicodeを扱うライブラリとして高い評価を得ています。ICUは何度かのバージョンアップを繰り返しながら、その度にライブラリとしての完成度を上げてきました。

ICU 2.4では、新たにUnicode文字列を対象とした正規表現(regular Expression)をサポートします。正規表現を扱うアプリケーションの構築にはBoost Regex++などのライブラリを使用していましたが、ICUが正規表現をサポートしたことで、選択の幅がより広がったことになります。

ICUの正規表現を利用する場合は、少なくともライブラリ: icuucicuin をリンクしてください。

なお、以下のサンプルは Microsoft Visual C++ 6.0 および 7.0 でコンパイル/実行を確認しました。

ICUが定義した型UCharwchar_tが等価であることを前提としており、そうでない場合はUnicodeString str = L"..."; のような代入ができません。

RegexPatternとRegexMatcher

ICUの正規表現は、<unicode/regex.h>に定義された二つのクラス:RegexPatternRegexMatcherによってサポートされます。RegexPatternは正規表現文字列を内部形式にコンパイルしたもの、RegexMatcherRegexPatternに基づいた正規表現パターンが文字列のどの位置に現れるかを検索します。この二つはJava2 1.4で導入された java.util.regex パッケージの Pattern, Matcher とコンパチブルなインタフェースを提供しています。

パターン生成

RegexPatternのインスタンスはstaticメソッド:

RegexPattern* RegexPattern::compile(const UnicodeString& regex, UParseError& pe, UErrorCode& status);

によって生成します。regexに正規表現文字列を与えます。

コンパイルに失敗したときはstatusにエラーコード、peにエラーの詳細情報が格納されます。

UnicodeString regex = L"(.)や"; // 正規表現
  UParseError   pe;
  UErrorCode    status = U_ZERO_ERROR;
  RegexPattern* pattern = RegexPattern::compile(regex, pe, status);
  assert( U_SUCCESS(status) );

生成されたパターンからRegexMatcherを生成するのがRegexPatternのメソッド:

RegexMatcher* RegexPattern::matcher(const UnicodeString& input, UErrorCode& status);

です。inputに検索対象文字列を与えます。

UnicodeString input = L"松島やああ竹島や梅島や"; // 入力(検索対象)
  RegexMatcher* matcher = pattern->matcher(input, status);
  assert( U_SUCCESS(status) );

RegexPatternは、正規表現にマッチする部分を単語の区切りと見なしてトークン分割する

int32_t RegexPattern::split(const UnicodeString& input, UnicodeString dest[], UErrorCode& status);

を持っています。inputを単語毎に切り出し、dest[destCapacity]に格納し、単語数を返します。

RegexPatternによるトークン分割

#include <iostream>
#include <locale>
#include <cassert>

#include <unicode/regex.h>

int main() {
  std::locale::global(std::locale("ja"));

  UParseError error;
  UErrorCode  status = U_ZERO_ERROR;

  UnicodeString regex = L"[とやも]";
  RegexPattern* pattern = RegexPattern::compile(regex, error, status);
  assert( U_SUCCESS(status) );

  UnicodeString fruits[10];
  UnicodeString input = L"リンゴとミカンやナシとモモも";
  int32_t splits = pattern->split(input, fruits, 10, status);
  assert( U_SUCCESS(status) );

  for ( int32_t i = 0; i < splits; ++i ) {
    std::wcout << fruits[i].getTerminatedBuffer() << std::endl;
  }

  delete pattern;
  return 0;
}

実行結果

リンゴ
ミカン
ナシ
モモ

検索

RegexMatcher::find()を呼び出す毎に、 inputからregexとマッチする部分文字列を順に検索します(マッチしなかったとき、0を返します)。

find() の結果は:

int32_t RegexMatcher::start(int group, UErrorCode& status),

int32_t RegexMatcher::end(int group, UErrorCode& status)

で得られます。

それぞれ、マッチ個所の先頭/末尾の位置を返します。 groupには正規表現に現れるグルーピング(括弧で囲まれた部分)を与えることができ、0あるいは省略時には正規表現全体を意味します。

また、UnicodeString RegexMatcher::group(int groupNum, UErrorCode& status) は、マッチする部分文字列を返します。

matcher->reset(); 
  while ( matcher->find() ) {
    for ( int32_t i = 0; i <= matcher->groupCount(); ++i ) {
      std::wcout << L"[" << matcher->start(i,status)
                 << L"," << matcher->end(i,status)
                 << L")" << matcher->group(i,status).getTerminatedBuffer()
                 << std::endl;
      assert( U_SUCCESS(status) );
    }
    std::wcout << std::endl;
  }

置換

UnicodeString RegexMatcher::replaceFirst(const UnicdeString& replacement, UErrorCode& status)

によって、最初に現れたマッチ個所をreplacementに置き換えた文字列を返します。

マッチ個所すべてを置換するときは:

UnicodeString RegexMatcher::replaceAll(const UnicdeString& replacement, UErrorCode& status)

を用います。replaceAllは以下のコードと等価です:

UnicodeString result;
  matcher->reset();
  while ( matcher->find() ) {
    matcher->appendReplacement(result, replacement, status);
    assert( U_SUCCESS(status) );
  }
  matcher->appendTail(result);

残念ながら、グルーピングたとえば正規表現 "(.)や" の "(.)" に対応する部分(部分マッチ)を置換するメソッドを提供していないようです。

部分マッチの置換は、検索とUnicodeString::replaceを用いた以下のような実装となるでしょう。

#include <iostream>
#include <locale>
#include <cassert>

#include <vector>
#include <utility>

#include <unicode/regex.h>

int main() {
  std::locale::global(std::locale("ja"));

  UParseError error;
  UErrorCode  status = U_ZERO_ERROR;

  UnicodeString regex = L"(.)島や";
  RegexPattern* pattern = RegexPattern::compile(regex, error, status);
  assert( U_SUCCESS(status) );

  UnicodeString input = L"松島やああ竹島や梅島や";
  RegexMatcher* matcher = pattern->matcher(input, status);
  assert( U_SUCCESS(status) );

  /*
   * 部分マッチ"(.)"の置換
   */
  typedef std::pair<int32_t,int32_t> range_type;
  std::vector<range_type> matches;
  matcher->reset(); 
  while ( matcher->find() ) {
    // 第一グループを保存する
    matches.push_back(range_type(matcher->start(1,status),
                                 matcher->end(1,status)));
      assert( U_SUCCESS(status) );
  }
  UnicodeString replacement = L"松";
  UnicodeString result = input;
  // 末尾から置換
  while ( !matches.empty() ) { 
    range_type range = matches.back();
    matches.pop_back();
    result.replace(range.first, range.second - range.first, 
                   replacement);
  }
  std::wcout << result.getTerminatedBuffer() << std::endl;

  delete matcher;
  delete pattern;

  return 0;
}

サンプル

#include <iostream>
#include <locale>
#include <cassert>

#include <unicode/regex.h>

int main() {
  std::locale::global(std::locale("ja"));

  UParseError error;
  UErrorCode  status = U_ZERO_ERROR;

  UnicodeString regex = L"(.)島や";
  RegexPattern* pattern = RegexPattern::compile(regex, error, status);
  assert( U_SUCCESS(status) );

  UnicodeString input = L"松島やああ竹島や梅島や";
  RegexMatcher* matcher = pattern->matcher(input, status);
  assert( U_SUCCESS(status) );

  /*
   * 検索
   */
  matcher->reset(); 
  while ( matcher->find() ) {
    for ( int32_t i = 0; i <= matcher->groupCount(); ++i ) {
      std::wcout << L"[" << matcher->start(i,status)
                 << L"," << matcher->end(i,status)
                 << L")" << matcher->group(i,status).getTerminatedBuffer()
                 << std::endl;
      assert( U_SUCCESS(status) );
    }
    std::wcout << std::endl;
  }

  UnicodeString replacement = L"松島や";
  UnicodeString result;
  /*
   * 置換 : replaceFirst
   */
  result = matcher->replaceFirst(replacement, status);
  assert( U_SUCCESS(status) );
  std::wcout << result.getTerminatedBuffer() << std::endl;

  /*
   * 置換 : replaceAll
   */
  result = matcher->replaceAll(replacement, status);
  assert( U_SUCCESS(status) );
  std::wcout << result.getTerminatedBuffer() << std::endl;

  /*
   * 置換 : appendReplacement/appendAll
   */
  result = L"";
  matcher->reset();
  while ( matcher->find() ) {
    matcher->appendReplacement(result, replacement, status);
    assert( U_SUCCESS(status) );
  }
  matcher->appendTail(result);
  std::wcout << result.getTerminatedBuffer() << std::endl;

  delete matcher;
  delete pattern;

  return 0;
}

実行結果

[0,3)松島や
[0,1)松

[5,8)竹島や
[5,6)竹

[8,11)梅島や
[8,9)梅

松島やああ竹島や梅島や
松島やああ松島や松島や
松島やああ松島や松島や

※ コンパイラ/OSによっては std::wcoutが漢字やかなを正常に出力できないものがあります。

そのような場合は、以下のコードを付加し、std::cout << narrow(...); してください。

このとき、コード冒頭の#include <locale> および std::locale::global(std::locale("ja");は不要です。

UnicodeString -> std::string 変換

#include <iostream>
#include <string>
#include <unicode/unistr.h>

std::string narrow(const UnicodeString& str) {
  // マルチバイト文字は最大2-charと仮定する。
  // shift_jis/euc-jp ならこれで大丈夫。
  int32_t length = str.length();
  char* buffer = new char[length*2+1];
  str.extract(0, length, buffer, "shift_jis");
  std::string result(buffer);
  delete[] buffer;
  return result;
}

/*
 * お試し
 */
int main() {
  UnicodeString input = L"こんにちは、世界";
  std::cout << narrow(input) << std::endl;
  return 0;
}

実行結果

こんにちは、世界