Bir Java Dizesindeki bir dizi simge nasıl değiştirilir?


106

Aşağıdaki şablon String: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

Ayrıca ad, fatura numarası ve son tarih için Dize değişkenlerim var - şablondaki simgeleri değişkenlerle değiştirmenin en iyi yolu nedir?

(Bir değişkenin bir belirteç içermesi durumunda değiştirilmemesi gerektiğini unutmayın).


DÜZENLE

@Laginimaineb ve @ alan-moore sayesinde, işte benim çözümüm:

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

Yine de dikkat edilmesi gereken bir nokta, StringBuffer'ın yeni senkronize edilmiş StringBuilder ile aynı olmasıdır. Bununla birlikte, bu örnekte String'in oluşturulmasını senkronize etmeniz gerekmediğinden, StringBuilder'ı kullanmak daha iyi olabilir (kilit almak neredeyse sıfır maliyetli bir işlem olsa bile).
laginimaineb

1
Ne yazık ki, bu durumda StringBuffer kullanmanız gerekiyor; appendXXX () yöntemlerinin beklediği şey budur. Java 4'ten beri buradalar ve StringBuilder Java 5'e kadar eklenmedi. Yine de söylediğiniz gibi, önemli değil, sadece sinir bozucu.
Alan Moore

4
Bir şey daha var: replaceXXX () yöntemleri gibi appendReplacement (), $ 1, $ 2 vb. Gibi yakalama grubu referanslarını arar ve bunları ilişkili yakalama gruplarındaki metinle değiştirir. Değiştirme metniniz dolar işaretleri veya ters eğik çizgiler (dolar işaretlerinden kaçmak için kullanılan) içeriyorsa, bir sorununuz olabilir. Bununla başa çıkmanın en kolay yolu, yukarıdaki kodda yaptığım gibi, ekleme işlemini iki adıma bölmektir.
Alan Moore

Alan - bunu fark etmenden çok etkilendim. Bu kadar basit bir problemi çözmenin bu kadar zor olacağını düşünmemiştim!
Mark

Yanıtlar:


65

En etkili yol, ifadeleri sürekli olarak bulmak ve değiştirmek için bir eşleştirici kullanmak ve ardından metni bir dize oluşturucuya eklemek olacaktır:

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
Eşleşmeyen metni kopyalamak için Matcher'in appendReplacement () ve appendTail () yöntemlerini kullanmam dışında, bunu böyle yapardım; bunu elle yapmaya gerek yok.
Alan Moore

5
Aslında appendReplacement () ve appentTail () yöntemleri, senkronize edilmiş (burada kullanılmayan) bir StringBuffer gerektirir. Verilen cevap, testlerimde% 20 daha hızlı olan bir StringBuilder kullanıyor.
dube

103

Bunun için bir şablon motoru veya buna benzer bir şey kullanmanız gerektiğini gerçekten düşünmüyorum. String.formatYöntemi şu şekilde kullanabilirsiniz :

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

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

4
Bunun bir dezavantajı, parametreleri doğru sıraya
koymanız

Bir diğeri, kendi değiştirme belirteci biçiminizi belirleyememenizdir.
Franz D.

diğeri, dinamik olarak çalışmaması, bir anahtar / değer veri kümesine sahip olabilmesi ve daha sonra herhangi bir dizeye uygulayabilmesi
Brad Parks

43

Maalesef yukarıda bahsedilen rahat yöntem String.format yalnızca Java 1.5'ten itibaren kullanılabilir (bugünlerde oldukça standart olmalı, ancak asla bilemezsiniz). Bunun yerine, yer tutucuları değiştirmek için Java'nın sınıf MessageFormat'ı da kullanabilirsiniz .

"{Sayı}" biçimindeki yer tutucuları destekler, bu nedenle mesajınız "Merhaba {0} Lütfen son tarih olan {1} ekte bulun" şeklinde görünecektir. Bu Dizeler, ResourceBundles kullanılarak kolayca dışsallaştırılabilir (örneğin, birden çok yerel ayar ile yerelleştirme için). Değiştirme, MessageFormat sınıfının statik 'format' yöntemi kullanılarak yapılır:

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'ın adını hatırlayamadım ve bu yanıtı bulmak için ne kadar Google'da yapmam gerektiği biraz aptalca. Herkes ya String.format'mış gibi davranıyor ya da bir 3. taraf kullanıyor ve bu inanılmaz derecede kullanışlı aracı unutuyor.
Patrick

1
Bu, 2004'ten beri mevcut - neden sadece şimdi, 2017'de öğreniyorum? StringBuilder.append()S kapsamındaki bazı kodları yeniden düzenliyorum ve "Elbette daha iyi bir yol var ... daha fazla Pythonic ..." - ve kutsal saçmalık, bence bu yöntem Python'un biçimlendirme yöntemlerinden önce gelebilir. Aslında ... bu 2002'den daha eski olabilir ... Bunun ne zaman ortaya çıktığını bulamıyorum ...
ArtOfWarfare

42

Apache Velocity gibi bir şablon kütüphanesi kullanmayı deneyebilirsiniz.

http://velocity.apache.org/

İşte bir örnek:

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

Çıktı şu şekilde olacaktır:

