Как заменить набор токенов в строке Java?

106

У меня есть следующий шаблон строки: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

У меня также есть строковые переменные для имени, номера счета и срока оплаты - как лучше всего заменить токены в шаблоне переменными?

(Обратите внимание, что если переменная содержит токен, ее НЕ следует заменять).


РЕДАКТИРОВАТЬ

Благодаря @laginimaineb и @ alan-moore, вот мое решение:

public static String replaceTokens(String text, 
                                   Map<String, String> replacements) {
    Pattern pattern = Pattern.compile("\\[(.+?)\\]");
    Matcher matcher = pattern.matcher(text);
    StringBuffer buffer = new StringBuffer();

    while (matcher.find()) {
        String replacement = replacements.get(matcher.group(1));
        if (replacement != null) {
            // matcher.appendReplacement(buffer, replacement);
            // see comment 
            matcher.appendReplacement(buffer, "");
            buffer.append(replacement);
        }
    }
    matcher.appendTail(buffer);
    return buffer.toString();
}
отметка
источник
Однако следует отметить, что StringBuffer - это то же самое, что StringBuilder, только что синхронизированный. Однако, поскольку в этом примере вам не нужно синхронизировать построение String, вам может быть лучше использовать StringBuilder (даже если получение блокировок - это операция с почти нулевой стоимостью).
laginimaineb 06
1
К сожалению, в этом случае вам придется использовать StringBuffer; это то, что ожидают методы appendXXX (). Они существуют со времен Java 4, а StringBuilder не был добавлен до Java 5. Однако, как вы сказали, в этом нет ничего страшного, просто раздражает.
Алан Мур
4
Еще одна вещь: appendReplacement (), как и методы replaceXXX (), ищет ссылки на группы захвата, такие как $ 1, $ 2 и т. Д., И заменяет их текстом из связанных групп захвата. Если ваш замещающий текст может содержать знаки доллара или обратную косую черту (которые используются для экранирования знаков доллара), у вас может быть проблема. Самый простой способ справиться с этим - разбить операцию добавления на два этапа, как я сделал в приведенном выше коде.
Алан Мур,
Алан - очень впечатлен, что ты это заметил. Я не думал, что такую ​​простую проблему будет так сложно решить!
Марк
используйте java.sun.com/javase/6/docs/api/java/util/regex/…
Джейсон С.

Ответы:

65

Наиболее эффективным способом было бы использовать средство сопоставления, чтобы постоянно находить выражения и заменять их, а затем добавлять текст в построитель строк:

Pattern pattern = Pattern.compile("\\[(.+?)\\]");
Matcher matcher = pattern.matcher(text);
HashMap<String,String> replacements = new HashMap<String,String>();
//populate the replacements map ...
StringBuilder builder = new StringBuilder();
int i = 0;
while (matcher.find()) {
    String replacement = replacements.get(matcher.group(1));
    builder.append(text.substring(i, matcher.start()));
    if (replacement == null)
        builder.append(matcher.group(0));
    else
        builder.append(replacement);
    i = matcher.end();
}
builder.append(text.substring(i, text.length()));
return builder.toString();
лагинимайнеб
источник
10
Вот как я бы это сделал, за исключением того, что я бы использовал методы Matcher appendReplacement () и appendTail () для копирования несопоставленного текста; нет необходимости делать это вручную.
Алан Мур,
5
На самом деле методы appendReplacement () и appentTail () требуют StringBuffer, который синхронизируется (что здесь бесполезно). В данном ответе используется StringBuilder, который в моих тестах на 20% быстрее.
dube
103

Я действительно не думаю, что вам нужно использовать для этого шаблонизатор или что-то подобное. Вы можете использовать этот String.formatметод так:

String template = "Hello %s Please find attached %s which is due on %s";

String message = String.format(template, name, invoiceNumber, dueDate);
Пол Мори
источник
4
Одним из недостатков этого является то, что вы должны указывать параметры в правильном порядке.
указывать
Другой заключается в том, что вы не можете указать собственный формат замещающего токена.
Franz D.
другое заключается в том, что он не работает динамически, имея возможность иметь набор данных ключей / значений, а затем применять его к любой строке,
Брэд Паркс,
43

К сожалению, удобный метод String.format, упомянутый выше, доступен только начиная с Java 1.5 (который в настоящее время должен быть довольно стандартным, но вы никогда не узнаете). Вместо этого вы также можете использовать Java- класс MessageFormat для замены заполнителей.

Он поддерживает заполнители в форме '{number}', поэтому ваше сообщение будет выглядеть как «Здравствуйте, {0}, пожалуйста, найдите прикрепленный {1}, который должен быть отправлен {2}». Эти строки можно легко перенести во внешний вид с помощью ResourceBundles (например, для локализации с несколькими языковыми стандартами). Замена будет производиться с использованием метода static'format класса MessageFormat:

String msg = "Hello {0} Please find attached {1} which is due on {2}";
String[] values = {
  "John Doe", "invoice #123", "2009-06-30"
};
System.out.println(MessageFormat.format(msg, values));
Инструментарий
источник
3
Я не мог вспомнить название MessageFormat, и глупо, сколько времени пришлось потратить на поиск в Google, чтобы найти хотя бы этот ответ. Все ведут себя так, будто это либо String.format, либо сторонний, забывая об этой невероятно полезной утилите.
Патрик
1
Это доступно с 2004 года - почему я только узнал об этом сейчас, в 2017 году? Я реорганизовал код, описанный в StringBuilder.append()s, и подумал: «Конечно, есть способ получше ... что-то более питоническое ...» - и черт возьми, я думаю, этот метод может предшествовать методам форматирования Python. На самом деле ... это может быть старше 2002 года ... Я не могу найти, когда это на самом деле появилось ...
ArtOfWarfare
42

Вы можете попробовать использовать библиотеку шаблонов, например Apache Velocity.

http://velocity.apache.org/

Вот пример:

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.StringWriter;

public class TemplateExample {
    public static void main(String args[]) throws Exception {
        Velocity.init();

        VelocityContext context = new VelocityContext();
        context.put("name", "Mark");
        context.put("invoiceNumber", "42123");
        context.put("dueDate", "June 6, 2009");

        String template = "Hello $name. Please find attached invoice" +
                          " $invoiceNumber which is due on $dueDate.";
        StringWriter writer = new StringWriter();
        Velocity.evaluate(context, writer, "TemplateName", template);

        System.out.println(writer);
    }
}

Результатом будет:

Привет, Марк. Вы можете найти прилагаемый счет 42123, который необходимо оплатить 6 июня 2009 г.
Hallidave
источник
Раньше я использовал скорость. Прекрасно работает.
Hardwareguy
4
согласен, зачем изобретать велосипед
возражает
6
Использование целой библиотеки для такой простой задачи - это немного излишне. Velocity имеет много других функций, и я твердо уверен, что они не подходят для такой простой задачи.
Андрей Чобану
24

Вы можете использовать библиотеку шаблонов для замены сложных шаблонов.

FreeMarker - очень хороший выбор.

http://freemarker.sourceforge.net/

Но для простой задачи вам может помочь простой служебный класс.

org.apache.commons.lang3.text.StrSubstitutor

Он очень мощный, настраиваемый и простой в использовании.

Этот класс берет кусок текста и заменяет все переменные в нем. Определение переменной по умолчанию - $ {variableName}. Префикс и суффикс можно изменить с помощью конструкторов и методов set.

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

Например, если вы хотите заменить системную переменную среды в строку шаблона, вот код:

public class SysEnvSubstitutor {
    public static final String replace(final String source) {
        StrSubstitutor strSubstitutor = new StrSubstitutor(
                new StrLookup<Object>() {
                    @Override
                    public String lookup(final String key) {
                        return System.getenv(key);
                    }
                });
        return strSubstitutor.replace(source);
    }
}
Ли Инь
источник
2
org.apache.commons.lang3.text.StrSubstitutor отлично поработал для меня
ps0604,
18
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Вывод: Здравствуйте, присоединяйтесь! У вас 10 сообщений "

user2845137
источник
3
Джон явно проверяет свои сообщения так же часто, как я проверяю свою папку «спам», учитывая, что она длинная.
Hemmels
9

Это зависит от того, где находятся фактические данные, которые вы хотите заменить. У вас может быть такая карта:

Map<String, String> values = new HashMap<String, String>();

содержащий все данные, которые можно заменить. Затем вы можете перебирать карту и изменять все в строке следующим образом:

String s = "Your String with [Fields]";
for (Map.Entry<String, String> e : values.entrySet()) {
  s = s.replaceAll("\\[" + e.getKey() + "\\]", e.getValue());
}

Вы также можете перебирать String и находить элементы на карте. Но это немного сложнее, потому что вам нужно проанализировать строку в поисках []. Вы можете сделать это с помощью регулярного выражения, используя Pattern и Matcher.

Рикардо Маримон
источник
9
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
Бруно Раншарт
источник
2
Спасибо, но в моем случае строка шаблона может быть изменена пользователем, поэтому я не могу быть уверен в порядке токенов
Марк
3

Мое решение по замене токенов стиля $ {variable} (на основе ответов здесь и Spring UriTemplate):

public static String substituteVariables(String template, Map<String, String> variables) {
    Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}");
    Matcher matcher = pattern.matcher(template);
    // StringBuilder cannot be used here because Matcher expects StringBuffer
    StringBuffer buffer = new StringBuffer();
    while (matcher.find()) {
        if (variables.containsKey(matcher.group(1))) {
            String replacement = variables.get(matcher.group(1));
            // quote to work properly with $ and {,} signs
            matcher.appendReplacement(buffer, replacement != null ? Matcher.quoteReplacement(replacement) : "null");
        }
    }
    matcher.appendTail(buffer);
    return buffer.toString();
}
mihu86
источник
1

