【问题标题】:How to replace a set of tokens in a Java String?如何替换 Java 字符串中的一组标记?
【发布时间】:2010-10-31 21:03:56
【问题描述】:

我有以下模板字符串:"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 可能会更好(即使获取锁几乎是零成本的操作)。
  • 不幸的是,在这种情况下你必须使用 StringBuffer;这是 appendXXX() 方法所期望的。它们从 Java 4 开始就存在了,而 StringBuilder 直到 Java 5 才被添加。不过正如你所说,这没什么大不了的,只是烦人。
  • 还有一点:appendReplacement() 与 replaceXXX() 方法一样,会查找 $1、$2 等捕获组引用,并将它们替换为相关捕获组中的文本。如果您的替换文本可能包含美元符号或反斜杠(用于转义美元符号),您可能会遇到问题。处理这个问题的最简单方法是像我在上面的代码中所做的那样,将附加操作分成两个步骤。
  • 艾伦 - 你发现这一点给你留下了深刻的印象。没想到这么简单的问题会这么难解决!

标签: java regex templates


【解决方案1】:

我真的认为您不需要为此使用模板引擎或类似的东西。您可以使用String.format 方法,如下所示:

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

String message = String.format(template, name, invoiceNumber, dueDate);

【讨论】:

  • 这样做的一个缺点是您必须按正确的顺序放置参数
  • 还有一个是不能指定自己的替换token格式。
  • 另一个是它不能动态工作,能够拥有一个键/值数据集,然后将其应用于任何字符串
【解决方案2】:

最有效的方法是使用匹配器不断查找表达式并替换它们,然后将文本附加到字符串构建器:

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

【讨论】:

  • 我会这样做,除了我会使用 Matcher 的 appendReplacement() 和 appendTail() 方法来复制不匹配的文本;无需手动操作。
  • 其实appendReplacement() 和appentTail() 方法需要一个StringBuffer,它是snychronized(这里没用)。给定的答案使用了一个 StringBuilder,在我的测试中它快了 20%。
【解决方案3】:

不幸的是,上面提到的舒适方法 String.format 仅从 Java 1.5 开始可用(现在应该是相当标准的,但你永远不知道)。取而代之的是,您也可以使用 Java 的 class MessageFormat 来替换占位符。

它支持“{number}”形式的占位符,因此您的消息看起来像“您好{0}请查找附件 {1},它将于 {2}到期”。这些字符串可以很容易地使用 ResourceBundle 进行外部化(例如,用于具有多个语言环境的本地化)。替换将使用 MessageFormat 类的 static'format' 方法完成:

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));

【讨论】:

  • 我不记得 MessageFormat 的名称,而且我不得不做多少谷歌搜索才能找到这个答案,这有点愚蠢。每个人都认为它要么是 String.format 要么使用 3rd-party,忘记了这个非常有用的实用程序。
  • 这个从 2004 年就开始提供了 - 为什么我现在才知道,在 2017 年?我正在重构StringBuilder.append()s 中涵盖的一些代码,我在想“肯定有更好的方法......更多的 Pythonic......” - 天哪,我认为这种方法可能早于 Python 的格式化方法。实际上......这可能比 2002 年更早......我无法找到它何时真正存在......
【解决方案4】:

您可以尝试使用像 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,该发票将于 2009 年 6 月 6 日到期。

【讨论】:

  • 我过去使用过速度。效果很好。
  • 同意,为什么要重新发明轮子
  • 像这样的简单任务使用整个库有点矫枉过正。 Velocity 还有很多其他功能,我坚信它不适合像这样的简单任务。
【解决方案5】:

您可以使用模板库进行复杂的模板替换。

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

【讨论】:

  • org.apache.commons.lang3.text.StrSubstitutor 非常适合我
【解决方案6】:
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

输出: 你好加入!您有 10 条消息”

【讨论】:

  • 约翰清楚地检查他的邮件,就像我检查我的“垃圾邮件”文件夹一样频繁,因为它很长。
【解决方案7】:

这取决于您要替换的实际数据的位置。你可能有这样的地图:

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 使用正则表达式来做到这一点。

【讨论】:

    【解决方案8】:
    String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
    

    【讨论】:

    • 谢谢 - 但在我的情况下,模板字符串可以由用户修改,所以我不能确定令牌的顺序
    【解决方案9】:

    我替换 ${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();
    }
    

    【讨论】:

      【解决方案10】:

      使用 Apache Commons Library,您可以简单地使用 Stringutils.replaceEach:

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

      来自documentation

      替换另一个字符串中所有出现的字符串。

      传递给此方法的空引用是无操作的,或者如果有任何“搜索 string" 或 "string to replace" 为空,则替换将被忽略。 这不会重复。对于重复替换,调用重载的 方法。

       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"
      

      【讨论】:

        【解决方案11】:

        您可以使用 Apache Commons StringSubstitutor:

        例如:

         // Build map
         Map<String, String> valuesMap = new HashMap<>();
         valuesMap.put("animal", "quick brown fox");
         valuesMap.put("target", "lazy dog");
         String templateString = "The ${animal} jumped over the ${target}.";
        
         // Build StringSubstitutor
         StringSubstitutor sub = new StringSubstitutor(valuesMap);
        
         // Replace
         String resolvedString = sub.replace(templateString);
        

        屈服:

         "The quick brown fox jumped over the lazy dog."
        

        您还可以使用以下方法自定义前缀和后缀分隔符(上例中分别为${}):

        您还可以使用如下语法指定默认值:

        String templateString = "The ${animal:giraffe} jumped over the ${target}.";
        

        当没有提供animal 参数时,这将产生"The giraffe jumped over the lazy dog."

        【讨论】:

          【解决方案12】:
          【解决方案13】:

          仅供参考

          在新的 Kotlin 语言中, 您可以直接在源代码中使用“字符串模板”, 无需第三方库或模板引擎进行变量替换。

          这是语言本身的一个特性。

          见: https://kotlinlang.org/docs/reference/basic-types.html#string-templates

          【讨论】:

            【解决方案14】:

            过去,我用StringTemplateGroovy Templates 解决了这类问题。

            最终,是否使用模板引擎的决定应基于以下因素:

            • 您的应用程序中会包含许多这样的模板吗?
            • 您是否需要在不重新启动应用程序的情况下修改模板?
            • 谁将维护这些模板?参与该项目的 Java 程序员或业务分析师?
            • 您是否需要将逻辑放入模板中,例如基于变量值的条件文本?
            • 您是否需要在模板中包含其他模板?

            如果上述任何一项适用于您的项目,我会考虑使用模板引擎,其中大多数都提供此功能等等。

            【讨论】:

              【解决方案15】:

              我用过

              String template = "Hello %s Please find attached %s which is due on %s";
              
              String message = String.format(template, name, invoiceNumber, dueDate);
              

              【讨论】:

              • 这可行,但在我的情况下,模板字符串可由用户自定义,所以我不知道令牌的出现顺序。
              【解决方案16】:

              以下将&lt;&lt;VAR&gt;&gt; 形式的变量替换为从映射中查找的值。你可以test it online here

              例如,使用以下输入字符串

              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;
                  }
                }
              

              【讨论】:

                猜你喜欢
                • 2011-07-16
                • 1970-01-01
                • 1970-01-01
                • 2013-12-15
                • 1970-01-01
                • 2021-03-30
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多