Come sostituire un set di token in una stringa Java?


106

Ho il seguente modello di stringa: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

Ho anche variabili String per nome, numero di fattura e data di scadenza: qual è il modo migliore per sostituire i token nel modello con le variabili?

(Nota che se una variabile contiene un token NON dovrebbe essere sostituita).


MODIFICARE

Ringraziando @laginimaineb e @ alan-moore, ecco la mia soluzione:

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

Una cosa da notare, tuttavia, è che StringBuffer è lo stesso di StringBuilder appena sincronizzato. Tuttavia, poiché in questo esempio non è necessario sincronizzare la creazione della stringa, potrebbe essere meglio utilizzare StringBuilder (anche se l'acquisizione di serrature è quasi un'operazione a costo zero).
laginimaineb

1
Sfortunatamente, in questo caso devi usare StringBuffer; è ciò che si aspettano i metodi appendXXX (). Sono in circolazione da Java 4 e StringBuilder non è stato aggiunto fino a Java 5. Come hai detto, però, non è un grosso problema, solo fastidioso.
Alan Moore,

4
Un'altra cosa: appendReplacement (), come i metodi replaceXXX (), cerca riferimenti a gruppi di cattura come $ 1, $ 2, ecc. E li sostituisce con il testo dei gruppi di cattura associati. Se il testo sostitutivo potrebbe contenere segni di dollaro o barre rovesciate (utilizzate per sfuggire ai segni del dollaro), potresti avere un problema. Il modo più semplice per gestirlo è suddividere l'operazione di aggiunta in due passaggi come ho fatto nel codice sopra.
Alan Moore

Alan, sono molto impressionato che tu l'abbia notato. Non pensavo che un problema così semplice sarebbe stato così difficile da risolvere!
Marco

Risposte:


65

Il modo più efficiente sarebbe utilizzare un matcher per trovare continuamente le espressioni e sostituirle, quindi aggiungere il testo a un generatore di stringhe:

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
È così che lo farei, tranne per il fatto che userei i metodi appendReplacement () e appendTail () di Matcher per copiare il testo non corrispondente; non è necessario farlo a mano.
Alan Moore

5
In realtà i metodi appendReplacement () e appentTail () richiedono uno StringBuffer, che è snychronized (che non serve qui). La risposta data utilizza uno StringBuilder, che è il 20% più veloce nei miei test.
dube

103

Non penso davvero che tu abbia bisogno di usare un motore di modelli o qualcosa del genere per questo. Puoi usare il String.formatmetodo, in questo modo:

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

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

4
Uno svantaggio di questo è che devi mettere i parametri nell'ordine corretto
gerrytan

Un altro è che non è possibile specificare il formato del token sostitutivo.
Franz D.

un altro è che non funziona dinamicamente, essendo in grado di avere un set di dati di chiavi / valori e quindi applicarlo a qualsiasi stringa
Brad Parks

43

