Wie ersetze ich eine Reihe von Token in einem Java-String?

106

Ich habe die folgende Vorlage String : "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

Ich habe auch String-Variablen für Name, Rechnungsnummer und Fälligkeitsdatum. Wie lassen sich die Token in der Vorlage am besten durch die Variablen ersetzen?

(Beachten Sie, dass eine Variable, die zufällig ein Token enthält, NICHT ersetzt werden sollte.)


BEARBEITEN

Dank @laginimaineb und @ alan-moore ist hier meine Lösung:

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();
}
Kennzeichen
quelle
Zu beachten ist jedoch, dass StringBuffer mit StringBuilder identisch ist, der gerade synchronisiert wurde. Da Sie in diesem Beispiel die Erstellung des Strings jedoch nicht synchronisieren müssen, ist es möglicherweise besser, StringBuilder zu verwenden (obwohl das Erwerben von Sperren fast keine Kosten verursacht).
Laginimaineb
1
Leider müssen Sie in diesem Fall StringBuffer verwenden. Das erwarten die appendXXX () -Methoden. Sie gibt es seit Java 4 und StringBuilder wurde erst mit Java 5 hinzugefügt. Wie Sie sagten, ist es keine große Sache, nur ärgerlich.
Alan Moore
4
Eine weitere Sache: appendReplacement () sucht wie die replaceXXX () -Methoden nach Capture-Gruppenreferenzen wie $ 1, $ 2 usw. und ersetzt sie durch den Text der zugehörigen Capture-Gruppen. Wenn Ihr Ersatztext möglicherweise Dollarzeichen oder Backslashes enthält (die verwendet werden, um Dollarzeichen zu umgehen), liegt möglicherweise ein Problem vor. Der einfachste Weg, damit umzugehen, besteht darin, den Append-Vorgang in zwei Schritte zu unterteilen, wie ich es im obigen Code getan habe.
Alan Moore
Alan - sehr beeindruckt, dass du das entdeckt hast. Ich hätte nicht gedacht, dass ein so einfaches Problem so schwer zu lösen ist!
Mark

Antworten:

65

Am effizientesten wäre es, einen Matcher zu verwenden, um die Ausdrücke kontinuierlich zu finden und zu ersetzen, und dann den Text an einen String-Builder anzuhängen:

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();
Laginimaineb
quelle
10
So würde ich es machen, außer ich würde Matchers Methoden appendReplacement () und appendTail () verwenden, um den nicht übereinstimmenden Text zu kopieren. Das muss nicht von Hand gemacht werden.
Alan Moore
5
Tatsächlich erfordern die Methoden appendReplacement () und appentTail () einen StringBuffer, der snychronisiert ist (was hier keinen Nutzen hat). Die angegebene Antwort verwendet einen StringBuilder, der in meinen Tests 20% schneller ist.
Dube
103

Ich glaube wirklich nicht, dass Sie dafür eine Template-Engine oder ähnliches verwenden müssen. Sie können die String.formatMethode wie folgt verwenden:

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

String message = String.format(template, name, invoiceNumber, dueDate);
Paul Morie
quelle
4
Ein Nachteil davon ist, dass Sie die Parameter in die richtige Reihenfolge bringen müssen
Gerrytan
Zum anderen können Sie kein eigenes Ersatz-Token-Format angeben.
Franz D.
Ein weiterer Grund ist, dass es nicht dynamisch funktioniert, da es in der Lage ist, einen Datensatz mit Schlüsseln / Werten zu haben und ihn dann auf eine beliebige Zeichenfolge anzuwenden
Brad Parks
43

Leider ist die oben erwähnte komfortable Methode String.format nur ab Java 1.5 verfügbar (was heutzutage ziemlich Standard sein sollte, aber man weiß es nie). Stattdessen können Sie auch die Java- Klasse MessageFormat zum Ersetzen der Platzhalter verwenden.

