Frage Java Ersetzen mehrerer untergeordneter Teilzeichenfolgen in einer Zeichenfolge gleichzeitig (oder auf effizienteste Weise)


Ich muss viele verschiedene Unterzeichenfolgen in einer Zeichenfolge auf die effizienteste Weise ersetzen. Gibt es einen anderen Weg als den Brute-Force-Weg, jedes Feld mit string.replace zu ersetzen?


76
2017-08-25 07:52


Ursprung


Antworten:


Wenn die Zeichenkette, auf der Sie arbeiten, sehr lang ist oder Sie mit vielen Zeichenketten arbeiten, könnte es sich lohnen, einen java.util.regex.Matcher zu verwenden (das erfordert Zeitaufwand für die Kompilierung, daher ist es nicht effizient Wenn Ihre Eingabe sehr klein ist oder Ihr Suchmuster häufig wechselt.

Im Folgenden finden Sie ein vollständiges Beispiel basierend auf einer Liste von Token, die aus einer Karte entnommen wurden. (Verwendet StringUtils von Apache Commons Lang).

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());

Sobald der reguläre Ausdruck kompiliert wurde, ist das Scannen der Eingabe-Zeichenfolge im Allgemeinen sehr schnell (obwohl, wenn Ihr regulärer Ausdruck komplex ist oder ein Backtracking beinhaltet, Sie noch einen Benchmark benötigen würden, um dies zu bestätigen!)


84
2017-08-25 08:55



Algorithmus

Eine der effizientesten Methoden zum Ersetzen übereinstimmender Zeichenfolgen (ohne reguläre Ausdrücke) ist die Verwendung der Aho-Corasick-Algorithmus mit einem performant Trie (ausgesprochen "versuchen"), schnell Hashing Algorithmus und effizient Sammlungen Implementierung.

Einfacher Code

Der vielleicht einfachste Code zum Schreiben nutzt Apache StringUtils.replaceEach wie folgt:

  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 );
  }

Das verlangsamt sich bei großen Texten.

Schneller Code

Bors Umsetzung des Aho-Corasick-Algorithmus führt ein bisschen mehr Komplexität ein, die zu einem Implementierungsdetail wird, indem eine Fassade mit derselben Methodensignatur verwendet wird:

  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();
  }

Benchmarks

Für die Benchmarks wurde der Puffer mit erstellt ZufallsNumerisch wie folgt:

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

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

Woher MATCHES_DIVISOR bestimmt die Anzahl der zu injizierenden Variablen:

  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 ) );
    }
  }

Der Benchmark-Code selbst (JMH schien übertrieben):

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

1.000.000: 1.000

Ein einfacher Mikro-Benchmark mit 1.000.000 Zeichen und 1.000 zufällig platzierten Strings zum Ersetzen.

  • testStringUtils: 25 Sekunden, 25533 Millis
  • testBorAhoCorasick: 0 Sekunden, 68 Millis

Kein Wettbewerb.

10.000: 1.000

Verwenden von 10.000 Zeichen und 1.000 übereinstimmenden Zeichenfolgen zum Ersetzen von:

  • testStringUtils: 1 Sekunde, 1402 Millis
  • testBorAhoCorasick: 0 Sekunden, 37 Millis

Die Kluft schließt sich.

1.000: 10

Verwenden von 1.000 Zeichen und 10 übereinstimmenden Zeichenfolgen zum Ersetzen:

  • testStringUtils: 0 Sekunden, 7 Millis
  • testBorAhoCorasick: 0 Sekunden, 19 Millis

Bei kurzen Streichern überlagert der Overhead von Aho-Corasick den Brute-Force-Ansatz StringUtils.replaceEach.

Ein hybrider Ansatz basierend auf der Textlänge ist möglich, um das Beste aus beiden Implementierungen zu erhalten.

Implementierungen

Erwägen Sie, andere Implementierungen für Text mit mehr als 1 MB zu vergleichen, einschließlich:

Papiere

Papiere und Informationen zum Algorithmus:


33
2017-11-28 03:08



Wenn Sie einen String mehrmals ändern, ist es normalerweise effizienter, einen StringBuilder zu verwenden (Aber messen Sie Ihre Leistung, um es herauszufinden):

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();

Jedes Mal, wenn Sie einen String ersetzen, wird ein neues String-Objekt erstellt, da Strings unveränderlich sind. StringBuilder ist änderbar, dh es kann beliebig oft geändert werden.


7
2017-08-25 08:01



StringBuilder wird das Ersetzen effizienter durchführen, da sein Zeichenmatrixpuffer auf eine erforderliche Länge spezifiziert werden kann.StringBuilder ist für mehr als nur anhängen gedacht!

Die eigentliche Frage ist natürlich, ob das eine Optimierung zu weit ist? Die JVM ist sehr gut im Umgang mit der Erstellung mehrerer Objekte und der nachfolgenden Garbage Collection. Wie bei allen Optimierungsfragen lautet meine erste Frage, ob Sie dies gemessen haben und festgestellt haben, dass dies ein Problem ist.


4
2017-08-25 08:02



Wie wäre es mit dem alles ersetzen() Methode?


3
2017-08-25 07:59



Überprüfen Sie dies:

Zeichenfolge.format (str, STR [])

...

Beispielsweise:

String.format ("Setze dein% s dahin, wo dein% s ist", "Geld", "Mund");


2
2017-12-30 08:16



Rythm eine Java-Template-Engine jetzt mit einer neuen Funktion namens aufgerufen String-Interpolationsmodus was dir erlaubt etwas zu tun wie:

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

Der obige Fall zeigt, dass Sie das Argument der Vorlage nach Position übergeben können. Mit Rythm können Sie Argumente auch nach Namen übergeben:

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

Hinweis Rythm ist sehr schnell, etwa 2 bis 3 mal schneller als String.format und Velocity, weil es die Vorlage in Java-Byte-Code kompiliert, die Laufzeitleistung ist sehr nahe an Concatentation mit StringBuilder.

Links:


2
2017-07-01 08:42