Java Remplacement de plusieurs sous-chaînes différentes dans une chaîne à la fois (ou de la manière la plus efficace)


97

J'ai besoin de remplacer de nombreuses sous-chaînes différentes dans une chaîne de la manière la plus efficace. y a-t-il un autre moyen, autre que celui de la force brute, de remplacer chaque champ en utilisant string.replace?

Réponses:


102

Si la chaîne sur laquelle vous travaillez est très longue ou si vous utilisez de nombreuses chaînes, il peut être intéressant d'utiliser un java.util.regex.Matcher (cela demande du temps à l'avance pour la compilation, donc ce ne sera pas efficace si votre entrée est très petite ou votre modèle de recherche change fréquemment).

Vous trouverez ci-dessous un exemple complet, basé sur une liste de jetons tirés d'une carte. (Utilise StringUtils d'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());

Une fois l'expression régulière compilée, l'analyse de la chaîne d'entrée est généralement très rapide (bien que si votre expression régulière est complexe ou implique un retour en arrière, vous devrez toujours effectuer un benchmark pour le confirmer!)


1
Oui, doit être comparé au nombre d'itérations.
techzen

5
Je pense que vous devriez échapper aux caractères spéciaux de chaque jeton avant de le faire"%(" + StringUtils.join(tokens.keySet(), "|") + ")%";
Développeur Marius Žilėnas

Notez que l'on peut utiliser StringBuilder pour un peu plus de vitesse. StringBuilder n'est pas synchronisé. edit whoops ne fonctionne qu'avec java 9
Tinus Tate

3
Futur lecteur: pour regex, "(" et ")" englobera le groupe à rechercher. Le "%" compte comme un littéral dans le texte. Si vos termes ne commencent ET ne se terminent pas par "%", ils ne seront pas trouvés. Ajustez donc les préfixes et suffixes sur les deux parties (texte + code).
linuxunil

66

Algorithme

L'un des moyens les plus efficaces de remplacer les chaînes correspondantes (sans expressions régulières) consiste à utiliser l' algorithme Aho-Corasick avec un Trie performant (prononcé "try"), un algorithme de hachage rapide et une implémentation efficace des collections .

Code simple

Une solution simple tire parti d'Apache StringUtils.replaceEachcomme suit:

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

Cela ralentit sur les gros textes.

Code rapide

L'implémentation par Bor de l'algorithme Aho-Corasick introduit un peu plus de complexité qui devient un détail d'implémentation en utilisant une façade avec la même signature de méthode:

  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

Pour les benchmarks, le tampon a été créé en utilisant randomNumeric comme suit:

  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_DIVISORdicte le nombre de variables à injecter:

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

Le code de référence lui-même ( JMH semblait exagéré):

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 micro-benchmark simple avec 1 000 000 de caractères et 1 000 chaînes placées au hasard à remplacer.

  • testStringUtils: 25 secondes, 25533 millis
  • testBorAhoCorasick: 0 seconde, 68 millis

Pas de compétition.

10 000: 1 000

Utilisation de 10 000 caractères et 1 000 chaînes correspondantes pour remplacer:

  • testStringUtils: 1 seconde, 1402 millis
  • testBorAhoCorasick: 0 seconde, 37 millisecondes

Le fossé se referme.

1 000: 10

Utilisation de 1000 caractères et 10 chaînes correspondantes pour remplacer:

  • testStringUtils: 0 seconde, 7 millisecondes
  • testBorAhoCorasick: 0 seconde, 19 millisecondes

Pour les chaînes courtes, la surcharge de configuration d'Aho-Corasick éclipse l'approche de la force brute de StringUtils.replaceEach.

Une approche hybride basée sur la longueur du texte est possible, pour tirer le meilleur parti des deux implémentations.

Implémentations

Envisagez de comparer d'autres implémentations pour du texte de plus de 1 Mo, notamment:

Papiers

Articles et informations relatifs à l'algorithme:


5
Félicitations pour la mise à jour de cette question avec de nouvelles informations précieuses, c'est très bien. Je pense qu'un benchmark JMH est toujours approprié, au moins pour des valeurs raisonnables comme 10 000: 1 000 et 1 000: 10 (le JIT peut parfois faire des optimisations magiques).
Tunaki

supprimez le builder.onlyWholeWords () et il fonctionnera de la même manière que le remplacement de chaîne.
Ondrej Sotolar

Merci beaucoup pour cette excellente réponse. C'est vraiment très utile! Je voulais juste commenter que pour comparer les deux approches, et aussi pour donner un exemple plus significatif, on ne devrait construire le Trie qu'une seule fois dans la seconde approche et l'appliquer à de nombreuses chaînes d'entrée différentes. Je pense que c'est le principal avantage d'avoir accès au Trie par rapport à StringUtils: vous ne le construisez qu'une seule fois. Encore merci beaucoup pour cette réponse. Il partage très bien la méthodologie pour mettre en œuvre la deuxième approche
Vic Seedoubleyew

