Java Sostituzione di più sottostringhe diverse in una stringa contemporaneamente (o nel modo più efficiente)


97

Ho bisogno di sostituire molte sottostringhe diverse in una stringa nel modo più efficiente. c'è un altro modo diverso da quello della forza bruta per sostituire ogni campo usando string.replace?

Risposte:


102

Se la stringa su cui stai operando è molto lunga, o stai operando su molte stringhe, allora potrebbe essere utile usare un java.util.regex.Matcher (questo richiede tempo in anticipo per la compilazione, quindi non sarà efficiente se il tuo input è molto piccolo o il tuo modello di ricerca cambia frequentemente).

Di seguito è riportato un esempio completo, basato su un elenco di token presi da una mappa. (Utilizza StringUtils di 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());

Una volta che l'espressione regolare è stata compilata, la scansione della stringa di input è generalmente molto veloce (anche se se la tua espressione regolare è complessa o implica il backtracking, dovresti comunque fare un benchmark per confermarlo!)


1
Sì, tuttavia, deve essere valutato il numero di iterazioni.
techzen

5
Penso che dovresti sfuggire ai caratteri speciali in ogni token prima di farlo"%(" + StringUtils.join(tokens.keySet(), "|") + ")%";
Sviluppatore Marius Žilėnas

Nota che puoi usare StringBuilder per un po 'più di velocità. StringBuilder non è sincronizzato. edit whoops funziona solo con java 9
Tinus Tate

3
Lettore futuro: per regex, "(" e ")" racchiuderà il gruppo da cercare. Il "%" conta come un letterale nel testo. Se i tuoi termini non iniziano E finiscono con "%" non verranno trovati. Quindi regola prefissi e suffissi su entrambe le parti (testo + codice).
linuxunil

66

Algoritmo

Uno dei modi più efficienti per sostituire le stringhe corrispondenti (senza espressioni regolari) è utilizzare l' algoritmo Aho-Corasick con un Trie performante (pronunciato "try"), un algoritmo di hashing veloce e un'implementazione efficiente delle raccolte .

Codice semplice

Una semplice soluzione sfrutta Apache StringUtils.replaceEachcome segue:

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

Questo rallenta su testi di grandi dimensioni.

Codice veloce

L'implementazione di Bor dell'algoritmo Aho-Corasick introduce un po 'più di complessità che diventa un dettaglio di implementazione utilizzando una facciata con la stessa firma del metodo:

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

Punti di riferimenti

Per i benchmark, il buffer è stato creato utilizzando randomNumeric come segue:

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

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

Dove MATCHES_DIVISORdetermina il numero di variabili da iniettare:

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

Il codice benchmark stesso ( JMH sembrava eccessivo):

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

1.000.000: 1.000

Un semplice micro-benchmark con 1.000.000 di caratteri e 1.000 stringhe posizionate casualmente da sostituire.

  • testStringUtils: 25 secondi, 25533 millis
  • testBorAhoCorasick: 0 secondi, 68 millisecondi

Nessun contesto.

10.000: 1.000

Utilizzo di 10.000 caratteri e 1.000 stringhe corrispondenti per sostituire:

  • testStringUtils: 1 secondi, 1402 millis
  • testBorAhoCorasick: 0 secondi, 37 millisecondi

Il divario si chiude.

1.000: 10

Utilizzo di 1.000 caratteri e 10 stringhe corrispondenti per sostituire:

  • testStringUtils: 0 secondi, 7 millisecondi
  • testBorAhoCorasick: 0 secondi, 19 millisecondi

Per gli archi brevi, il sovraccarico di impostare Aho-Corasick eclissa l'approccio della forza bruta di StringUtils.replaceEach.

È possibile un approccio ibrido basato sulla lunghezza del testo, per ottenere il meglio da entrambe le implementazioni.

Implementazioni

Considera la possibilità di confrontare altre implementazioni per testo più lungo di 1 MB, tra cui:

Documenti

Documenti e informazioni relative all'algoritmo:


5
Complimenti per aver aggiornato questa domanda con nuove preziose informazioni, è molto carino. Penso che un benchmark JMH sia ancora appropriato, almeno per valori ragionevoli come 10.000: 1.000 e 1.000: 10 (il JIT a volte può eseguire ottimizzazioni magiche).
Tunaki

