Java Замена нескольких разных подстрок в строке сразу (или наиболее эффективным способом)


97

Мне нужно заменить много разных подстрок в строке наиболее эффективным способом. есть ли другой способ, кроме метода грубой силы для замены каждого поля с помощью string.replace?

Ответы:


102

Если строка, с которой вы работаете, очень длинная или вы работаете со многими строками, может быть целесообразно использовать java.util.regex.Matcher (для этого требуется время предварительной компиляции, поэтому он не будет эффективным если ваш ввод очень мал или ваш шаблон поиска часто меняется).

Ниже приведен полный пример, основанный на списке токенов, взятых с карты. (Использует StringUtils из 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());

Как только регулярное выражение скомпилировано, сканирование входной строки обычно происходит очень быстро (хотя, если ваше регулярное выражение является сложным или включает в себя отслеживание с возвратом, вам все равно потребуется выполнить тест, чтобы подтвердить это!)


1
Да, необходимо провести сравнительный анализ количества итераций.
techzen

5
Я думаю, вам следует избегать специальных символов в каждом токене, прежде чем делать это"%(" + StringUtils.join(tokens.keySet(), "|") + ")%";
Разработчик Мариус Жиленас,

Обратите внимание, что для большей скорости можно использовать StringBuilder. StringBuilder не синхронизируется. edit whoops работает только с java 9
Tinus Tate

3
Будущее средство чтения: для регулярного выражения "(" и ")" будут заключать группу для поиска. «%» Считается буквальным в тексте. Если ваши термины не начинаются и не заканчиваются на "%", они не будут найдены. Поэтому настройте префиксы и суффиксы в обеих частях (текст + код).
linuxunil

66

Алгоритм

Одним из наиболее эффективных способов замены совпадающих строк (без регулярных выражений) является использование алгоритма Aho-Corasick с эффективным Trie (произносится как «попытка»), алгоритмом быстрого хеширования и эффективной реализацией коллекций .

Простой код

Простое решение использует 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: 1000

Использование 10000 символов и 1000 совпадающих строк для замены:

  • testStringUtils: 1 секунда, 1402 миллисекунды
  • testBorAhoCorasick: 0 секунд, 37 миллисекунд

Разделение закрывается.

1000: 10

Использование 1000 символов и 10 совпадающих строк для замены:

  • testStringUtils: 0 секунд, 7 миллисекунд
  • testBorAhoCorasick: 0 секунд, 19 миллисекунд

Для коротких струн накладные расходы на настройку Aho-Corasick затмевают грубую силу StringUtils.replaceEach.

Возможен гибридный подход, основанный на длине текста, чтобы получить лучшее от обеих реализаций.

Реализации

Рассмотрите возможность сравнения других реализаций для текста размером более 1 МБ, в том числе:

Статьи

Документы и информация по алгоритму:


5
Престижность за обновление этого вопроса новой ценной информацией, это очень приятно. Я думаю, что тест JMH по-прежнему уместен, по крайней мере, для разумных значений, таких как 10 000: 1000 и 1000: 10 (иногда JIT может выполнять волшебную оптимизацию).
Tunaki

удалите builder.onlyWholeWords (), и он будет работать аналогично замене строки.
Ондрей Сотолар

Большое спасибо за отличный ответ. Это определенно очень полезно! Я просто хотел прокомментировать, что для сравнения двух подходов, а также для того, чтобы дать более значимый пример, нужно построить Trie только один раз во втором подходе и применить его ко многим различным входным строкам. Я думаю, что это главное преимущество доступа к Trie по сравнению с StringUtils: вы создаете его только один раз. Тем не менее, большое спасибо за этот ответ. Он очень хорошо разделяет методологию реализации второго подхода
Vic Seedoubleyew

Отличный момент, @VicSeedoubleyew. Хотите обновить ответ?
Дэйв Джарвис,

9

Это сработало для меня:

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

Пример:

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

Продукт: яблоко-банан-фрукт


Именно то, что мне было нужно, мой друг :)
GOXR3PLUS