Merhaba Mark. Lütfen 6 Haziran 2009 tarihinde son ödeme tarihi olan 42123 numaralı ekli faturayı bulun.

Geçmişte hız kullandım. Harika çalışıyor.
Hardwareguy

4
katılıyorum, neden tekerleği yeniden icat ediyor
nesneler

6
Bunun gibi basit bir görev için bütün bir kitaplığı kullanmak biraz abartılıdır. Velocity'nin birçok başka özelliği var ve bunun böyle basit bir görev için uygun olmadığına inanıyorum.
Andrei Ciobanu

24

Karmaşık şablon değişimi için şablon kitaplığını kullanabilirsiniz.

FreeMarker çok iyi bir seçimdir.

http://freemarker.sourceforge.net/

Ancak basit görevler için size yardımcı olabilecek basit bir yardımcı program sınıfı vardır.

org.apache.commons.lang3.text.StrSubstitutor

Çok güçlü, özelleştirilebilir ve kullanımı kolaydır.

Bu sınıf bir parça metin alır ve içindeki tüm değişkenlerin yerine geçer. Bir değişkenin varsayılan tanımı $ {variableName} şeklindedir. Önek ve sonek, yapıcılar ve ayar yöntemleri aracılığıyla değiştirilebilir.

Değişken değerler tipik olarak bir haritadan çözümlenir, ancak sistem özelliklerinden veya özel bir değişken çözümleyici sağlanarak da çözülebilir.

Örneğin, sistem ortam değişkenini bir şablon dizesiyle değiştirmek istiyorsanız, kod şu şekildedir:

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 benim için harika çalıştı
ps0604

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

Çıktı: Merhaba Katıl! 10 mesajınız var "


3
John, bir Long olması koşuluyla "spam" klasörümü kontrol ettiğim sıklıkta mesajlarını açıkça kontrol ediyor.
Hemmels

9

Değiştirmek istediğiniz gerçek verilerin nerede olduğuna bağlıdır. Bunun gibi bir Haritanız olabilir:

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

Değiştirilebilecek tüm verileri içeren. Ardından harita üzerinde yineleme yapabilir ve Dize'deki her şeyi aşağıdaki gibi değiştirebilirsiniz:

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

Ayrıca, Dize üzerinde yineleme yapabilir ve öğeleri haritada bulabilirsiniz. Ama bu biraz daha karmaşık çünkü [] için aranan String'i ayrıştırmanız gerekiyor. Pattern ve Matcher'ı kullanarak bunu normal bir ifadeyle yapabilirsiniz.


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

2
Teşekkürler - ama benim durumumda şablon dizesi kullanıcı tarafından değiştirilebilir, bu yüzden belirteçlerin sırasından emin olamıyorum
Mark

3

$ {Değişken} stil belirteçlerini değiştirmeye yönelik çözümüm (buradaki yanıtlardan ve Bahar UriTemplate'inden esinlenilmiştir):

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 ile Stringutils.replaceEach'i kullanabilirsiniz :

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

Gönderen belgeler :

Dizelerin tüm oluşumlarını başka bir Dize içinde değiştirir.

Bu yönteme iletilen boş bir başvuru işlemsizdir veya herhangi bir "arama dizesi" veya "değiştirilecek dizge" boşsa, bu değiştirme yoksayılacaktır. Bu tekrar etmeyecek. Yinelenen değiştirmeler için aşırı yüklenmiş yöntemi çağırın.

 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

Geçmişte bu tür problemleri StringTemplate ve Groovy Şablonları ile çözmüştüm .

Nihayetinde, bir şablonlama motoru kullanıp kullanmama kararı aşağıdaki faktörlere dayanmalıdır:

  • Uygulamada bu şablonların çoğuna sahip olacak mısınız?
  • Uygulamayı yeniden başlatmadan şablonları değiştirme yeteneğine ihtiyacınız var mı?
  • Bu şablonları kim koruyacak? Projede yer alan bir Java programcısı veya iş analisti mi?
  • Değişkenlerdeki değerlere dayalı koşullu metin gibi şablonlarınıza mantık koymanız gerekecek mi?
  • Bir şablona başka şablonlar ekleme yeteneğine ihtiyacınız olacak mı?

Yukarıdakilerden herhangi biri projeniz için geçerliyse, çoğu bu işlevi ve daha fazlasını sağlayan bir şablon oluşturma motoru kullanmayı düşünürdüm.


0

kullandım

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

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

2
Bu işe yarar, ancak benim durumumda şablon dizesi kullanıcı tarafından özelleştirilebilir, bu nedenle belirteçlerin hangi sırayla görüneceğini bilmiyorum.
Mark

0

Aşağıdaki, formun değişkenlerini <<VAR>>bir Haritadan aranan değerlerle değiştirir . Burada çevrimiçi olarak test edebilirsiniz

Örneğin, aşağıdaki girdi dizesiyle

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

ve aşağıdaki değişken değerleri

Weight, 42
Height, HEIGHT 51

aşağıdaki çıktıyı verir

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

Hi there 42 was here

İşte kod

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

ve istenmese de, bir dizedeki değişkenleri application.properties dosyanızdaki özelliklerle değiştirmek için benzer bir yaklaşım kullanabilirsiniz, ancak bu zaten yapılıyor olabilir:

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;
    }
  }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.