Es werden Platzhalter in der Form '{number}' unterstützt, sodass Ihre Nachricht wie folgt aussehen würde: "Hallo {0}. Bitte finden Sie die angehängte {1}, die auf {2} fällig ist." Diese Zeichenfolgen können mithilfe von ResourceBundles problemlos externalisiert werden (z. B. zur Lokalisierung mit mehreren Gebietsschemas). Das Ersetzen würde mit der statischen 'Format'-Methode der Klasse MessageFormat erfolgen:

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));
Toolkit
quelle
3
Ich konnte mich nicht an den Namen von MessageFormat erinnern und es ist irgendwie albern, wie viel Googeln ich tun musste, um selbst diese Antwort zu finden. Jeder verhält sich entweder wie String.format oder verwendet einen Drittanbieter, wobei dieses unglaublich nützliche Dienstprogramm vergessen wird.
Patrick
1
Dies ist seit 2004 verfügbar - warum lerne ich gerade erst 2017 davon? Ich überarbeite einen Code, der in StringBuilder.append()s behandelt wird, und dachte: "Sicher gibt es einen besseren Weg ... etwas mehr Pythonisches ..." - und heiliger Mist, ich denke, diese Methode könnte vor Pythons Formatierungsmethoden liegen. Eigentlich ... das ist vielleicht älter als 2002 ... Ich kann nicht finden, wann das tatsächlich zustande gekommen ist ...
ArtOfWarfare
42

Sie können versuchen, eine Vorlagenbibliothek wie Apache Velocity zu verwenden.

http://velocity.apache.org/

Hier ist ein Beispiel:

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

Die Ausgabe wäre:

Hallo Mark. Die beigefügte Rechnung 42123 ist am 6. Juni 2009 fällig.
Hallidave
quelle
Ich habe in der Vergangenheit Geschwindigkeit verwendet. Funktioniert super.
Hardwareguy
4
stimme zu, warum das Rad neu erfinden
Objekte
6
Es ist ein bisschen übertrieben, eine ganze Bibliothek für eine einfache Aufgabe wie diese zu verwenden. Velocity hat viele andere Funktionen, und ich bin der festen Überzeugung, dass dies für eine einfache Aufgabe wie diese nicht geeignet ist.
Andrei Ciobanu
24

Sie können die Vorlagenbibliothek zum Ersetzen komplexer Vorlagen verwenden.

FreeMarker ist eine sehr gute Wahl.

http://freemarker.sourceforge.net/

Für einfache Aufgaben gibt es jedoch eine einfache Dienstprogrammklasse, die Ihnen helfen kann.

org.apache.commons.lang3.text.StrSubstitutor

Es ist sehr leistungsfähig, anpassbar und einfach zu bedienen.

Diese Klasse nimmt einen Text und ersetzt alle darin enthaltenen Variablen. Die Standarddefinition einer Variablen ist $ {variableName}. Das Präfix und das Suffix können über Konstruktoren und Set-Methoden geändert werden.

Variablenwerte werden normalerweise aus einer Karte aufgelöst, können aber auch aus Systemeigenschaften oder durch Bereitstellung eines benutzerdefinierten Variablenauflösers aufgelöst werden.

Wenn Sie beispielsweise die Systemumgebungsvariable durch eine Vorlagenzeichenfolge ersetzen möchten, lautet der folgende Code:

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);
    }
}
Li Ying
quelle
2
org.apache.commons.lang3.text.StrSubstitutor hat großartig für mich
funktioniert
17
System.out.println(MessageFormat.format("Hello {0}! You have {1} messages", "Join",10L));

Ausgabe: Hallo Join! Sie haben 10 Nachrichten "

user2845137
quelle
2
John überprüft seine Nachrichten eindeutig so oft, wie ich meinen "Spam" -Ordner überprüfe, da er lang ist.
Hemmels
9

Dies hängt davon ab, wo sich die tatsächlichen Daten befinden, die Sie ersetzen möchten. Möglicherweise haben Sie eine Karte wie diese:

Map<String, String> values = new HashMap<String, String>();

enthält alle Daten, die ersetzt werden können. Anschließend können Sie die Karte durchlaufen und alles in der Zeichenfolge wie folgt ändern:

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

