web-dev-qa-db-ja.com

Javaストリング内の複数の異なるサブストリングを一度に(または最も効率的な方法で)置換する

文字列内の多くの異なる部分文字列を最も効率的な方法で置き換える必要があります。 string.replaceを使用して各フィールドを置き換える総当たり的な方法以外の別の方法がありますか?

89
Yossale

操作している文字列が非常に長い場合、または多くの文字列を操作している場合は、Java.util.regex.Matcherを使用する価値があります(これにはコンパイルに時間がかかりますので、効率的ではありません入力が非常に小さい場合、または検索パターンが頻繁に変更される場合)。

以下は、マップから取得したトークンのリストに基づいた完全な例です。 (Apache Commons LangのStringUtilsを使用します)。

Map<String,String> tokens = new HashMap<String,String>();
tokens.put("cat", "Garfield");
tokens.put("beverage", "coffee");

String template = "%cat% really needs some %beverage%.";

// Create pattern of the format "%(cat|beverage)%"
String patternString = "%(" + StringUtils.join(tokens.keySet(), "|") + ")%";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(template);

StringBuffer sb = new StringBuffer();
while(matcher.find()) {
    matcher.appendReplacement(sb, tokens.get(matcher.group(1)));
}
matcher.appendTail(sb);

System.out.println(sb.toString());

正規表現がコンパイルされると、通常、入力文字列のスキャンは非常に高速です(ただし、正規表現が複雑であるか、バックトラッキングが必要な場合は、これを確認するためにベンチマークする必要があります!)

94
Todd Owen

アルゴリズム

(正規表現なしで)一致する文字列を置換する最も効率的な方法の1つは、 Aho-Corasickアルゴリズム をパフォーマントと使用することです Trie (「try」と発音)、高速- ハッシュ アルゴリズム、および効率的な コレクション 実装。

簡単なコード

シンプルなソリューションは、Apacheの StringUtils.replaceEach 次のように:

  private String testStringUtils(
    final String text, final Map<String, String> definitions ) {
    final String[] keys = keys( definitions );
    final String[] values = values( definitions );

    return StringUtils.replaceEach( text, keys, values );
  }

これは、大きなテキストでは遅くなります。

高速コード

Borの実装 Aho-Corasickアルゴリズムでは、同じメソッドシグネチャを持つファサードを使用することにより、実装の詳細になるもう少し複雑さが導入されます。

  private String testBorAhoCorasick(
    final String text, final Map<String, String> definitions ) {
    // Create a buffer sufficiently large that re-allocations are minimized.
    final StringBuilder sb = new StringBuilder( text.length() << 1 );

    final TrieBuilder builder = Trie.builder();
    builder.onlyWholeWords();
    builder.removeOverlaps();

    final String[] keys = keys( definitions );

    for( final String key : keys ) {
      builder.addKeyword( key );
    }

    final Trie trie = builder.build();
    final Collection<Emit> emits = trie.parseText( text );

    int prevIndex = 0;

    for( final Emit emit : emits ) {
      final int matchIndex = emit.getStart();

      sb.append( text.substring( prevIndex, matchIndex ) );
      sb.append( definitions.get( emit.getKeyword() ) );
      prevIndex = emit.getEnd() + 1;
    }

    // Add the remainder of the string (contains no more matches).
    sb.append( text.substring( prevIndex ) );

    return sb.toString();
  }

ベンチマーク

ベンチマークでは、次のように randomNumeric を使用してバッファーが作成されました。

  private final static int TEXT_SIZE = 1000;
  private final static int MATCHES_DIVISOR = 10;

  private final static StringBuilder SOURCE
    = new StringBuilder( randomNumeric( TEXT_SIZE ) );

どこ MATCHES_DIVISORは、注入する変数の数を決定します。

  private void injectVariables( final Map<String, String> definitions ) {
    for( int i = (SOURCE.length() / MATCHES_DIVISOR) + 1; i > 0; i-- ) {
      final int r = current().nextInt( 1, SOURCE.length() );
      SOURCE.insert( r, randomKey( definitions ) );
    }
  }

