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

‘置換’はどうやればいいのですか?

そうですよね、’検索’ができたならその次は’置換’でしょう。置換すなわち文字列のある部分を他の文字列で置き換えるのは、単なる’検索’より少しばかり複雑です。

charwchar_t配列を文字列として用いている場合、’置換’はひどく面倒な処理が必要となります。’置換’によって文字列の長さが変わらないのなら何の問題もないのですが、文字列の一部をより短い文字列で置き換える場合、置き換える箇所の後ろに続く部分を短くなった分だけ前に詰めなくてはなりません。また、より長い文字列で置き換える場合、置き換える箇所の後ろに続く部分をより後ろにズラさなくてはなりません。困ったことにcharwchar_t配列の長さを知ることができませんから、置換による領域あふれの危険が常につきまといます。

それに対し標準C++ライブラリが提供する文字列クラスstd::basic_string(string/wstring)は長さをそれ自身が管理していますから、置換は思いのままです。std::basic_stringには部分文字列の置換を行うメソッドが用意されています。

std::string str;
str.replace(pos,len,rep);

によって、strposからlen文字をrepに置き換えます。

std::string str = "Hello, World";
str.replace(7,5,"C++"); // str = "Hello, C++"

boost::match_resultsのメソッドposition(n)およびlength(n)はマッチした部分の位置と長さを返してくれますから、これを使えば置換は簡単です:

std::string str = "Hello, World";
boost::reg_expression<char> regex = "World";
boost::match_results<std::string::iterator> results;
if ( boost::regex_search(str, regex, results) ) {
  str.replace(results.position(0), results.length(0), "C++");
}

このコードはstr中の最初に現れる”World”を”C++”に置き換えます。

では最初に現れるものだけでなく、文字列中のすべての”World”を”C++”に置き換えるにはどうすればよいのでしょうか。

boost::regex_grep()は第1引数にcallback関数オブジェクトを与えることで、与えた文字列から正規表現にマッチする箇所が見つかる度にcallbackされ、callbackがfalseを返した時点で検索を終了します。

#include <iostream>
#include <boost/regex.hpp>

sturut callback {
  bool operator()(const boost::match_results<std::string::iterator>& mr) {
  std::cout << "position= " << mr.position(0) << " length= " << mr.length(0) << std::endl;
  return true;
  }
};

int main() {
  std::string str = "Hello, World. Good bye World.";
  boost::reg_expression<char> regex = "World";
  boost::regex_grep(callback(), str.begin(), str.end(), regex);
  return 0;
}

したがって、callback内で位置と長さを適当なコンテナに記憶させることができます:

#include <iostream>
#include <vector>
#include <utility>
#include <boost/regex.hpp>

std::vector< std::pair<int,int> > container;

sturut callback {
  bool operator()(const boost::match_results<std::string::iterator>& mr) {
    container.push_back(std::pair<int,int>(mr.position(0), mr.length(0)));
    return true;
  }
};

int main() {
  std::string str = "Hello, World. Good bye World.";
  boost::reg_expression<char> regex = "World";
  boost::regex_grep(callback(), str.begin(), str.end(), regex);
...

これでstr中に現れる”World”の位置と長さがcontainerに格納されました。この情報を元にstr.replace()によって”C++”に置き換えることができます。このとき、見つかったのとは’逆順’で置き換えを行わないと、正しく置換されないので注意してください:

...
  while ( !container.empty() ) {
    str.replace(container.back().first, container.back().second, "C++");
    container.pop_back();
  }
  std::cout << str << std::cout;
  return 0;
}

以上のようなアルゴリズムで正規表現のマッチと置換が実現できます。が、これではあまりに複雑/煩雑です。そこで、この一連の処理を関数テンプレートregex_replace()を実装しました。

regex/replace.h

#ifndef __REGEX_REPLACE_H__
#define __REGEX_REPLACE_H__

#include <boost/regex.hpp>