С библиотекой Apache Commons вы можете просто использовать Stringutils.replaceEach :

public static String replaceEach(String text,
                             String[] searchList,
                             String[] replacementList)

Из документации :

Заменяет все вхождения строки в другую строку.

Пустая ссылка, переданная этому методу, не работает, или если какая-либо «строка поиска» или «строка для замены» имеет значение null, эта замена будет проигнорирована. Это не повторится. Для повторяющихся замен вызовите перегруженный метод.

 StringUtils.replaceEach(null, *, *)        = null

  StringUtils.replaceEach("", *, *)          = ""

  StringUtils.replaceEach("aba", null, null) = "aba"

  StringUtils.replaceEach("aba", new String[0], null) = "aba"

  StringUtils.replaceEach("aba", null, new String[0]) = "aba"

  StringUtils.replaceEach("aba", new String[]{"a"}, null)  = "aba"

  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"

  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"

  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
  (example of how it does not repeat)

StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"
AR1
источник
1

К вашему сведению

В новом языке Kotlin вы можете напрямую использовать «строковые шаблоны» в исходном коде, никакая сторонняя библиотека или механизм шаблонов не должны выполнять замену переменных.

Это особенность самого языка.

См. Https://kotlinlang.org/docs/reference/basic-types.html#string-templates

Ли Инь
источник
0

Раньше я решал эту проблему с помощью StringTemplate и Groovy Templates .

В конечном счете, решение использовать шаблонизатор или нет, должно основываться на следующих факторах:

  • Будет ли у вас много таких шаблонов в приложении?
  • Вам нужна возможность изменять шаблоны без перезапуска приложения?
  • Кто будет поддерживать эти шаблоны? Программист Java или бизнес-аналитик, вовлеченный в проект?
  • Потребуется ли вам возможность добавлять логику в ваши шаблоны, например условный текст, основанный на значениях переменных?
  • Вам понадобится возможность включать в шаблон другие шаблоны?

Если что-либо из вышеперечисленного применимо к вашему проекту, я бы подумал об использовании механизма шаблонов, большинство из которых предоставляют эту функциональность и многое другое.

Франсуа Гравель
источник
0

я использовал

String template = "Hello %s Please find attached %s which is due on %s";

String message = String.format(template, name, invoiceNumber, dueDate);
mtwom
источник
2
Это сработает, но в моем случае строка шаблона настраивается пользователем, поэтому я не знаю, в каком порядке появятся токены.
Марк
0

Следующее заменяет переменные формы <<VAR>>значениями, найденными на карте. Вы можете протестировать это онлайн здесь

Например, со следующей входной строкой

BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70
Hi there <<Weight>> was here

и следующие значения переменных

Weight, 42
Height, HEIGHT 51

выводит следующие

BMI=(42/(HEIGHT 51*HEIGHT 51)) * 70

Hi there 42 was here

Вот код

  static Pattern pattern = Pattern.compile("<<([a-z][a-z0-9]*)>>", Pattern.CASE_INSENSITIVE);

  public static String replaceVarsWithValues(String message, Map<String,String> varValues) {
    try {
      StringBuffer newStr = new StringBuffer(message);
      int lenDiff = 0;
      Matcher m = pattern.matcher(message);
      while (m.find()) {
        String fullText = m.group(0);
        String keyName = m.group(1);
        String newValue = varValues.get(keyName)+"";
        String replacementText = newValue;
        newStr = newStr.replace(m.start() - lenDiff, m.end() - lenDiff, replacementText);
        lenDiff += fullText.length() - replacementText.length();
      }
      return newStr.toString();
    } catch (Exception e) {
      return message;
    }
  }


  public static void main(String args[]) throws Exception {
      String testString = "BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70\n\nHi there <<Weight>> was here";
      HashMap<String,String> values = new HashMap<>();
      values.put("Weight", "42");
      values.put("Height", "HEIGHT 51");
      System.out.println(replaceVarsWithValues(testString, values));
  }

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

private static Pattern patternMatchForProperties =
      Pattern.compile("[$][{]([.a-z0-9_]*)[}]", Pattern.CASE_INSENSITIVE);

protected String replaceVarsWithProperties(String message) {
    try {
      StringBuffer newStr = new StringBuffer(message);
      int lenDiff = 0;
      Matcher m = patternMatchForProperties.matcher(message);
      while (m.find()) {
        String fullText = m.group(0);
        String keyName = m.group(1);
        String newValue = System.getProperty(keyName);
        String replacementText = newValue;
        newStr = newStr.replace(m.start() - lenDiff, m.end() - lenDiff, replacementText);
        lenDiff += fullText.length() - replacementText.length();
      }
      return newStr.toString();
    } catch (Exception e) {
      return message;
    }
  }
Брэд Паркс
источник