Як замінити набір жетонів у Java String?


106

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

У мене також є змінні String для імені, номера рахунка-фактури та строку - який найкращий спосіб замінити маркери в шаблоні змінними?

(Зверніть увагу, що якщо змінна містить маркер, її НЕ слід замінювати).


EDIT

Завдяки @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

1
На жаль, вам доведеться використовувати StringBuffer в цьому випадку; це те, чого очікують методи appendXXX (). Вони існують з Java 4, і StringBuilder не додавали до Java 5. Як ви вже сказали, це не так вже й просто, дратує.
Алан Мур

4
І ще одне: appendReplacement (), як і метод substituXXX (), шукає посилання групи захоплення, такі як $ 1, $ 2 і т.д., і замінює їх текстом із пов'язаних груп захоплення. Якщо ваш текст заміни може містити знаки долара або зворотні риски (які використовуються для уникнення знаків долара), у вас може виникнути проблема. Найпростіший спосіб вирішити це - розбити операцію додавання на два етапи, як я це зробив у наведеному вище коді.
Алан Мур

Алан - дуже вразив, що ти це помітив. Я не думав, що таку просту проблему буде так важко вирішити!
Марк

Відповіді:


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's appendReplacement () та appendTail (), щоб скопіювати текст, який не відповідає тексту; немає необхідності робити це вручну.
Алан Мур

5
Насправді для методів appendReplacement () та appentTail () потрібен StringBuffer, який є сніхронізованим (який тут не корисний). Дана відповідь використовує StringBuilder, який на 20% швидший у моїх тестах.
дуб

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
Одним із недоліків цього є те, що ви повинні встановити параметри в правильному порядку
gerrytan

Інша полягає в тому, що ви не можете вказати власний формат маркера заміни.
Франц Д.

інше - це те, що він не працює динамічно, маючи можливість мати набір даних ключів / значень, а потім застосовувати його до будь-якого рядка
Brad Parks

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.

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 року.

Я раніше використовував швидкість. Чудово працює.
Hardwareguy

4
погодьтеся, навіщо винаходити колесо
об’єкти

6
Трохи надмірно використовувати цілу бібліотеку для такого простого завдання. У швидкості є безліч інших особливостей, і я впевнений, що це не підходить для такої простої задачі.
Андрій Чобану

24

Ви можете використовувати бібліотеку шаблонів для складної заміни шаблонів.

FreeMarker - дуже хороший вибір.

http://freemarker.sourceforge.net/

Але для простого завдання вам може допомогти простий клас корисності.

org.apache.commons.lang3.text.StrSubstitutor

Він дуже потужний, налаштовується та простий у використанні.

Цей клас займає фрагмент тексту та замінює всі змінні в ньому. За замовчуванням визначення змінної - $ {variaName}. Префікс і суфікс можна змінювати за допомогою конструкторів та встановлених методів.

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

Наприклад, якщо ви хочете замінити змінну системного середовища на рядок шаблону, ось код:

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

17
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Вихід: Привіт, приєднуйтесь! У вас є 10 повідомлень "


2
Джон чітко перевіряє свої повідомлення так часто, як я перевіряю свою папку "спам", враховуючи, що це Довга.
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());
}

Ви також можете перебрати рядок і знайти елементи на карті. Але це трохи складніше, тому що вам потрібно проаналізувати Рядок пошуку []. Ви можете зробити це з регулярним виразом, використовуючи Pattern і Matcher.


9
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)

1
Дякую - але в моєму випадку рядок шаблону може бути змінений користувачем, тому я не можу бути впевнений у порядку жетонів
Марк

3

Моє рішення щодо заміни лексем у стилі $ {змінний} (натхненний відповідями тут та 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();
}


1

З бібліотекою Apache Commons ви можете просто використовувати Stringutils.replaceEach :

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

З документації :

Замінює всі виникнення рядків у іншій рядку.

Нульова посилання, передана на цей метод, є неоперативною, або якщо будь-яка "рядок пошуку" або "рядок для заміни" є нульовою, ця заміна буде проігнорована. Це не повториться. Для повторення заміни викликайте метод перевантаження.

 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"

1

FYI

У новій мові Kotlin ви можете використовувати "String Templates" у своєму вихідному коді безпосередньо, жодна стороння бібліотека чи механізм шаблонів не потребують заміни змінної.

Це особливість самої мови.

Дивіться: 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);

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;
    }
  }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.