如何替换Java字符串中的一组标记?


106

我有以下模板字符串:"Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]"

我还有用于名称,发票编号和到期日的String变量-用变量替换模板中的标记的最佳方法是什么?

(请注意,如果变量恰好包含令牌,则不应将其替换)。


编辑

感谢@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开始,它们就存在了,直到Java 5才添加StringBuilder。正如您所说,这没什么大不了的,只是令人讨厌。
艾伦·摩尔

4
还有一件事:appendReplacement()与replaceXXX()方法一样,查找捕获组引用,例如$ 1,$ 2等,并将它们替换为关联的捕获组中的文本。如果替换文本可能包含美元符号或反斜杠(用于转义美元符号),则可能有问题。解决该问题的最简单方法是将添加操作分为两步,就像我在上面的代码中所做的那样。
艾伦·摩尔

艾伦-给您留下深刻的印象。我认为这样简单的问题不会那么难解决!
马克

Answers:


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 2014年

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

另一个是您不能指定自己的替换令牌格式。
Franz D.

另一个是它不能动态工作,能够拥有键/值的数据集,然后将其应用于任何字符串
Brad Parks

43

不幸的是,上面提到的舒适方法String.format仅可从Java 1.5开始使用(如今这应该已经很标准了,但您永远不会知道)。取而代之的是,您还可以使用Java的MessageFormat类来替换占位符。

它支持格式为“ {number}”的占位符,因此您的消息看起来像是“您好{0},请在{2}上找到附件{1}”。这些字符串可以轻松地使用ResourceBundles进行外部化(例如,用于具有多个语言环境的本地化)。替换将使用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));

3
我不记得MessageFormat的名称,这真是愚蠢,我什至要找到这个答案也要做多少谷歌搜索。每个人的行为就像是String.format或使用3rd-party,而忘记了这个非常有用的实用程序。
Patrick

1
自2004年以来一直可用-为什么我现在才在2017年才了解呢?我正在重构StringBuilder.append()s中涉及的一些代码,并且我在想:“当然,还有更好的方法……更多Python式的东西……”-废话,我认为该方法可能早于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);
    }
}

输出为:

你好马克。请查看2009年6月6日到期的发票42123。

我过去使用过速度。效果很好。
Hardwareguy

4
同意,为什么要重新发明轮子
反对

6
将整个库用于这样的简单任务有点矫over过正。速度还有很多其他功能,我坚信这不适用于像这样的简单任务。
Andrei Ciobanu

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 2015年

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中的所有内容:

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

您还可以遍历String并在地图中找到元素。但这有点复杂,因为您需要解析String搜索[]。您可以使用Pattern和Matcher使用正则表达式来实现。



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


1

使用Apache Commons Library,您可以简单地使用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"


0

过去,我已经使用StringTemplateGroovy 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>>从Map查找的值。您可以在这里在线测试

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

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;
    }
  }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.