rimuovere il builder.onlyWholeWords () e funzionerà in modo simile alla sostituzione della stringa.
Ondrej Sotolar

Grazie mille per questa eccellente risposta. Questo è decisamente molto utile! Volevo solo commentare che per confrontare i due approcci e anche per fornire un esempio più significativo, si dovrebbe costruire il Trie solo una volta nel secondo approccio e applicarlo a molte stringhe di input differenti. Penso che questo sia il vantaggio principale di avere accesso a Trie rispetto a StringUtils: lo costruisci solo una volta. Tuttavia, grazie mille per questa risposta. Condivide molto bene la metodologia per implementare il secondo approccio
Vic Seedoubleyew il

Un punto eccellente, @VicSeedoubleyew. Vuoi aggiornare la risposta?
Dave Jarvis

9

Questo ha funzionato per me:

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

Esempio:

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

Produzione: mela-banana-frutta-


Esattamente quello di cui avevo bisogno, amico mio :)
GOXR3PLUS

7

Se cambierai una stringa molte volte, di solito è più efficiente usare StringBuilder (ma misura le tue prestazioni per scoprirlo) :

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

Ogni volta che esegui una sostituzione su una stringa, viene creato un nuovo oggetto String, perché le stringhe sono immutabili. StringBuilder è mutevole, cioè può essere modificato quanto vuoi.


Ho paura, non aiuta. Ogni volta che la sostituzione differisce dall'originale in lunghezza, è necessario un po 'di spostamento, che può essere più costoso rispetto alla ricostruzione della corda. Oppure mi sfugge qualcosa?
maaartinus

4

StringBuildereseguirà la sostituzione in modo più efficiente, poiché il buffer della matrice di caratteri può essere specificato alla lunghezza richiesta. StringBuilderè progettato per qualcosa di più che aggiungere!

Ovviamente la vera domanda è se questa è un'ottimizzazione troppo lontana? La JVM è molto brava a gestire la creazione di più oggetti e la successiva garbage collection, e come tutte le domande di ottimizzazione, la mia prima domanda è se hai misurato questo e determinato che è un problema.


2

Che ne dici di usare il metodo replaceAll () ?


4
Molte sottostringhe differenti possono essere gestite in una regex (/substring1|substring2|.../). Tutto dipende dal tipo di sostituzione che l'OP sta cercando di fare.
Avi

4
L'OP sta cercando qualcosa di più efficiente distr.replaceAll(search1, replace1).replaceAll(search2, replace2).replaceAll(search3, replace3).replaceAll(search4, replace4)
Kip

2

Rythm un motore di modelli java ora rilasciato con una nuova funzionalità chiamata modalità di interpolazione delle stringhe che ti consente di fare qualcosa come:

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

Il caso precedente mostra che puoi passare l'argomento al modello in base alla posizione. Rythm ti consente anche di passare argomenti per nome:

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

Nota Rythm è MOLTO VELOCE, da 2 a 3 volte più veloce di String.format e velocità, poiché compila il modello in java byte code, le prestazioni di runtime sono molto vicine alla concatenazione con StringBuilder.

Collegamenti:


Questa è una funzionalità molto vecchia disponibile con numerosi linguaggi di creazione di modelli come velocità e persino JSP. Inoltre non risponde alla domanda che non richiede che le stringhe di ricerca siano in alcun formato predefinito.
Angsuman Chakraborty

Interessante, la risposta accettata fornisce un esempio: il token separato "%cat% really needs some %beverage%."; non è %un formato predefinito? Il tuo primo punto è ancora più divertente, JDK fornisce molte "vecchie funzionalità", alcune iniziano dagli anni '90, perché le persone si preoccupano di usarle? I tuoi commenti e il tuo voto negativo non hanno alcun senso
Gelin Luo

Qual è il punto di introdurre il motore di modelli Rythm quando ci sono già molti motori di modelli preesistenti e ampiamente utilizzati come Velocity o Freemarker per l'avvio? Anche perché introdurre un altro prodotto quando le funzionalità principali di Java sono più che sufficienti. Dubito davvero della tua dichiarazione sulla performance perché anche Pattern può essere compilato. Mi piacerebbe vedere dei numeri reali.
Angsuman Chakraborty