Un excellent point, @VicSeedoubleyew. Voulez-vous mettre à jour la réponse?
Dave Jarvis

9

Cela a fonctionné pour moi:

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

Exemple:

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

Sortie: pomme-banane-frui-


Exactement ce dont j'avais besoin mon ami :)
GOXR3PLUS

7

Si vous allez changer une chaîne plusieurs fois, il est généralement plus efficace d'utiliser un StringBuilder (mais mesurez vos performances pour le savoir) :

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

Chaque fois que vous effectuez un remplacement sur une chaîne, un nouvel objet String est créé, car les chaînes sont immuables. StringBuilder est modifiable, c'est-à-dire qu'il peut être modifié autant que vous le souhaitez.


J'ai peur, ça n'aide pas. Chaque fois que le remplacement diffère de l'original en longueur, vous aurez besoin d'un décalage, ce qui peut être plus coûteux que de reconstruire la chaîne. Ou est-ce que je manque quelque chose?
maaartinus

4

StringBuildereffectuera le remplacement plus efficacement, puisque son tampon de tableau de caractères peut être spécifié à une longueur requise. StringBuilderest conçu pour plus que l'ajout!

Bien sûr, la vraie question est de savoir si c'est une optimisation trop loin? La JVM est très efficace pour gérer la création de plusieurs objets et le ramasse-miettes qui s'ensuit, et comme toutes les questions d'optimisation, ma première question est de savoir si vous avez mesuré cela et déterminé que c'est un problème.


2

Que diriez-vous d'utiliser la méthode replaceAll () ?


4
De nombreuses sous-chaînes différentes peuvent être gérées dans une expression régulière (/substring1|substring2|.../). Tout dépend du type de remplacement que le PO tente de faire.
Avi

4
L'OP cherche quelque chose de plus efficace questr.replaceAll(search1, replace1).replaceAll(search2, replace2).replaceAll(search3, replace3).replaceAll(search4, replace4)
Kip

2

Rythm un moteur de template java maintenant publié avec une nouvelle fonctionnalité appelée mode d'interpolation String qui vous permet de faire quelque chose comme:

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

Le cas ci-dessus montre que vous pouvez passer l'argument au modèle par position. Rythm vous permet également de passer des arguments par nom:

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

Remarque Rythm est TRÈS RAPIDE, environ 2 à 3 fois plus rapide que String.format et vélocité, car il compile le modèle en code octet java, les performances d'exécution sont très proches de la concatentation avec StringBuilder.

Liens:


C'est une capacité très très ancienne disponible avec de nombreux langages de création de modèles tels que la vitesse, JSP même. De plus, il ne répond pas à la question qui n'exige pas que les chaînes de recherche soient dans un format prédéfini.
Angsuman Chakraborty

Intéressant, la réponse acceptée fournit un exemple :, ce jeton séparé "%cat% really needs some %beverage%."; n'est-il pas %un format prédéfini? Votre premier point est encore plus drôle, JDK fournit un tas de "vieilles capacités", certaines commencent à partir des années 90, pourquoi les gens se donnent-ils la peine de les utiliser? Vos commentaires et votre vote négatif n'ont aucun sens
Gelin Luo

Quel est l'intérêt d'introduire le moteur de modèles Rythm alors qu'il existe déjà de nombreux moteurs de modèles préexistants et largement utilisés comme Velocity ou Freemarker pour démarrer? Aussi pourquoi introduire un autre produit alors que les fonctionnalités Java de base suffisent largement. Je doute vraiment de votre déclaration sur les performances car Pattern peut également être compilé. J'adorerais voir des chiffres réels.
Angsuman Chakraborty

Vert, vous manquez le point. Le questionneur souhaite remplacer des chaînes arbitraires alors que votre solution ne remplacera que les chaînes au format prédéfini comme @ précédé. Oui, l'exemple utilise% mais uniquement à titre d'exemple, pas comme facteur limitant. Donc, votre réponse ne répond pas à la question et donc au point négatif.
Angsuman Chakraborty

2

Ce qui suit est basé sur la réponse de Todd Owen . Cette solution pose le problème que si les remplacements contiennent des caractères qui ont une signification particulière dans les expressions régulières, vous pouvez obtenir des résultats inattendus. Je voulais également pouvoir éventuellement faire une recherche insensible à la casse. Voici ce que j'ai trouvé:

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

Voici mes cas de test unitaires:

@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

Vérifie ça:

String.format(str,STR[])

Par exemple:

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

0

Résumé: Implémentation de classe unique de la réponse de Dave, pour choisir automatiquement le plus efficace des deux algorithmes.

Il s'agit d'une implémentation complète et unique basée sur l'excellente réponse ci-dessus de Dave Jarvis . La classe choisit automatiquement entre les deux différents algorithmes fournis, pour une efficacité maximale. (Cette réponse s'adresse aux personnes qui souhaitent simplement copier et coller rapidement.)

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

Dépendances Maven nécessaires:

(Ajoutez-les à votre fichier pom si nécessaire.)

    <!-- 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>
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.