Làm cách nào để thay thế một tập hợp các mã thông báo trong Chuỗi Java?


106

Tôi có mẫu chuỗi sau: "Hello [Name] Please find attached [Invoice Number] which is due on [Due Date]".

Tôi cũng có các biến Chuỗi cho tên, số hóa đơn và ngày đến hạn - cách tốt nhất để thay thế các mã thông báo trong mẫu bằng các biến là gì?

(Lưu ý rằng nếu một biến có chứa một mã thông báo thì nó KHÔNG được thay thế).


BIÊN TẬP

Cảm ơn @laginimaineb và @ alan-moore, đây là giải pháp của tôi:

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

Tuy nhiên, một điều cần lưu ý là StringBuffer giống như StringBuilder vừa được đồng bộ hóa. Tuy nhiên, vì trong ví dụ này, bạn không cần phải đồng bộ hóa việc xây dựng Chuỗi, nên tốt hơn hết bạn nên sử dụng StringBuilder (mặc dù việc mua khóa gần như là một hoạt động không tốn phí).
laginimaineb

1
Thật không may, bạn phải sử dụng StringBuffer trong trường hợp này; đó là những gì các phương thức appendXXX () mong đợi. Chúng đã xuất hiện từ Java 4 và StringBuilder đã không được thêm vào cho đến Java 5. Như bạn đã nói, nó không có vấn đề gì lớn, chỉ gây khó chịu.
Alan Moore

4
Một điều nữa: appendReplacement (), giống như các phương thức ReplaceXXX (), tìm kiếm các tham chiếu nhóm chụp như $ 1, $ 2, v.v. và thay thế chúng bằng văn bản từ các nhóm chụp được liên kết. Nếu văn bản thay thế của bạn có thể chứa ký hiệu đô la hoặc dấu gạch chéo ngược (được sử dụng để thoát khỏi ký hiệu đô la), bạn có thể gặp sự cố. Cách dễ nhất để giải quyết vấn đề đó là chia thao tác nối thành hai bước như tôi đã thực hiện trong đoạn mã trên.
Alan Moore

Alan - rất ấn tượng khi bạn phát hiện ra điều đó. Tôi không nghĩ rằng một vấn đề đơn giản như vậy lại khó giải quyết như vậy!
Đánh dấu

Câu trả lời:


65

Cách hiệu quả nhất sẽ là sử dụng trình so khớp để liên tục tìm các biểu thức và thay thế chúng, sau đó nối văn bản vào trình tạo chuỗi:

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
Đây là cách tôi sẽ làm điều đó, ngoại trừ tôi sẽ sử dụng các phương thức appendReplacement () và appendTail () của Matcher để sao chép văn bản chưa khớp; không cần phải làm điều đó bằng tay.
Alan Moore

5
Trên thực tế, các phương thức appendReplacement () và appentTail () yêu cầu một StringBuffer, được snychro hóa (ở đây không dùng đến). Câu trả lời đã cho sử dụng StringBuilder, nhanh hơn 20% trong các thử nghiệm của tôi.
dube

103

Tôi thực sự không nghĩ rằng bạn cần sử dụng một công cụ tạo khuôn mẫu hoặc bất cứ thứ gì tương tự cho việc này. Bạn có thể sử dụng String.formatphương pháp như sau:

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

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

4
Một nhược điểm của việc này là bạn cần phải đặt các thông số trong đúng trật tự
gerrytan

Một điều khác là bạn không thể chỉ định định dạng mã thông báo thay thế của riêng mình.
Franz D.

khác là nó không hoạt động động, có thể có một tập dữ liệu gồm các khóa / giá trị và sau đó áp dụng nó cho bất kỳ chuỗi nào
Brad Parks

43

Thật không may, phương thức thoải mái String.format được đề cập ở trên chỉ có sẵn bắt đầu với Java 1.5 (ngày nay sẽ khá chuẩn, nhưng bạn chưa bao giờ biết). Thay vào đó, bạn cũng có thể sử dụng lớp MessageFormat của Java để thay thế các trình giữ chỗ.

Nó hỗ trợ trình giữ chỗ ở dạng '{number}', vì vậy thông báo của bạn sẽ giống như "Xin chào {0} Vui lòng tìm tệp đính kèm {1} đến hạn vào {2}". Các Chuỗi này có thể dễ dàng được ngoại hóa bằng ResourceBundles (ví dụ: để bản địa hóa với nhiều ngôn ngữ). Việc thay thế sẽ được thực hiện bằng phương thức static'format 'của lớp 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
Tôi không thể nhớ tên của MessageFormat, và thật ngớ ngẩn khi tôi phải làm Google Google để tìm ra câu trả lời này. Mọi người đều hành động như thể đó là String.format hoặc sử dụng bên thứ 3 mà quên mất tiện ích cực kỳ hữu ích này.
Patrick

1
Điều này đã có từ năm 2004 - tại sao tôi chỉ mới biết đến năm 2017? Tôi đang cấu trúc lại một số mã có trong StringBuilder.append()s và tôi đang nghĩ "Chắc chắn có một cách tốt hơn ... một cái gì đó giống Pythonic hơn ..." - và thánh tào lao, tôi nghĩ rằng phương pháp này có thể có trước các phương pháp định dạng của Python. Trên thực tế ... điều này có thể lớn hơn 2002 ... Tôi không thể tìm thấy khi điều này thực sự ra đời ...
ArtOfWarfare

42

Bạn có thể thử sử dụng thư viện tạo khuôn mẫu như Apache Velocity.

http://velocity.apache.org/

Đây là một ví dụ:

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

Đầu ra sẽ là:

Xin chào Mark. Vui lòng tìm hóa đơn đính kèm 42123 đến hạn vào ngày 6 tháng 6 năm 2009.

Tôi đã sử dụng vận tốc trong quá khứ. Hoạt động tuyệt vời.
Phần cứng mua

4
đồng ý, tại sao minh lại bánh xe
đối tượng

6
Sẽ hơi quá mức cần thiết khi sử dụng toàn bộ thư viện cho một nhiệm vụ đơn giản như thế này. Velocity có rất nhiều tính năng khác và tôi thực sự tin rằng nó không phù hợp với một nhiệm vụ đơn giản như thế này.
Andrei Ciobanu

24

Bạn có thể sử dụng thư viện mẫu để thay thế mẫu phức tạp.

FreeMarker là một lựa chọn rất tốt.

http://freemarker.sourceforge.net/

Nhưng đối với nhiệm vụ đơn giản, có một lớp tiện ích đơn giản có thể giúp bạn.

org.apache.commons.lang3.text.StrSubstitutor

Nó rất mạnh mẽ, có thể tùy chỉnh và dễ sử dụng.

Lớp này nhận một đoạn văn bản và thay thế tất cả các biến bên trong nó. Định nghĩa mặc định của một biến là $ {variableName}. Tiền tố và hậu tố có thể được thay đổi thông qua các hàm tạo và các phương thức đặt.

Các giá trị biến thường được giải quyết từ bản đồ, nhưng cũng có thể được giải quyết từ các thuộc tính hệ thống hoặc bằng cách cung cấp trình phân giải biến tùy chỉnh.

Ví dụ: nếu bạn muốn thay thế biến môi trường hệ thống thành một chuỗi mẫu, đây là mã:

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 đã làm việc rất tốt cho tôi
ps0604

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

Đầu ra: Xin chào Tham gia! Bạn có 10 tin nhắn "


2
John rõ ràng kiểm tra tin nhắn của mình thường xuyên khi tôi kiểm tra thư mục "thư rác" của tôi vì nó là Long.
Hemmels

9

Nó phụ thuộc vào vị trí của dữ liệu thực tế mà bạn muốn thay thế. Bạn có thể có một Bản đồ như thế này:

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

chứa tất cả dữ liệu có thể được thay thế. Sau đó, bạn có thể lặp lại bản đồ và thay đổi mọi thứ trong Chuỗi như sau:

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

Bạn cũng có thể lặp lại Chuỗi và tìm các phần tử trong bản đồ. Nhưng điều đó phức tạp hơn một chút vì bạn cần phải phân tích cú pháp Chuỗi đang tìm kiếm []. Bạn có thể làm điều đó với một biểu thức chính quy bằng cách sử dụng Mẫu và Đối sánh.


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

1
Cảm ơn - nhưng trong trường hợp của tôi, người dùng có thể sửa đổi chuỗi mẫu, vì vậy tôi không thể chắc chắn về thứ tự của các mã thông báo
Mark

3

Giải pháp của tôi để thay thế mã thông báo kiểu $ {variable} (lấy cảm hứng từ các câu trả lời ở đây và bởi 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

Với Apache Commons Library, bạn chỉ cần sử dụng Stringutils.replaceEach :

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

Từ tài liệu :

Thay thế tất cả các lần xuất hiện của Chuỗi trong một Chuỗi khác.

Tham chiếu null được chuyển đến phương thức này là không tham gia hoặc nếu bất kỳ "chuỗi tìm kiếm" hoặc "chuỗi cần thay thế" nào là rỗng, thì thay thế đó sẽ bị bỏ qua. Điều này sẽ không lặp lại. Để lặp lại thay thế, hãy gọi phương thức nạp chồng.

 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

Trước đây, tôi đã giải quyết loại vấn đề này với StringTemplateGroovy Templates .

Cuối cùng, quyết định sử dụng động cơ tạo khuôn mẫu hay không phải dựa trên các yếu tố sau:

  • Bạn sẽ có nhiều mẫu này trong ứng dụng?
  • Bạn có cần khả năng sửa đổi các mẫu mà không cần khởi động lại ứng dụng không?
  • Ai sẽ duy trì các mẫu này? Một lập trình viên Java hay một nhà phân tích kinh doanh tham gia vào dự án?
  • Bạn sẽ cần khả năng đưa logic vào các mẫu của mình, chẳng hạn như văn bản có điều kiện dựa trên các giá trị trong các biến?
  • Bạn có cần khả năng đưa các mẫu khác vào một mẫu không?

Nếu bất kỳ điều nào ở trên áp dụng cho dự án của bạn, tôi sẽ xem xét sử dụng công cụ tạo khuôn mẫu, hầu hết trong số đó cung cấp chức năng này và hơn thế nữa.


0

Tôi đã sử dụng

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

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

2
Điều đó sẽ hoạt động, nhưng trong trường hợp của tôi, chuỗi mẫu có thể tùy chỉnh bởi người dùng, vì vậy tôi không biết các mã thông báo sẽ xuất hiện theo thứ tự nào.
Đánh dấu

0

Phần sau thay thế các biến của biểu mẫu <<VAR>>, với các giá trị được tra cứu từ Bản đồ. Bạn có thể kiểm tra nó trực tuyến tại đây

Ví dụ: với chuỗi đầu vào sau

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

và các giá trị biến sau

Weight, 42
Height, HEIGHT 51

kết quả sau

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

Hi there 42 was here

Đây là mã

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

và mặc dù không được yêu cầu, bạn có thể sử dụng một phương pháp tương tự để thay thế các biến trong một chuỗi bằng các thuộc tính từ tệp application.properties của bạn, mặc dù điều này có thể đã được thực hiện:

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;
    }
  }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.