ベンチマークコード自体( [〜#〜] jmh [〜#〜] 過剰に思えた):

long duration = System.nanoTime();
final String result = testBorAhoCorasick( text, definitions );
duration = System.nanoTime() - duration;
System.out.println( elapsed( duration ) );

1,000,000:1,000

置換する1,000,000文字と1,000個のランダムに配置された文字列を含むシンプルなマイクロベンチマーク。

  • testStringUtils:25秒、25533ミリ秒
  • testBorAhoCorasick:0秒、68ミリ秒

コンテストはありません。

10,000:1,000

10,000文字と一致する1,000文字列を使用して置き換える:

  • testStringUtils:1秒、1402ミリ秒
  • testBorAhoCorasick:0秒、37ミリ秒

分割が終了します。

1,000:10

1,000文字と10個の一致する文字列を使用して置き換える:

  • testStringUtils:0秒、7ミリ秒
  • testBorAhoCorasick:0秒、19ミリ秒

短い文字列の場合、Aho-Corasickを設定するオーバーヘッドにより、StringUtils.replaceEach

両方の実装を最大限に活用するために、テキストの長さに基づくハイブリッドアプローチが可能です。

実装

以下を含む、1 MBを超えるテキストのその他の実装を比較することを検討してください。

論文

アルゴリズムに関する論文と情報:

52
Dave Jarvis

Stringを何度も変更する場合、通常はStringBuilder (ただし、パフォーマンスを測定して確認):を使用する方が効率的です。

String str = "The rain in Spain falls mainly on the plain";
StringBuilder sb = new StringBuilder(str);
// do your replacing in sb - although you'll find this trickier than simply using String
String newStr = sb.toString();

文字列は不変なので、文字列で置換を行うたびに、新しい文字列オブジェクトが作成されます。 StringBuilderは可変です。つまり、必要なだけ変更できます。

7
Steve McLeod

これは私のために働いた:

String result = input.replaceAll("string1|string2|string3","replacementString");

例:

String input = "applemangobananaarefriuits";
String result = input.replaceAll("mango|are|ts","-");
System.out.println(result);

出力: Apple-banana-friui-

5
Bikram

StringBuilderは、文字配列バッファーを必要な長さに指定できるため、置換をより効率的に実行します。StringBuilderは、追加以外にも使用できるように設計されています。

もちろん、本当の問題は、これがあまりにも最適化されているかどうかです。 JVMは複数のオブジェクトの作成とその後のガベージコレクションの処理に非常に優れており、すべての最適化の質問と同様に、私の最初の質問は、これを測定して問題だと判断したかどうかです。

4
Brian Agnew

Rythm a Javaテンプレートエンジンはと呼ばれる新機能とともにリリースされました) 文字列補間モード次のようなことができます:

String result = Rythm.render("@name is inviting you", "Diana");

上記の例は、位置によってテンプレートに引数を渡すことができることを示しています。 Rythmでは、名前で引数を渡すこともできます。

Map<String, Object> args = new HashMap<String, Object>();
args.put("title", "Mr.");
args.put("name", "John");
String result = Rythm.render("Hello @title @name", args);

注Rythmは非常に高速で、String.formatおよびvelocityよりも約2〜3倍高速です。テンプレートをJavaバイトコードにコンパイルするため、ランタイムパフォーマンスはStringBuilderとの連結に非常に近いです。

リンク:

2
Gelin Luo

replaceAll() メソッドを使用してはどうですか?

2
Avi

以下は Todd Owenの答え に基づいています。このソリューションには、置換に正規表現で特別な意味を持つ文字が含まれている場合、予期しない結果が得られるという問題があります。また、オプションで大文字と小文字を区別しない検索を実行できるようにしたいと考えました。ここに私が思いついたものがあります:

/**
 * Performs simultaneous search/replace of multiple strings. Case Sensitive!
 */
public String replaceMultiple(String target, Map<String, String> replacements) {
  return replaceMultiple(target, replacements, true);
}

/**
 * Performs simultaneous search/replace of multiple strings.
 * 
 * @param target        string to perform replacements on.
 * @param replacements  map where key represents value to search for, and value represents replacem
 * @param caseSensitive whether or not the search is case-sensitive.
 * @return replaced string
 */
public String replaceMultiple(String target, Map<String, String> replacements, boolean caseSensitive) {
  if(target == null || "".equals(target) || replacements == null || replacements.size() == 0)
    return target;

  //if we are doing case-insensitive replacements, we need to make the map case-insensitive--make a new map with all-lower-case keys
  if(!caseSensitive) {
    Map<String, String> altReplacements = new HashMap<String, String>(replacements.size());
    for(String key : replacements.keySet())
      altReplacements.put(key.toLowerCase(), replacements.get(key));

    replacements = altReplacements;
  }

  StringBuilder patternString = new StringBuilder();
  if(!caseSensitive)
    patternString.append("(?i)");

  patternString.append('(');
  boolean first = true;
  for(String key : replacements.keySet()) {
    if(first)
      first = false;
    else
      patternString.append('|');

    patternString.append(Pattern.quote(key));
  }
  patternString.append(')');

  Pattern pattern = Pattern.compile(patternString.toString());
  Matcher matcher = pattern.matcher(target);

  StringBuffer res = new StringBuffer();
  while(matcher.find()) {
    String match = matcher.group(1);
    if(!caseSensitive)
      match = match.toLowerCase();
    matcher.appendReplacement(res, replacements.get(match));
  }
  matcher.appendTail(res);

  return res.toString();
}

ユニットテストケースは次のとおりです。

@Test
public void replaceMultipleTest() {
  assertNull(ExtStringUtils.replaceMultiple(null, null));
  assertNull(ExtStringUtils.replaceMultiple(null, Collections.<String, String>emptyMap()));
  assertEquals("", ExtStringUtils.replaceMultiple("", null));
  assertEquals("", ExtStringUtils.replaceMultiple("", Collections.<String, String>emptyMap()));

  assertEquals("folks, we are not sane anymore. with me, i promise you, we will burn in flames", ExtStringUtils.replaceMultiple("folks, we are not winning anymore. with me, i promise you, we will win big league", makeMap("win big league", "burn in flames", "winning", "sane")));

  assertEquals("bcaacbbcaacb", ExtStringUtils.replaceMultiple("abccbaabccba", makeMap("a", "b", "b", "c", "c", "a")));
  assertEquals("bcaCBAbcCCBb", ExtStringUtils.replaceMultiple("abcCBAabCCBa", makeMap("a", "b", "b", "c", "c", "a")));
  assertEquals("bcaacbbcaacb", ExtStringUtils.replaceMultiple("abcCBAabCCBa", makeMap("a", "b", "b", "c", "c", "a"), false));

  assertEquals("c colon  backslash temp backslash  star  dot  star ", ExtStringUtils.replaceMultiple("c:\\temp\\*.*", makeMap(".", " dot ", ":", " colon ", "\\", " backslash ", "*", " star "), false));
}

private Map<String, String> makeMap(String ... vals) {
  Map<String, String> map = new HashMap<String, String>(vals.length / 2);
  for(int i = 1; i < vals.length; i+= 2)
    map.put(vals[i-1], vals[i]);
  return map;
}
1
Kip

これをチェックして:

String.format(str、STR [])

...

例えば:

String.format( "%sがある場所に%sを置く"、 "money"、 "mouth");

1
Ali
public String replace(String input, Map<String, String> pairs) {
  // Reverse lexic-order of keys is good enough for most cases,
  // as it puts longer words before their prefixes ("tool" before "too").
  // However, there are corner cases, which this algorithm doesn't handle
  // no matter what order of keys you choose, eg. it fails to match "edit"
  // before "bed" in "..bedit.." because "bed" appears first in the input,
  // but "edit" may be the desired longer match. Depends which you prefer.
  final Map<String, String> sorted = 
      new TreeMap<String, String>(Collections.reverseOrder());
  sorted.putAll(pairs);
  final String[] keys = sorted.keySet().toArray(new String[sorted.size()]);
  final String[] vals = sorted.values().toArray(new String[sorted.size()]);
  final int lo = 0, hi = input.length();
  final StringBuilder result = new StringBuilder();
  int s = lo;
  for (int i = s; i < hi; i++) {
    for (int p = 0; p < keys.length; p++) {
      if (input.regionMatches(i, keys[p], 0, keys[p].length())) {
        /* TODO: check for "edit", if this is "bed" in "..bedit.." case,
         * i.e. look ahead for all prioritized/longer keys starting within
         * the current match region; iff found, then ignore match ("bed")
         * and continue search (find "edit" later), else handle match. */
        // if (better-match-overlaps-right-ahead)
        //   continue;
        result.append(input, s, i).append(vals[p]);
        i += keys[p].length();
        s = i--;
      }
    }
  }
  if (s == lo) // no matches? no changes!
    return input;
  return result.append(input, s, hi).toString();
}
0
Robin479