Sfortunatamente il comodo metodo String.format menzionato sopra è disponibile solo a partire da Java 1.5 (che dovrebbe essere piuttosto standard al giorno d'oggi, ma non si sa mai). Invece di quello potresti anche usare la classe MessageFormat di Java per sostituire i segnaposto.

Supporta segnaposto nel formato "{numero}", quindi il tuo messaggio sarebbe "Salve {0} Per favore trova allegato {1} che scade il {2}". Queste stringhe possono essere facilmente esternalizzate utilizzando ResourceBundles (ad esempio per la localizzazione con più localizzazioni). La sostituzione verrebbe eseguita utilizzando il metodo static'format 'della classe MessageFormat:

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
Non riuscivo a ricordare il nome di MessageFormat, ed è un po 'sciocco quanto ho dovuto fare su Google per trovare anche questa risposta. Tutti si comportano come se fosse String.format o utilizzassero una terza parte, dimenticando questa utilità incredibilmente utile.
Patrick

1
Questo è disponibile dal 2004 - perché sto solo imparando a conoscere ora, nel 2017? Sto refactoring del codice che è coperto in StringBuilder.append()se stavo pensando "Sicuramente c'è un modo migliore ... qualcosa di più Pythonic ..." - e santa merda, penso che questo metodo potrebbe essere anteriore ai metodi di formattazione di Python. In realtà ... questo potrebbe essere più vecchio del 2002 ... Non riesco a trovare quando sia effettivamente nato ...
ArtOfWarfare

42

Potresti provare a utilizzare una libreria di modelli come Apache Velocity.

http://velocity.apache.org/

Ecco un esempio:

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

L'output sarebbe:

Ciao Mark. Si prega di trovare la fattura allegata 42123 che scade il 6 giugno 2009.

Ho usato la velocità in passato. Funziona alla grande.
Hardwareguy

4
d'accordo, perché reinventare la ruota
oggetti

6
È un po 'eccessivo usare un'intera libreria per un compito semplice come questo. Velocity ha molte altre caratteristiche e credo fermamente che non sia adatto per un compito semplice come questo.
Andrei Ciobanu

24

È possibile utilizzare la libreria di modelli per la sostituzione di modelli complessi.

FreeMarker è un'ottima scelta.

http://freemarker.sourceforge.net/

Ma per compiti semplici, c'è una semplice classe di utilità che può aiutarti.

org.apache.commons.lang3.text.StrSubstitutor

È molto potente, personalizzabile e facile da usare.

Questa classe prende un pezzo di testo e sostituisce tutte le variabili al suo interno. La definizione predefinita di una variabile è $ {variableName}. Il prefisso e il suffisso possono essere modificati tramite costruttori e metodi set.

I valori delle variabili vengono generalmente risolti da una mappa, ma possono anche essere risolti dalle proprietà di sistema o fornendo un risolutore di variabili personalizzato.

Ad esempio, se desideri sostituire la variabile di ambiente di sistema in una stringa modello, ecco il codice:

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 ha funzionato benissimo per me
ps0604

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

Risultato: Hello Join! Hai 10 messaggi "


2
John controlla chiaramente i suoi messaggi tutte le volte che controllo la mia cartella "spam" dato che è lunga.
Hemmels,

9

Dipende da dove si trovano i dati effettivi che si desidera sostituire. Potresti avere una mappa come questa:

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

contenente tutti i dati che possono essere sostituiti. Quindi puoi iterare sulla mappa e modificare tutto nella stringa come segue:

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

Puoi anche iterare sulla stringa e trovare gli elementi nella mappa. Ma questo è un po 'più complicato perché è necessario analizzare la stringa cercando []. Puoi farlo con un'espressione regolare usando Pattern e Matcher.


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

1
Grazie - ma nel mio caso la stringa del template può essere modificata dall'utente, quindi non posso essere sicuro dell'ordine dei token
Mark

3

La mia soluzione per sostituire i token di stile $ {variable} (ispirati alle risposte qui e allo 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

Con la libreria Apache Commons, puoi semplicemente usare Stringutils.replaceEach :

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

Dalla documentazione :

Sostituisce tutte le occorrenze di stringhe all'interno di un'altra stringa.

Un riferimento null passato a questo metodo è un no-op, o se una qualsiasi "stringa di ricerca" o "stringa da sostituire" è null, tale sostituzione verrà ignorata. Questo non si ripeterà. Per ripetere le sostituzioni, chiamare il metodo sovraccarico.

 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

In passato, ho risolto questo tipo di problema con StringTemplate e Groovy Templates .

In definitiva, la decisione di utilizzare o meno un motore di modelli dovrebbe essere basata sui seguenti fattori:

  • Avrai molti di questi modelli nell'applicazione?
  • Hai bisogno della possibilità di modificare i modelli senza riavviare l'applicazione?
  • Chi manterrà questi modelli? Un programmatore Java o un analista aziendale coinvolto nel progetto?
  • Avrai bisogno della capacità di inserire la logica nei tuoi modelli, come il testo condizionale basato sui valori nelle variabili?
  • Avrai bisogno della possibilità di includere altri modelli in un modello?

Se uno qualsiasi dei precedenti si applica al tuo progetto, prenderei in considerazione l'utilizzo di un motore di modelli, la maggior parte dei quali fornisce questa funzionalità e altro ancora.


0

ero solito

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

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

2
Funzionerebbe, ma nel mio caso la stringa del modello è personalizzabile dall'utente, quindi non so in quale ordine appariranno i token.
Marco

0

Quanto segue sostituisce le variabili del modulo <<VAR>>, con i valori cercati da una mappa. Puoi provarlo online qui

Ad esempio, con la seguente stringa di input

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

e i seguenti valori delle variabili

Weight, 42
Height, HEIGHT 51

restituisce quanto segue

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

Hi there 42 was here

Ecco il codice

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

e sebbene non sia richiesto, è possibile utilizzare un approccio simile per sostituire le variabili in una stringa con le proprietà dal file application.properties, anche se potrebbe essere già stato fatto:

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;
    }
  }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.