Java String에서 토큰 세트를 대체하는 방법은 무엇입니까?


106

다음 템플릿 String : "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 09-06-06

1
불행히도이 경우에는 StringBuffer를 사용해야합니다. appendXXX () 메소드가 기대하는 것입니다. Java 4 이후로 사용되어 왔으며 StringBuilder는 Java 5까지 추가되지 않았습니다. 말씀 하셨듯이 큰 문제는 아닙니다.
Alan Moore

4
한 가지 더 : replaceXXX () 메서드와 같은 appendReplacement ()는 $ 1, $ 2 등과 같은 캡처 그룹 참조를 찾아 관련 캡처 그룹의 텍스트로 바꿉니다. 대체 텍스트에 달러 기호 또는 백 슬래시 (달러 기호를 이스케이프하는 데 사용됨)가 포함되어 있으면 문제가있을 수 있습니다. 이를 처리하는 가장 쉬운 방법은 위 코드에서 수행 한 것처럼 추가 작업을 두 단계로 나누는 것입니다.
Alan Moore

앨런-당신이 그것을 발견 한 것에 매우 감동했습니다. 그런 간단한 문제는 해결하기가 그렇게 어려울 것이라고 생각하지 않았습니다!
Mark

답변:


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
Matcher의 appendReplacement () 및 appendTail () 메서드를 사용하여 일치하지 않는 텍스트를 복사하는 것을 제외하고는 이렇게합니다. 손으로 할 필요가 없습니다.
Alan Moore

5
실제로 appendReplacement () 및 appentTail () 메서드에는 snychronized (여기서는 사용되지 않음) 인 StringBuffer가 필요합니다. 주어진 대답은 내 테스트에서 20 % 더 빠른 StringBuilder를 사용합니다.
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.

또 다른는 키 / 값의 데이터 세트를 한 후 모든 문자열에 적용 할 수있는 동적 작업을 나던이다
브래드 공원

43

안타깝게도 위에서 언급 한 편리한 String.format 메서드는 Java 1.5 (요즘에는 꽤 표준이되어야하지만 알 수 없습니다)부터 만 사용할 수 있습니다. 대신 자리 표시자를 대체하기 위해 Java의 클래스 MessageFormat 을 사용할 수도 있습니다 .

'{number}'형식의 자리 표시자를 지원하므로 메시지는 "안녕하세요 {0} {2}에 제출해야하는 첨부 된 {1}을 (를) 찾으세요"와 같이 표시됩니다. 이러한 문자열은 ResourceBundles를 사용하여 쉽게 외부화 할 수 있습니다 (예 : 여러 로케일로 지역화). 대체는 MessageFormat 클래스의 static'format '메소드를 사용하여 수행됩니다.

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의 이름을 기억할 수 없었고,이 답변조차 찾기 위해 얼마나 많은 인터넷 검색을해야했는지는 어리석은 일입니다. 모두가 String.format 또는 타사를 사용하는 것처럼 행동하며이 매우 유용한 유틸리티를 잊어 버립니다.
Patrick

1
이것은 2004 년부터 가능했습니다. 왜 지금은 2017 년에만 배우는 것일까 요? 나는 StringBuilder.append()s 에서 다루는 코드를 리팩토링 하고 있는데 "확실히 더 나은 방법이있다 ... 뭔가 더 Pythonic ..."이라고 생각하고 있었다. 이런 똥, 나는이 방법이 파이썬의 형식화 방법보다 앞선다고 생각한다. 이 실제로 존재로 왔을 때 사실 ...이 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);
    }
}

출력은 다음과 같습니다.

안녕, 마크. 2009 년 6 월 6 일 마감 인 첨부 된 송장 42123을 찾으십시오.

나는 과거에 속도를 사용했습니다. 잘 작동합니다.
Hardwareguy

4
바퀴를 재발견 왜 동의
객체

