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 не синхронізовано. редагувати 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 );
  }

Це уповільнює роботу з великими текстами.

Швидкий код

Реалізація Бором алгоритму Ахо-Корасіка вносить трохи більше складності, що стає деталлю реалізації за допомогою фасаду з тим самим підписом методу:

  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

Простий мікро-орієнтир із 1000000 символів та 1000 випадково розміщених рядків для заміни.

  • testStringUtils: 25 секунд, 25533 мілісекунди
  • testBorAhoCorasick: 0 секунд, 68 мілісів

Немає конкурсу.

10 000: 1 000

Використання 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: 1 000 та 1 000: 10 (JIT може іноді робити магічну оптимізацію).
Tunaki

видаліть builder.onlyWholeWords (), і він буде працювати аналогічно заміні рядків.
Ондрей Сотолар,

Щиро дякую за цю чудову відповідь. Це, безумовно, дуже корисно! Я просто хотів прокоментувати, що для порівняння двох підходів, а також для надання більш значущого прикладу, слід побудувати Trie лише один раз у другому підході і застосовувати його до багатьох різних вхідних рядків. Я думаю, це головна перевага доступу до Trie проти StringUtils: ви створюєте його лише один раз. Тим не менше, дякую вам за цю відповідь. Він дуже добре поділяє методологію реалізації другого підходу
Віка Седублеєва,

Чудовий момент, @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

Якщо ви збираєтеся багато разів змінювати рядок, то зазвичай ефективніше використовувати 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, оскільки рядки незмінні. StringBuilder можна змінювати, тобто його можна змінювати скільки завгодно.


Боюсь, це не допомагає. Кожного разу, коли заміна відрізняється від оригіналу довжиною, вам знадобиться певна зміна, яка може бути дорожчою, ніж побудова рядка заново. Або мені чогось не вистачає?
maaartinus

4

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

Звичайно, справжнє питання полягає в тому, чи це далеко не оптимізація? JVM дуже добре справляється зі створенням декількох об'єктів та подальшим збиранням сміття, і, як і всі запитання щодо оптимізації, моє перше запитання - чи виміряли ви це і визначили, що це проблема.


2

Як щодо використання методу replaceAll () ?


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

4
ОП шукає щось більш ефективне, ніжstr.replaceAll(search1, replace1).replaceAll(search2, replace2).replaceAll(search3, replace3).replaceAll(search4, replace4)
Кіп,

2

Зараз випущений механізм шаблонів Java Rythm з новою функцією, яка називається режим інтерполяції рядків, що дозволяє робити щось на зразок:

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. Крім того, він не відповідає на запитання, для якого не потрібно, щоб пошукові рядки були у будь-якому заздалегідь визначеному форматі.
Angsuman Chakraborty

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

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

Зелений, ти пропускаєш суть. Допитувач хоче замінити довільні рядки, тоді як ваше рішення замінить лише рядки у заздалегідь визначеному форматі, як @ попередній. Так, у прикладі використовується%, але лише як приклад, а не як обмежуючий фактор. Отже, ви не відповідаєте на питання і, отже, негативний момент.
Angsuman Chakraborty

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.