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

1
残念ながら、この場合はStringBufferを使用する必要があります。これは、appendXXX()メソッドが期待するものです。それらはJava 4から存在しており、StringBuilderはJava 5まで追加されませんでしたが、あなたが言ったように、それは大したことではなく、ただ迷惑です。
アランムーア

4
もう1つ、appendReplacement()は、replaceXXX()メソッドと同様に、$ 1、$ 2などのキャプチャグループ参照を探し、それらを関連するキャプチャグループのテキストで置き換えます。置換テキストにドル記号またはバックスラッシュ(ドル記号をエスケープするために使用される)が含まれている場合、問題が発生している可能性があります。これに対処する最も簡単な方法は、上記のコードで行ったように、追加操作を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のappendReplacement()メソッドとappendTail()メソッドを使用して一致しないテキストをコピーすることを除いて、これは私が行う方法です。手作業で行う必要はありません。
アランムーア

5
実際、appendReplacement()メソッドとappentTail()メソッドには、同期化されているStringBufferが必要です(ここでは使用しません)。与えられた答えは、私のテストでは20%速いStringBuilderを使用しています。
デューブ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
これの欠点の1つは、パラメーターを正しい順序で配置する必要があることです
gerrytan

もう1つは、独自の置換トークン形式を指定できないことです。
Franz D.17年

もう1つは、動的に機能せず、キー/値のデータセットを
Brad Parks

43

残念ながら、上記の快適なメソッドString.formatは、Java 1.5以降でのみ使用できます(これは、今日ではかなり標準になっているはずですが、あなたにはわかりません)。その代わりに、JavaのクラスMessageFormatを使用することもできます。、プレースホルダーを置き換えるためにを。

「{番号}」の形式のプレースホルダーをサポートしているため、メッセージは「こんにちは{0}添付の{1}を見つけてください。これは{2}の期限です」のようになります。これらの文字列は、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のように機能するか、サードパーティを使用して、この信じられないほど有用なユーティリティを忘れています。
Patrick

1
これは2004年から利用可能になっています。なぜ今、2017年だけになってしまったのですか?私はStringBuilder.append()sでカバーされているいくつかのコードをリファクタリングしていて、「確かにもっと良い方法がある...もっとPythonicなものがある...」と思っていました。実際に...これは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
このような単純なタスクにライブラリ全体を使用するのは少しやり過ぎです。Velocityには他にも多くの機能があり、このような単純なタスクには適していないと強く信じています。
Andrei Ciobanu 2011年

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

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

出力: Hello Join!10件のメッセージがあります」


2
Johnは自分の「スパム」フォルダをチェックするのと同じくらい頻繁にメッセージをチェックします。
Hemmels 2017

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

文字列を繰り返し処理して、マップ内の要素を見つけることもできます。ただし、[]を検索する文字列を解析する必要があるため、少し複雑です。パターンとマッチャーを使用して正規表現でそれを行うことができます。


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

1
ありがとう-しかし、私の場合、テンプレート文字列はユーザ​​ーが変更できるため、トークンの順序がわかりません
Mark

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)

ドキュメントから:

別の文字列内のすべての文字列を置き換えます。

このメソッドに渡されるnull参照は何もしない、または「検索文字列」または「置換する文字列」が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"


0

過去に、この種の問題をStringTemplateGroovyテンプレートで解決しました

最終的に、テンプレートエンジンを使用するかどうかは、次の要素に基づいて決定する必要があります。

  • これらのテンプレートの多くをアプリケーションに含めますか?
  • アプリケーションを再起動せずにテンプレートを変更する機能が必要ですか?
  • これらのテンプレートを保守するのは誰ですか?プロジェクトに関与するJavaプログラマーまたはビジネスアナリスト?
  • 変数の値に基づくコンディショナルテキストのように、ロジックをテンプレートに配置する機能が必要ですか?
  • テンプレートに他のテンプレートを含める機能が必要ですか?

上記のいずれかがプロジェクトに当てはまる場合は、テンプレートエンジンを使用することを検討します。そのほとんどがこの機能を提供します。


0

使った

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

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

2
これでうまくいきますが、私の場合、テンプレート文字列はユーザ​​ーがカスタマイズできるため、トークンが表示される順序がわかりません。
Mark

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;
    }
  }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.