7

Если вы собираетесь менять 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();

Каждый раз, когда вы выполняете замену String, создается новый объект String, потому что строки неизменяемы. StringBuilder является изменяемым, то есть его можно изменять сколько угодно.


Боюсь, это не поможет. Всякий раз, когда замена отличается от оригинала по длине, вам потребуется некоторое смещение, что может оказаться более дорогостоящим, чем создание новой строки. Или я что-то упускаю?
maaartinus

4

StringBuilderвыполнит замену более эффективно, так как его буфер массива символов может быть указан до необходимой длины. StringBuilderпредназначен не только для добавления!

Конечно, вопрос в том, не заходит ли это оптимизация слишком далеко? JVM очень хорошо справляется с созданием нескольких объектов и последующей сборкой мусора, и, как и все вопросы оптимизации, мой первый вопрос заключается в том, измерили ли вы это и определили ли вы, что это проблема.


2

Как насчет использования метода replaceAll () ?


4
В регулярном выражении можно обрабатывать множество различных подстрок (/substring1|substring2|.../). Все зависит от того, какую замену пытается сделать OP.
Avi

4
OP ищет что-то более эффективное, чемstr.replaceAll(search1, replace1).replaceAll(search2, replace2).replaceAll(search3, replace3).replaceAll(search4, replace4)
Кип

2

Rythm, механизм шаблонов 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 ОЧЕНЬ БЫСТРЫЙ, примерно в 2–3 раза быстрее, чем String.format и скорость, поскольку он компилирует шаблон в байт-код Java, производительность во время выполнения очень близка к объединению с StringBuilder.

Ссылки:


Это очень старая возможность, доступная во многих языках шаблонов, таких как скорость, даже JSP. Также он не отвечает на вопрос, который не требует, чтобы строки поиска были в каком-либо заранее определенном формате.
Ангсуман Чакраборти

Интересно, что принятый ответ представляет собой пример: "%cat% really needs some %beverage%."; не является ли этот %разделенный токен заранее определенным форматом? Ваше первое замечание еще более забавно, JDK предоставляет множество «старых возможностей», некоторые из них начинаются с 90-х годов, зачем людям их использовать? Ваши комментарии и голосование против не имеют никакого реального смысла
Гелин Ло

Какой смысл вводить шаблонный движок Rythm, когда уже существует много уже существующих шаблонных движков, которые широко используются, например, Velocity или Freemarker для загрузки? Также зачем вводить еще один продукт, если основных функций Java более чем достаточно. Я действительно сомневаюсь в вашем утверждении о производительности, потому что Pattern также можно скомпилировать. Хотелось бы увидеть реальные цифры.
Ангсуман Чакраборти,

Грин, вы упускаете суть. Запрашивающий хочет заменить произвольные строки, тогда как ваше решение заменит только строки в предопределенном формате, например @. Да, в примере% используется только как пример, а не как ограничивающий фактор. Итак, ваш ответ не дает ответа на вопрос и, следовательно, отрицательный момент.
Ангсуман Чакраборти,

2

Приведенное ниже основано на ответе Тодда Оуэна . У этого решения есть проблема, заключающаяся в том, что если замены содержат символы, которые имеют особое значение в регулярных выражениях, вы можете получить неожиданные результаты. Я также хотел иметь возможность выполнять поиск без учета регистра. Вот что я придумал:

/**
 * 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
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

Проверь это:

String.format(str,STR[])

Например:

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

0

Описание: Одноклассная реализация ответа Дэйва для автоматического выбора наиболее эффективного из двух алгоритмов.

Это полная реализация одного класса, основанная на приведенном выше отличном ответе Дэйва Джарвиса. . Класс автоматически выбирает между двумя различными предоставленными алгоритмами для максимальной эффективности. (Этот ответ предназначен для людей, которые просто хотят быстро скопировать и вставить.)

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

Необходимые зависимости Maven:

(При необходимости добавьте их в свой файл pom.)

    <!-- 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>
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.