จะแทนที่ชุดโทเค็นใน Java String ได้อย่างไร?


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 อย่างที่คุณพูดมันไม่ใช่เรื่องใหญ่ แต่น่ารำคาญ
Alan Moore

4
อีกอย่างหนึ่ง: appendReplacement () เช่นเมธอด replaceXXX () ค้นหาการอ้างอิงกลุ่มการจับภาพเช่น $ 1, $ 2 เป็นต้นและแทนที่ด้วยข้อความจากกลุ่มการบันทึกที่เกี่ยวข้อง หากข้อความแทนที่ของคุณอาจมีเครื่องหมายดอลลาร์หรือแบ็กสแลช (ซึ่งใช้เพื่อหลีกเลี่ยงเครื่องหมายดอลลาร์) คุณอาจมีปัญหา วิธีที่ง่ายที่สุดในการจัดการคือแบ่งการดำเนินการต่อท้ายออกเป็นสองขั้นตอนตามที่ฉันได้ทำในโค้ดด้านบน
Alan Moore

อลัน - ประทับใจมากที่คุณเห็นสิ่งนั้น ฉันไม่คิดว่าปัญหาง่ายๆแบบนี้จะแก้ยากขนาดนี้!
มาร์ค

คำตอบ:


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
นี่คือวิธีที่ฉันจะทำยกเว้นฉันจะใช้วิธี appendReplacement () และ appendTail () ของ Matcher เพื่อคัดลอกข้อความที่ไม่ตรงกัน ไม่จำเป็นต้องทำด้วยมือ
Alan Moore

5
จริงๆแล้ว appendReplacement () และ appentTail () เมธอดต้องการ StringBuffer ซึ่งเป็นแบบ snychronized (ซึ่งไม่มีประโยชน์ที่นี่) คำตอบที่ให้มาใช้ StringBuilder ซึ่งเร็วกว่า 20% ในการทดสอบของฉัน
dube

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
ข้อเสียอย่างหนึ่งคือคุณต้องใส่พารามิเตอร์ให้ถูกต้อง
gerrytan

อีกประการหนึ่งคือคุณไม่สามารถระบุรูปแบบโทเค็นทดแทนของคุณเองได้
Franz D.

อีกประการหนึ่งก็คือมันไม่ทำงานแบบไดนามิกสามารถมีชุดข้อมูลของคีย์ / ค่าแล้วนำไปใช้กับสตริงใดก็ได้
Brad Parks

43

น่าเสียดายที่วิธีการที่สะดวกสบาย String.format ที่กล่าวถึงข้างต้นนั้นสามารถใช้งานได้โดยเริ่มต้นด้วย Java 1.5 เท่านั้น (ซึ่งน่าจะเป็นมาตรฐานที่ดีในปัจจุบัน แต่คุณไม่เคยรู้) แทนที่จะเป็นเช่นนั้นคุณอาจใช้MessageFormat คลาสของ Java เพื่อแทนที่ตัวยึดตำแหน่ง

สนับสนุนตัวยึดตำแหน่งในรูปแบบ "{number}" ดังนั้นข้อความของคุณจะมีลักษณะเป็น "สวัสดี {0} โปรดดูเอกสารแนบ {1} ซึ่งครบกำหนดในวันที่ {2}" สตริงเหล่านี้สามารถทำให้ภายนอกได้อย่างง่ายดายโดยใช้ ResourceBundles (เช่นสำหรับการแปลด้วยหลายโลแคล) การแทนที่จะทำได้โดยใช้เมธอด static'format 'ของคลาส 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
ฉันจำชื่อ MessageFormat ไม่ได้และมันเป็นเรื่องโง่มากที่ Googling ต้องทำเพื่อหาคำตอบนี้ ทุกคนทำเหมือนว่าเป็น String.format หรือใช้บุคคลที่สามลืมยูทิลิตี้ที่มีประโยชน์อย่างเหลือเชื่อ
Patrick

1
สิ่งนี้มีให้บริการตั้งแต่ปี 2004 - ทำไมฉันเพิ่งเรียนรู้ในตอนนี้ในปี 2017 ฉันกำลังปรับโครงสร้างโค้ดบางส่วนที่อยู่ในStringBuilder.append()s และฉันก็คิดว่า "แน่นอนว่ามีวิธีที่ดีกว่า ... มีอะไรมากกว่า Pythonic ... " - และอึศักดิ์สิทธิ์ฉันคิดว่าวิธีนี้อาจใช้วิธีการจัดรูปแบบของ Python ก่อนหน้านี้ อันที่จริง ... อาจจะเก่ากว่าปี 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);
    }
}

ผลลัพธ์จะเป็น:

สวัสดีมาร์ค โปรดดูแนบใบแจ้งหนี้ 42123 ซึ่งครบกำหนดชำระในวันที่ 6 มิถุนายน 2552

ฉันเคยใช้ความเร็วในอดีต ใช้งานได้ดี
Hardwareguy

4
เห็นด้วยทำไมต้องสร้างล้อใหม่
วัตถุ

6
การใช้ไลบรารีทั้งหมดสำหรับงานง่ายๆเช่นนี้เป็นเรื่องที่ยากเกินไป Velocity มีคุณสมบัติอื่น ๆ อีกมากมายและฉันเชื่อเป็นอย่างยิ่งว่ามันไม่เหมาะกับงานง่ายๆแบบนี้
Andrei Ciobanu

24

