【问题标题】:How to replace tokens in a string without StringTokenizer如何在没有 StringTokenizer 的情况下替换字符串中的标记
【发布时间】:2010-11-11 10:47:21
【问题描述】:

给定这样的字符串:

 Hello {FIRST_NAME}, this is a personalized message for you.

其中 FIRST_NAME 是一个任意标记(传递给方法的映射中的键),用于编写一个将该字符串转换为的例程:

Hello Jim, this is a personalized message for you.

给定一张包含 FIRST_NAME -> Jim 条目的地图。

似乎 StringTokenizer 是最直接的方法,但 Javadocs 确实说您应该更喜欢使用正则表达式方法。您将如何在基于正则表达式的解决方案中做到这一点?

【问题讨论】:

标签: java regex stringtokenizer


【解决方案1】:

感谢大家的回答!

Gizmo 的回答绝对是开箱即用的,而且是一个很好的解决方案,但不幸的是,格式不适合 Formatter 类在这种情况下所做的事情。

Adam Paynter 用正确的模式真正抓住了问题的核心。

Peter Nix 和 Sean Bright 有一个很好的解决方法来避免正则表达式的所有复杂性,但如果有错误的标记,我需要提出一些错误,但没有这样做。

但是就进行正则表达式和合理的替换循环而言,这是我想出的答案(在 Google 和现有答案的一点帮助下,包括 Sean Bright 关于如何使用 group(1) vs组()):

private static Pattern tokenPattern = Pattern.compile("\\{([^}]*)\\}");

public static String process(String template, Map<String, Object> params) {
    StringBuffer sb = new StringBuffer();
    Matcher myMatcher = tokenPattern.matcher(template);
    while (myMatcher.find()) {
        String field = myMatcher.group(1);
        myMatcher.appendReplacement(sb, "");
        sb.append(doParameter(field, params));
   }
    myMatcher.appendTail(sb);
    return sb.toString();
}

doParameter 从映射中获取值并将其转换为字符串,如果不存在则抛出异常。

还请注意,我更改了模式以查找空括号(即 {}),因为这是明确检查的错误条件。

编辑:请注意,appendReplacement 与字符串的内容无关。根据 javadocs,它将 $ 和反斜杠识别为特殊字符,因此我在上面的示例中添加了一些转义来处理它。没有以最注重性能的方式完成,但就我而言,这还不够大,不值得尝试对字符串创建进行微优化。

感谢 Alan M 的评论,这可以更简单地避免 appendReplacement 的特殊字符问题。

【讨论】:

  • 这是一个很好的答案。很遗憾我没有仔细阅读 JavaDocs...
  • 你不需要逃避替换,只要让它远离 appendReplacement():myMatcher.appendReplacement(sb, ""); sb.append(doParameter(field, params));
  • 感谢您提供此更新非常有用的问题和答案!
【解决方案2】:

好吧,我宁愿使用 String.format(),或者更好的MessageFormat