Sie können auch über den String iterieren und die Elemente in der Karte finden. Das ist jedoch etwas komplizierter, da Sie den String nach [] durchsuchen müssen. Sie können dies mit einem regulären Ausdruck unter Verwendung von Pattern und Matcher tun.

Ricardo Marimon
quelle
9
String.format("Hello %s Please find attached %s which is due on %s", name, invoice, date)
Bruno Ranschaert
quelle
1
Danke - aber in meinem Fall die Vorlage Zeichenfolge kann vom Anwender geändert werden, so dass ich nicht sicher , ob in der Größenordnung von Tokens sein kann
Mark
3

Meine Lösung zum Ersetzen von Token im Stil von $ {variable} (inspiriert von den Antworten hier und von der 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();
}
mihu86
quelle
1

Mit der Apache Commons Library können Sie einfach Stringutils.replaceEach verwenden :

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

Aus der Dokumentation :

Ersetzt alle Vorkommen von Strings in einem anderen String.

Eine an diese Methode übergebene Nullreferenz ist ein No-Op. Wenn eine "Suchzeichenfolge" oder "zu ersetzende Zeichenfolge" null ist, wird diese Ersetzung ignoriert. Dies wird nicht wiederholt. Rufen Sie zum Wiederholen von Ersetzungen die überladene Methode auf.

 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"
AR1
quelle
1

Zu Ihrer Information

In der neuen Sprache Kotlin können Sie "String Templates" direkt in Ihrem Quellcode verwenden. Keine Bibliothek oder Template-Engine eines Drittanbieters muss die Variablen ersetzen.

Es ist ein Merkmal der Sprache selbst.

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

Li Ying
quelle
0

In der Vergangenheit habe ich diese Art von Problem mit StringTemplate und Groovy Templates gelöst .

Letztendlich sollte die Entscheidung, ob eine Template-Engine verwendet wird oder nicht, auf folgenden Faktoren beruhen:

  • Haben Sie viele dieser Vorlagen in der Anwendung?
  • Benötigen Sie die Möglichkeit, die Vorlagen zu ändern, ohne die Anwendung neu zu starten?
  • Wer wird diese Vorlagen pflegen? Ein Java-Programmierer oder ein an dem Projekt beteiligter Business Analyst?
  • Müssen Sie in der Lage sein, Logik in Ihre Vorlagen einzufügen, z. B. bedingten Text, der auf Werten in den Variablen basiert?
  • Benötigen Sie die Möglichkeit, andere Vorlagen in eine Vorlage aufzunehmen?

Wenn einer der oben genannten Punkte auf Ihr Projekt zutrifft, würde ich die Verwendung einer Template-Engine in Betracht ziehen, von denen die meisten diese Funktionalität bieten, und mehr.

Francois Kies
quelle
0

ich benutzte

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

String message = String.format(template, name, invoiceNumber, dueDate);
mtwom
quelle
2
Das würde funktionieren, aber in meinem Fall kann die Vorlagenzeichenfolge vom Benutzer angepasst werden, sodass ich nicht weiß, in welcher Reihenfolge die Token angezeigt werden.
Mark
0

Im Folgenden werden Variablen des Formulars <<VAR>>durch Werte ersetzt, die in einer Karte nachgeschlagen wurden. Sie können es hier online testen

Zum Beispiel mit der folgenden Eingabezeichenfolge

BMI=(<<Weight>>/(<<Height>>*<<Height>>)) * 70
Hi there <<Weight>> was here

und die folgenden Variablenwerte

Weight, 42
Height, HEIGHT 51

gibt folgendes aus

BMI=(42/(HEIGHT 51*HEIGHT 51)) * 70

Hi there 42 was here

Hier ist der Code

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

und obwohl nicht angefordert, können Sie einen ähnlichen Ansatz verwenden, um Variablen in einer Zeichenfolge durch Eigenschaften aus Ihrer application.properties-Datei zu ersetzen, obwohl dies möglicherweise bereits durchgeführt wird:

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;
    }
  }
Brad Parks
quelle