คุณสามารถใช้ไลบรารีเทมเพลตสำหรับการแทนที่เทมเพลตที่ซับซ้อน

FreeMarker เป็นทางเลือกที่ดีมาก

http://freemarker.sourceforge.net/

แต่สำหรับงานง่ายๆมีคลาสยูทิลิตี้ง่ายๆสามารถช่วยคุณได้

org.apache.commons.lang3.text.StrSubstitutor

มีประสิทธิภาพมากปรับแต่งได้และใช้งานง่าย

คลาสนี้ใช้ข้อความส่วนหนึ่งและแทนที่ตัวแปรทั้งหมดที่อยู่ในนั้น นิยามเริ่มต้นของตัวแปรคือ $ {variableName} คำนำหน้าและคำต่อท้ายสามารถเปลี่ยนแปลงได้โดยใช้ตัวสร้างและกำหนดวิธีการ

โดยทั่วไปค่าตัวแปรจะได้รับการแก้ไขจากแผนที่ แต่ยังสามารถแก้ไขได้จากคุณสมบัติของระบบหรือโดยการจัดหาตัวแก้ไขตัวแปรที่กำหนดเอง

ตัวอย่างเช่นหากคุณต้องการแทนที่ตัวแปรสภาพแวดล้อมระบบเป็นสตริงเทมเพลตนี่คือรหัส:

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

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

เอาท์พุท: สวัสดีเข้าร่วม! คุณมี 10 ข้อความ "


3
จอห์นตรวจสอบข้อความของเขาอย่างชัดเจนบ่อยครั้งเท่าที่ฉันตรวจสอบโฟลเดอร์ "สแปม" เนื่องจากเป็นแบบยาว
Hemmels

9

ขึ้นอยู่กับตำแหน่งของข้อมูลจริงที่คุณต้องการแทนที่ คุณอาจมีแผนที่ดังนี้:

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

มีข้อมูลทั้งหมดที่สามารถแทนที่ได้ จากนั้นคุณสามารถทำซ้ำบนแผนที่และเปลี่ยนทุกอย่างใน String ได้ดังนี้:

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

นอกจากนี้คุณยังสามารถวนซ้ำบน String และค้นหาองค์ประกอบในแผนที่ แต่นั่นซับซ้อนกว่าเล็กน้อยเพราะคุณต้องแยกวิเคราะห์ String เพื่อค้นหา [] คุณสามารถทำได้ด้วยนิพจน์ทั่วไปโดยใช้ Pattern และ Matcher


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

2
ขอบคุณ - แต่ในกรณีของฉันผู้ใช้สามารถแก้ไขสตริงเทมเพลตได้ดังนั้นฉันจึงไม่แน่ใจในลำดับของโทเค็น
ทำเครื่องหมาย

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)

จากเอกสารประกอบ :

แทนที่สตริงที่เกิดขึ้นทั้งหมดภายในสตริงอื่น

การอ้างอิงค่าว่างที่ส่งไปยังเมธอดนี้คือ no-op หรือหาก "สตริงการค้นหา" หรือ "สตริงที่จะแทนที่" เป็นโมฆะการแทนที่นั้นจะถูกละเว้น สิ่งนี้จะไม่เกิดซ้ำ สำหรับการแทนที่ซ้ำให้เรียกใช้เมธอดโอเวอร์โหลด

 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"

1

FYI

ในภาษาใหม่ Kotlin คุณสามารถใช้ "String Templates" ในซอร์สโค้ดของคุณได้โดยตรงโดยไม่ต้องใช้ไลบรารีของบุคคลที่สามหรือเอ็นจินแม่แบบในการเปลี่ยนตัวแปร

มันเป็นคุณสมบัติของภาษาเอง

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


0

ในอดีตที่ผ่านมาผมได้แก้ไขปัญหาชนิดนี้กับStringTemplateและGroovy แม่

ท้ายที่สุดแล้วการตัดสินใจใช้เครื่องมือสร้างเทมเพลตหรือไม่ควรขึ้นอยู่กับปัจจัยต่อไปนี้:

  • คุณจะมีเทมเพลตเหล่านี้มากมายในแอปพลิเคชันหรือไม่?
  • คุณต้องการความสามารถในการปรับเปลี่ยนเทมเพลตโดยไม่ต้องรีสตาร์ทแอปพลิเคชันหรือไม่?
  • ใครจะเป็นผู้ดูแลเทมเพลตเหล่านี้ โปรแกรมเมอร์ Java หรือนักวิเคราะห์ธุรกิจที่เกี่ยวข้องกับโครงการ?
  • คุณต้องการความสามารถในการใส่ตรรกะในเทมเพลตของคุณเช่นข้อความเงื่อนไขตามค่าในตัวแปรหรือไม่?
  • คุณต้องการความสามารถในการรวมเทมเพลตอื่น ๆ ในเทมเพลตหรือไม่?

หากข้อใดข้อหนึ่งข้างต้นใช้กับโครงการของคุณฉันจะพิจารณาใช้เครื่องมือสร้างเทมเพลตซึ่งส่วนใหญ่มีฟังก์ชันนี้และอื่น ๆ


0

ฉันใช้

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

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

2
วิธีนี้ใช้ได้ แต่ในกรณีของฉันสตริงเทมเพลตสามารถปรับแต่งได้โดยผู้ใช้ดังนั้นฉันจึงไม่รู้ว่าโทเค็นจะปรากฏในลำดับใด
ทำเครื่องหมาย

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.