web-dev-qa-db-fra.com

Java Remplacement de plusieurs sous-chaînes dans une chaîne de temps

Je dois remplacer beaucoup de sous-chaînes différentes dans une chaîne de la manière la plus efficace. existe-t-il un autre moyen que le moyen par force brute de remplacer chaque champ à l'aide de string.replace? 

80
Yossale

Si la chaîne sur laquelle vous travaillez est très longue, ou si vous utilisez beaucoup de chaînes, il pourrait être intéressant d’utiliser un fichier Java.util.regex.Matcher (cela nécessite du temps de compilation à l’avance, il ne sera donc pas efficace si votre saisie est très petite ou si 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 que l'expression régulière est 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 soit complexe ou implique un retour en arrière, vous devez toujours effectuer une analyse comparative pour le confirmer!)

88
Todd Owen

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 exécutant Trie (prononcé "try"), fast hachage , et efficace collections mise en oeuvre.

Code simple

Peut-être que le code le plus simple à écrire exploite le code StringUtils.replaceEach d’Apache comme 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 les gros textes.

Code rapide

L'implémentation de 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();
  }

Des repères

Pour les tests, 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_DIVISOR indique 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 excessif):

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 simple micro-repère 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

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

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

La fracture se ferme.

1000: 10

En utilisant 1 000 caractères et 10 chaînes correspondantes pour remplacer:

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

Pour les chaînes courtes, la surcharge de la configuration d’Aho-Corasick éclipse l’approche en 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

Pensez à comparer d'autres implémentations pour un texte d'une longueur supérieure à 1 Mo, notamment:

Papiers

Articles et informations relatifs à l'algorithme:

40
Dave Jarvis

Si vous allez changer plusieurs fois une chaîne, 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.

7
Steve McLeod

StringBuilder effectuera le remplacement plus efficacement, car son tampon de tableau de caractères peut être spécifié à la longueur requise .StringBuilder est conçu pour plus que l'ajout!

Bien sûr, la vraie question est de savoir s’il s’agit d’une optimisation trop poussée. La machine virtuelle Java gère très bien la création de plusieurs objets et la collecte de place suivante. 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.

4
Brian Agnew

Rythmez un moteur de template Java avec une nouvelle fonctionnalité appelée Mode d'interpolation de chaîne 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 transmettre un 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 TRES FAST, 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 concaténation avec StringBuilder.

Liens:

2
Gelin Luo

Cela a fonctionné pour moi:

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

Exemple:

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

Résultat: Apple-banana-friui-

2
Bikram Pandit

Pourquoi ne pas utiliser la méthode replaceAll () ?

2
Avi

Vérifie ça:

String.format (str, STR [])

...

Par exemple:

String.format ("Mettez votre% s où votre% s est", "argent", "bouche");

1
Ali

Ce qui suit est basé sur La réponse de Todd Owen . Cette solution pose le problème suivant: si les remplacements contiennent des caractères ayant une signification particulière dans les expressions régulières, vous pouvez obtenir des résultats inattendus. Je voulais aussi pouvoir éventuellement faire une recherche insensible à la casse. Voici ce que je suis venu avec:

/**
 * 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 tests 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;
}
0
Kip
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