Verde, stai perdendo il punto. L'interrogante vuole sostituire stringhe arbitrarie mentre la tua soluzione sostituirà solo stringhe in un formato predefinito come @ preceduto. Sì, l'esempio utilizza% ma solo come esempio, non come fattore limitante. Quindi la tua risposta non risponde alla domanda e quindi al punto negativo.
Angsuman Chakraborty

2

Quanto segue è basato sulla risposta di Todd Owen . Questa soluzione presenta il problema che se le sostituzioni contengono caratteri che hanno un significato speciale nelle espressioni regolari, è possibile ottenere risultati imprevisti. Volevo anche essere in grado di eseguire una ricerca senza distinzione tra maiuscole e minuscole. Ecco cosa mi è venuto in mente:

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

Ecco i miei casi di unit test:

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

1

Controllare questo:

String.format(str,STR[])

Per esempio:

String.format( "Put your %s where your %s is", "money", "mouth" );

0

Riepilogo: implementazione di una singola classe della risposta di Dave, per scegliere automaticamente il più efficiente dei due algoritmi.

Questa è un'implementazione completa di una singola classe basata sull'eccellente risposta di Dave Jarvis . La classe sceglie automaticamente tra i due diversi algoritmi forniti, per la massima efficienza. (Questa risposta è per le persone che vorrebbero semplicemente copiare e incollare rapidamente.)

Classe ReplaceStrings:

package somepackage

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.ahocorasick.trie.Emit;
import org.ahocorasick.trie.Trie;
import org.ahocorasick.trie.Trie.TrieBuilder;
import org.apache.commons.lang3.StringUtils;

/**
 * ReplaceStrings, This class is used to replace multiple strings in a section of text, with high
 * time efficiency. The chosen algorithms were adapted from: https://stackoverflow.com/a/40836618
 */
public final class ReplaceStrings {

    /**
     * replace, This replaces multiple strings in a section of text, according to the supplied
     * search and replace definitions. For maximum efficiency, this will automatically choose
     * between two possible replacement algorithms.
     *
     * Performance note: If it is known in advance that the source text is long, then this method
     * signature has a very small additional performance advantage over the other method signature.
     * (Although either method signature will still choose the best algorithm.)
     */
    public static String replace(
        final String sourceText, final Map<String, String> searchReplaceDefinitions) {
        final boolean useLongAlgorithm
            = (sourceText.length() > 1000 || searchReplaceDefinitions.size() > 25);
        if (useLongAlgorithm) {
            // No parameter adaptations are needed for the long algorithm.
            return replaceUsing_AhoCorasickAlgorithm(sourceText, searchReplaceDefinitions);
        } else {
            // Create search and replace arrays, which are needed by the short algorithm.
            final ArrayList<String> searchList = new ArrayList<>();
            final ArrayList<String> replaceList = new ArrayList<>();
            final Set<Map.Entry<String, String>> allEntries = searchReplaceDefinitions.entrySet();
            for (Map.Entry<String, String> entry : allEntries) {
                searchList.add(entry.getKey());
                replaceList.add(entry.getValue());
            }
            return replaceUsing_StringUtilsAlgorithm(sourceText, searchList, replaceList);
        }
    }

    /**
     * replace, This replaces multiple strings in a section of text, according to the supplied
     * search strings and replacement strings. For maximum efficiency, this will automatically
     * choose between two possible replacement algorithms.
     *
     * Performance note: If it is known in advance that the source text is short, then this method
     * signature has a very small additional performance advantage over the other method signature.
     * (Although either method signature will still choose the best algorithm.)
     */
    public static String replace(final String sourceText,
        final ArrayList<String> searchList, final ArrayList<String> replacementList) {
        if (searchList.size() != replacementList.size()) {
            throw new RuntimeException("ReplaceStrings.replace(), "
                + "The search list and the replacement list must be the same size.");
        }
        final boolean useLongAlgorithm = (sourceText.length() > 1000 || searchList.size() > 25);
        if (useLongAlgorithm) {
            // Create a definitions map, which is needed by the long algorithm.
            HashMap<String, String> definitions = new HashMap<>();
            final int searchListLength = searchList.size();
            for (int index = 0; index < searchListLength; ++index) {
                definitions.put(searchList.get(index), replacementList.get(index));
            }
            return replaceUsing_AhoCorasickAlgorithm(sourceText, definitions);
        } else {
            // No parameter adaptations are needed for the short algorithm.
            return replaceUsing_StringUtilsAlgorithm(sourceText, searchList, replacementList);
        }
    }