【讨论】:

    【解决方案3】:
    String.replaceAll("{FIRST_NAME}", actualName);
    

    查看 javadocs here

    【讨论】:

    • 其性能为 o(n*k),其中 n 是输入字符串的大小,k 是键的数量。
    • @Daniel 您是否阅读了源代码来得出这个结论? Java 用字符串做了一些非常聪明的事情。我希望它很有可能会胜过您想出的任何其他解决方案。
    • @BillK 我认为他可能意味着如果您在字符串中有多个要替换的键,则必须重复调用replaceAll,因此*k
    • 我想我是说 K 可能没有你想象的那么大。我还没有看到这种性能产生影响的案例——我几乎没有在嵌入式 Java 系统上工作过,甚至尝试优化所有字符串操作,因为 Java 的传说是它产生了巨大的差异——它没有不。您必须对 JVM 进行实际改进的字符串优化类型要深入得多。此外,性能之前的可读性,这正是你想要做的,你必须在替换可读的东西之前证明它修复了明显的问题。
    【解决方案4】:

    试试这个:

    注意:author's final solution 建立在此示例之上,并且更加简洁。

    public class TokenReplacer {
    
        private Pattern tokenPattern;
    
        public TokenReplacer() {
            tokenPattern = Pattern.compile("\\{([^}]+)\\}");
        }
    
        public String replaceTokens(String text, Map<String, String> valuesByKey) {
            StringBuilder output = new StringBuilder();
            Matcher tokenMatcher = tokenPattern.matcher(text);
    
            int cursor = 0;
            while (tokenMatcher.find()) {
                // A token is defined as a sequence of the format "{...}".
                // A key is defined as the content between the brackets.
                int tokenStart = tokenMatcher.start();
                int tokenEnd = tokenMatcher.end();
                int keyStart = tokenMatcher.start(1);
                int keyEnd = tokenMatcher.end(1);
    
                output.append(text.substring(cursor, tokenStart));
    
                String token = text.substring(tokenStart, tokenEnd);
                String key = text.substring(keyStart, keyEnd);
    
                if (valuesByKey.containsKey(key)) {
                    String value = valuesByKey.get(key);
                    output.append(value);
                } else {
                    output.append(token);
                }
    
                cursor = tokenEnd;
            }
            output.append(text.substring(cursor));
    
            return output.toString();
        }
    
    }
    

    【讨论】:

    • 这将为每一行重新编译模式。我更喜欢我的模式尽可能预编译! :-) 另外,你最好检查一下令牌是否存在。
    • 我的意思是,检查标记是否存在于地图中。
    • 您可以将tokenPattern 设为任何包含此方法的类的实例变量,以避免每次都编译它。代码将自动适应未检测到令牌的情况 (output.append(text.substring(cursor)))。
    • 最新的更改包括检查密钥是否存在。
    • 这是一个很好的答案,值得被接受的答案。找到令牌后,请参阅我的答案以获得更简洁的附加方式。
    【解决方案5】:

    通过导入 java.util.regex.*:

    Pattern p = Pattern.compile("{([^{}]*)}");
    Matcher m = p.matcher(line);  // line being "Hello, {FIRST_NAME}..."
    while (m.find) {
      String key = m.group(1);
      if (map.containsKey(key)) {
        String value= map.get(key);
        m.replaceFirst(value);
      }
    }
    

    因此,推荐使用正则表达式,因为它可以轻松识别字符串中需要替换的位置,以及提取键名进行替换。比打断整个字符串要高效得多。

    您可能希望在内部循环使用 Matcher 行,在外部使用 Pattern 行,这样您就可以替换所有行。该模式永远不需要重新编译,避免不必要地重新编译会更有效。

    【讨论】:

    • m.group(0) 是整个匹配项(即 {FIRST_NAME})。 m.group(1) 只是关键(即 FIRST_NAME)。
    【解决方案6】:

    最直接的方法似乎是这样的:

    public static void main(String[] args) {
        String tokenString = "Hello {FIRST_NAME}, this is a personalized message for you.";
        Map<String, String> tokenMap = new HashMap<String, String>();
        tokenMap.put("{FIRST_NAME}", "Jim");
        String transformedString = tokenString;
        for (String token : tokenMap.keySet()) {
            transformedString = transformedString.replace(token, tokenMap.get(token));
        }
        System.out.println("New String: " + transformedString);
    }
    

    它遍历所有标记并用您需要的内容替换每个标记,并使用标准字符串方法进行替换,从而跳过整个 RegEx 的挫折。

    【讨论】:

    • 这意味着读取每个标记的整个字符串。如果您有 k 个令牌和 n 个字节要处理,那么该算法的顺序为 o(n*k)。非常低效。
    • 理论上,如所述,它是 o(n*k),但你的陈述对我来说感觉像是过早的优化。如果不了解该算法的调用频率、字符串中存在多少令牌、字符串有多长以及节省时间的关键程度,则无法说效率低下的影响有多大。如果仅调用一次,总运行时间为 10 毫秒,即使它可能在 1 毫秒(例如)时同样有效,它肯定比它可能慢一个数量级,但性能损失真的那么大吗?宏伟的计划?
    【解决方案7】:

    根据您的字符串的复杂程度,您可以尝试使用更严格的字符串模板语言,例如 Velocity。在 Velocity 的情况下,你会做这样的事情:

    Velocity.init();
    VelocityContext context = new VelocityContext();
    context.put( "name", "Bob" );
    StringWriter output = new StringWriter();
    Velocity.evaluate( context, output, "", 
          "Hello, #name, this is a personalized message for you.");
    System.out.println(output.toString());
    

    但如果您只想替换一两个值,这可能有点过头了。

    【讨论】:

      【解决方案8】:
      import java.util.HashMap;
      
      public class ReplaceTest {
      
        public static void main(String[] args) {
          HashMap<String, String> map = new HashMap<String, String>();
      
          map.put("FIRST_NAME", "Jim");
          map.put("LAST_NAME",  "Johnson");
          map.put("PHONE",      "410-555-1212");
      
          String s = "Hello {FIRST_NAME} {LAST_NAME}, this is a personalized message for you.";
      
          for (String key : map.keySet()) {
            s = s.replaceAll("\\{" + key + "\\}", map.get(key));
          }
      
          System.out.println(s);
        }
      
      }
      

      【讨论】:

        【解决方案9】:

        文档意味着您应该更喜欢编写基于正则表达式的标记器 IIRC。对您来说更好的是标准正则表达式搜索替换。

        【讨论】:

          【解决方案10】:

          通常我们会在这种情况下使用 MessageFormat,同时从 ResourceBundle 加载实际的消息文本。这为您提供了对 G10N 友好的额外好处。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2015-02-12
            • 2020-10-27
            • 1970-01-01
            • 2022-01-03
            • 1970-01-01
            • 2017-04-27
            • 1970-01-01
            相关资源
            最近更新 更多