6
이와 같은 간단한 작업을 위해 전체 라이브러리를 사용하는 것은 약간 과잉입니다. Velocity에는 다른 많은 기능이 있으며, 이와 같은 간단한 작업에는 적합하지 않다고 굳게 믿습니다.
Andrei Ciobanu 2011-06-15

24

복잡한 템플릿 교체를 위해 템플릿 라이브러리를 사용할 수 있습니다.

FreeMarker는 아주 좋은 선택입니다.

http://freemarker.sourceforge.net/

그러나 간단한 작업의 경우 간단한 유틸리티 클래스가 도움이 될 수 있습니다.

org.apache.commons.lang3.text.StrSubstitutor

매우 강력하고 사용자 정의가 가능하며 사용하기 쉽습니다.

이 클래스는 텍스트 조각을 가져와 그 안의 모든 변수를 대체합니다. 변수의 기본 정의는 $ {variableName}입니다. 접두사와 접미사는 생성자와 set 메서드를 통해 변경할 수 있습니다.

변수 값은 일반적으로 맵에서 확인되지만 시스템 속성에서 확인하거나 사용자 지정 변수 확인자를 제공하여 확인할 수도 있습니다.

예를 들어, 시스템 환경 변수를 템플릿 문자열로 대체하려는 경우 다음 코드가 있습니다.

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가 저에게 잘
맞았

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

출력 : Hello Join! 10 개의 메시지가 있습니다. "


2
John은 내 "스팸"폴더가 Long이라는 점을 감안할 때 자주 자신의 메시지를 명확하게 확인합니다.
Hemmels

9

대체하려는 실제 데이터가있는 위치에 따라 다릅니다. 다음과 같은지도가있을 수 있습니다.

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

대체 할 수있는 모든 데이터를 포함합니다. 그런 다음 맵을 반복하고 다음과 같이 문자열의 모든 것을 변경할 수 있습니다.

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

문자열을 반복하고 맵에서 요소를 찾을 수도 있습니다. 그러나 []를 검색하는 문자열을 구문 분석해야하기 때문에 조금 더 복잡합니다. Pattern 및 Matcher를 사용하여 정규식으로 수행 할 수 있습니다.


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

1
덕분에 - 나는 토큰의 순서로 확신 할 수 있지만 내 경우에 템플릿 문자열은 사용자에 의해 수정 될 수있다
마크

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)

로부터 문서 :

다른 문자열 내의 모든 문자열을 대체합니다.

이 메서드에 전달 된 null 참조는 작동하지 않거나 "검색 문자열"또는 "교체 할 문자열"이 null 인 경우 해당 교체가 무시됩니다. 반복되지 않습니다. 반복 교체의 경우 오버로드 된 메서드를 호출합니다.

 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

과거에는 StringTemplateGroovy Templates로 이러한 종류의 문제를 해결했습니다 .

궁극적으로 템플릿 엔진 사용 여부는 다음 요소를 기반으로 결정해야합니다.

  • 응용 프로그램에 이러한 템플릿이 많이 있습니까?
  • 응용 프로그램을 다시 시작하지 않고 템플릿을 수정할 수있는 기능이 필요합니까?
  • 이 템플릿은 누가 유지합니까? 프로젝트에 참여한 Java 프로그래머 또는 비즈니스 분석가?
  • 변수의 값을 기반으로하는 조건부 텍스트와 같이 템플릿에 논리를 넣을 수있는 기능이 필요합니까?
  • 템플릿에 다른 템플릿을 포함 할 수있는 기능이 필요합니까?

위의 내용 중 하나가 프로젝트에 적용되는 경우 템플릿 엔진을 사용하는 것이 좋습니다. 템플릿 엔진은 대부분이 기능 등을 제공합니다.


0

나는 사용했다

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

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

2
작동하지만 제 경우에는 사용자가 템플릿 문자열을 사용자 정의 할 수 있으므로 토큰이 어떤 순서로 표시 될지 모르겠습니다.
Mark

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.