    /**
     * replaceUsing_StringUtilsAlgorithm, This is a string replacement algorithm that is most
     * efficient for sourceText under 1000 characters, and less than 25 search strings.
     */
    private static String replaceUsing_StringUtilsAlgorithm(final String sourceText,
        final ArrayList<String> searchList, final ArrayList<String> replacementList) {
        final String[] searchArray = searchList.toArray(new String[]{});
        final String[] replacementArray = replacementList.toArray(new String[]{});
        return StringUtils.replaceEach(sourceText, searchArray, replacementArray);
    }

    /**
     * replaceUsing_AhoCorasickAlgorithm, This is a string replacement algorithm that is most
     * efficient for sourceText over 1000 characters, or large lists of search strings.
     */
    private static String replaceUsing_AhoCorasickAlgorithm(final String sourceText,
        final Map<String, String> searchReplaceDefinitions) {
        // Create a buffer sufficiently large that re-allocations are minimized.
        final StringBuilder sb = new StringBuilder(sourceText.length() << 1);
        final TrieBuilder builder = Trie.builder();
        builder.onlyWholeWords();
        builder.ignoreOverlaps();
        for (final String key : searchReplaceDefinitions.keySet()) {
            builder.addKeyword(key);
        }
        final Trie trie = builder.build();
        final Collection<Emit> emits = trie.parseText(sourceText);
        int prevIndex = 0;
        for (final Emit emit : emits) {
            final int matchIndex = emit.getStart();

            sb.append(sourceText.substring(prevIndex, matchIndex));
            sb.append(searchReplaceDefinitions.get(emit.getKeyword()));
            prevIndex = emit.getEnd() + 1;
        }
        // Add the remainder of the string (contains no more matches).
        sb.append(sourceText.substring(prevIndex));
        return sb.toString();
    }

    /**
     * main, This contains some test and example code.
     */
    public static void main(String[] args) {
        String shortSource = "The quick brown fox jumped over something. ";
        StringBuilder longSourceBuilder = new StringBuilder();
        for (int i = 0; i < 50; ++i) {
            longSourceBuilder.append(shortSource);
        }
        String longSource = longSourceBuilder.toString();
        HashMap<String, String> searchReplaceMap = new HashMap<>();
        ArrayList<String> searchList = new ArrayList<>();
        ArrayList<String> replaceList = new ArrayList<>();
        searchReplaceMap.put("fox", "grasshopper");
        searchReplaceMap.put("something", "the mountain");
        searchList.add("fox");
        replaceList.add("grasshopper");
        searchList.add("something");
        replaceList.add("the mountain");
        String shortResultUsingArrays = replace(shortSource, searchList, replaceList);
        String shortResultUsingMap = replace(shortSource, searchReplaceMap);
        String longResultUsingArrays = replace(longSource, searchList, replaceList);
        String longResultUsingMap = replace(longSource, searchReplaceMap);
        System.out.println(shortResultUsingArrays);
        System.out.println("----------------------------------------------");
        System.out.println(shortResultUsingMap);
        System.out.println("----------------------------------------------");
        System.out.println(longResultUsingArrays);
        System.out.println("----------------------------------------------");
        System.out.println(longResultUsingMap);
        System.out.println("----------------------------------------------");
    }
}

Dipendenze Maven necessarie:

(Aggiungili al tuo file pom se necessario.)

    <!-- Apache Commons utilities. Super commonly used utilities.
    https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.10</version>
    </dependency>

    <!-- ahocorasick, An algorithm used for efficient searching and 
    replacing of multiple strings.
    https://mvnrepository.com/artifact/org.ahocorasick/ahocorasick -->
    <dependency>
        <groupId>org.ahocorasick</groupId>
        <artifactId>ahocorasick</artifactId>
        <version>0.4.0</version>
    </dependency>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.