#include <vector>  // std::vector
#include <utility> // std::pair

namespace s34 {

  /*
   * replace helper (internal)
   */
  namespace detail { 
    template<class Iterator>
    class pushback_submatch {
    public:
      typedef std::vector< std::pair<int,int> > container;
      explicit pushback_submatch(container& v, int s, bool a)
        : vec_(&v), sub_(s), all_(a) {}
      bool operator()(const boost::match_results<Iterator>& mr) {
        vec_->push_back(container::value_type(mr.position(sub_),mr.length(sub_)));
        return all_;
      }
    private: 
      container* vec_;
      int sub_;
      bool all_;
    };
  }

  /*
   * size_t
   * regex_replace(std::basic_string<charT, ST, SA>& str, 
   *             const boost::reg_expression<charT, traits, Allocator>& e, 
   *             const std::basic_string<charT, ST, SA>& rep,
   *             int      sub =0,
   *             bool     all =false,
   *             unsigned flags = boost::match_default)
   *
   *   str から e にマッチする部分文字列を探し、
   *   そのsub番目のマッチ部分をrepで置き換える。
   *   all==trueのときは、この置換を連続して行う。
   *     
   */
  template<class ST, class SA, class Allocator, class charT, class traits>
  size_t
  regex_replace(std::basic_string<charT, ST, SA>& str, 
                const boost::reg_expression<charT, traits, Allocator>& e, 
                const std::basic_string<charT, ST, SA>& rep,
                int      sub =0,
                bool     all =false,
                unsigned flags = boost::match_default) {
    typedef detail::pushback_submatch<std::basic_string<charT,ST,SA>::iterator> helper;
    helper::container sub_stack;
    size_t n = boost::regex_grep(helper(sub_stack, sub, all), 
                                 str.begin(), str.end(),
                                 e, flags);
    while ( !sub_stack.empty() ) {
      str.replace(sub_stack.back().first, sub_stack.back().second, rep);
      sub_stack.pop_back();
    }
    return n;
  }

}

#endif

テスト・コード

#include <iostream>
#include <regex/replace.h>

using std::wcout;
using std::endl;

void s34_replace_trial() {
  std::wstring                   source   = L"僕は海が好きで山が好きで川も好き";
  boost::reg_expression<wchar_t> regex    = L"(山|川)(.)好き";
  std::wstring                   replace  = L"だって";
  int                            submatch = 2;

  wcout << L"¥nreplace strings that matches the 2'nd braces of '"
        << regex.str() << L"' with '"
        << replace << L"'" << endl; 
  wcout << L"before replace [" << source << L"]" << endl;
  s34::regex_replace(source, regex, replace, submatch, true);
  wcout << L"after  replace [" << source << L"]" << endl;
}

実行結果

replace strings that matches the 2'nd braces of '(山|川)(.)好き' with 'だって'
before replace [僕は海が好きで山が好きで川も好き]
after  replace [僕は海が好きで山だって好きで川だって好き]

とてもシンプルに実装できているのですが、少しばかり問題があります。検索された文字列の位置と長さを保持しなくてはならないためにメモリを食うんです。

たとえば “aaaaaaaaaaaa….”(N個の’a’) から “a”を検索すると、位置と長さの組をN個貯めておかなければならないわけです。(それに加えて、文字列の置換を繰り返すためにそれほど速くはないでしょう。)

そこで、最初に長さ0の文字列を用意しておき、マッチするたびに置換と共に連結していくアルゴリズムを考えました。少々複雑ですが速くてコンパクトです。

regex/replace.h

#ifndef __REGEX_REPLACE_H__
#define __REGEX_REPLACE_H__

#include <boost/regex.hpp>

#include <vector>  // std::vector
#include <utility> // std::pair

namespace s34 {

  /*
   * size_t
   * regex_replace(std::basic_string<charT, ST, SA>& str, 
   *             const boost::reg_expression<charT, traits, Allocator>& e, 
   *             const std::basic_string<charT, ST, SA>& rep,
   *             int      sub =0,
   *             bool     all =false,
   *             unsigned flags = boost::match_default)
   *
   *   str から e にマッチする部分文字列を探し、
   *   そのsub番目のマッチ部分をrepで置き換える。
   *   all==trueのときは、この置換を連続して行う。
   *     
   */
  template<class ST, class SA, class Allocator, class charT, class traits>
  size_t
  regex_replace(std::basic_string<charT, ST, SA>& str, 
                const boost::reg_expression<charT, traits, Allocator>& e, 
                const std::basic_string<charT, ST, SA>& rep,
                int      sub =0,
                bool     all =false,
                unsigned flags = boost::match_default) {
    std::basic_string<charT, ST, SA> result;
    boost::match_results<const charT*> results;
    bool   matched;
    size_t count = 0;
    size_t start = 0;
    size_t length = str.length();
    do {
      matched = boost::regex_search(str.data() + start, results, e, flags);
      if ( matched ) {
        ++count;
        result.append(str, start, results.position(sub));
        result.append(rep);
        size_t tmp = start;
        start += results.position(0) + results.length(0);
        if ( !results.length(0) ) ++start;
        size_t trail = tmp + results.position(sub) + results.length(sub);
        result.append(str, trail, start - trail);
      }
    } while ( matched && all &&  start < length );
    result.append(str, start, str.length() - start);
    str = result;
    return count;
  }

}

#endif

※ おまけ

正規表現なんて気の利いたものは要らない。単に文字列の一部を他の文字列で置換したいんだけど…

了解。簡単です。

単純な文字列の置換

#include <iostream>
#include <string>

template<class E, class T, class A>
std::basic_string<E,T,A>
replace_all(
  const std::basic_string<E,T,A>& source,   // source中にある
  const std::basic_string<E,T,A>& pattern,  // patternを
  const std::basic_string<E,T,A>& placement // placementに置き換える
  ) {
  std::basic_string<E,T,A> result(source);
  for ( std::string::size_type pos = 0 ;
        std::string::npos != (pos = result.find(pattern,pos));
        pos += placement.size() )
    result.replace(pos, pattern.size(), placement);
  return result;
}

int main() {
  std::string source    = "すうどんもうどんもうどんのうち" ;
  std::string pattern   = "うどん" ;
  std::string placement = "もも" ;
  // 結果 :               "すもももももももものうち"
  std::cout << replace_all(source,pattern,placement) << std::endl;
  
  return 0;
}

非常に簡単な実装ですが、replaceが頻繁に行われるのであまり速くありません。そこで…

単純な文字列の置換 スピードアップ版

#include <iostream>
#include <string>

template<class E, class T, class A>
std::basic_string<E,T,A>
replace_all(
  const std::basic_string<E,T,A>& source,
  const std::basic_string<E,T,A>& pattern,
  const std::basic_string<E,T,A>& placement
  ) {
  std::basic_string<E,T,A> result;
  std::basic_string<E,T,A>::size_type pos_before = 0;
  std::basic_string<E,T,A>::size_type pos = 0;
  std::basic_string<E,T,A>::size_type len = pattern.size();
  while( ( pos = source.find( pattern, pos ) ) != std::string::npos ) {
    result.append(source, pos_before, pos - pos_before);
    result.append(placement);
    pos += len ;
    pos_before = pos ;
  }
  result.append(source, pos_before, source.size() - pos_before) ;
  return result;
}

int main() {
  std::string source    = "すうどんもうどんもうどんのうち" ;
  std::string pattern   = "うどん" ;
  std::string placement = "もも" ;
  // 結果 :               "すもももももももものうち"
  std::cout << replace_all(source,pattern,placement) << std::endl;
  
  return 0;
}

※ 上記の二つのreplace_allはマルチバイト文字を考慮していません。