1. 개요

Java에서 문자열의 값을 찾거나 대체해야 할 때 일반적 으로 정규식을 사용 합니다 . 이를 통해 문자열의 일부 또는 전체가 패턴 과 일치 하는지 확인할 수 있습니다 . MatcherString 모두 에서 replaceAll 메서드 를  사용하여 문자열의 여러 토큰에 동일한 대체를 쉽게 적용있습니다 .

이 사용방법(예제)에서는 문자열에서 발견된 각 토큰에 대해 다른 대체를 적용하는 방법을 탐색합니다. 이렇게 하면 특정 문자를 이스케이프하거나 자리 표시자 값을 바꾸는 것과 같은 사용 사례를 쉽게 충족할 수 있습니다.

또한 토큰을 올바르게 식별하기 위해 정규 표현식을 조정하는 몇 가지 트릭을 살펴보겠습니다.

2. 매치 개별 처리

토큰별 대체 알고리즘을 구축하기 전에 정규 표현식에 대한 Java API를 이해해야 합니다. 캡처 그룹과 비 캡처 그룹을 사용하여 까다로운 일치 문제를 해결해 보겠습니다.

2.1. 제목 케이스 예

문자열의 모든 제목 단어를 처리하는 알고리즘을 구축한다고 가정해 보겠습니다. 이 단어는 하나의 대문자로 시작하여 소문자로만 끝나거나 계속됩니다.

우리의 입력은 다음과 같습니다.

"First 3 Capital Words! then 10 TLAs, I Found"

제목 단어의 정의에서 다음과 같은 일치 항목이 포함됩니다.

  • 첫 번째
  • 수도
  • 단어
  • NS
  • 설립하다

이 패턴을 인식하는 정규식은 다음과 같습니다.

"(?<=^|[^A-Za-z])([A-Z][a-z]*)(?=[^A-Za-z]|$)"

이를 이해하기 위해 구성 요소로 분해해 보겠습니다. 중간부터 시작하겠습니다.

[A-Z]

단일 대문자를 인식합니다.

단일 문자 단어 또는 소문자 뒤에 오는 단어를 허용하므로 다음과 같습니다.

[a-z]*

0개 이상의 소문자를 인식합니다.

어떤 경우에는 위의 두 문자 클래스로 토큰을 인식하기에 충분합니다. 불행히도 예제 텍스트에는 여러 개의 대문자로 시작하는 단어가 있습니다. 따라서 우리가 찾은 하나의 대문자가 문자가 아닌 문자 다음에 가장 먼저 나타나야 함을 표현해야 합니다.

유사하게, 우리가 단일 대문자 단어를 허용할 때, 우리가 찾은 단일 대문자가 다중 대문자 단어의 첫 번째 단어가 아니어야 한다는 것을 표현해야 합니다.

표현 [^A-Za-z]  는 "문자 없음"을 의미합니다. 캡처하지 않는 그룹의 표현식 시작 부분에 다음 중 하나를 넣었습니다.

(?<=^|[^A-Za-z])

비 캡처 그룹과 시작 (? <=, 않는 정확한 경계에 일치 나타납니다을 보장하기 위해 모양 숨김을. 마지막에 그것의 대응은 문자 그 후속에 대해 동일한 작업을 수행합니다.

그러나 단어가 문자열의 맨 처음 또는 끝 부분에 닿는 경우 이를 고려해야 합니다. 여기에 추가한 ^| 첫 번째 그룹에 "문자열 또는 문자가 아닌 문자의 시작"을 의미하도록 하고 마지막 비 캡처 그룹 끝에 |$를 추가하여 문자열의 끝이 경계가 되도록 합니다. .

non-capturing 그룹에서 발견된 캐릭터는 find 를 사용할 때  매치에 나타나지 않습니다 .

이와 같은 간단한 사용 사례에도 많은 경우가 있을 있으므로 정규식을 테스트하는 것이 중요합니다 . 이를 위해 단위 테스트를 작성하거나 IDE의 내장 도구를 사용하거나 Regexr 과 같은 온라인 도구를 사용할 수 있습니다 .

2.2. 예제 테스트

EXAMPLE_INPUT 이라는 상수의 예제 텍스트 TITLE_CASE_PATTERN 이라는  패턴 의 정규 표현식을  사용 하여 Matcher 클래스에서 find사용 하여 단위 테스트에서 모든 일치 항목을 추출해 보겠습니다 .

Matcher matcher = TITLE_CASE_PATTERN.matcher(EXAMPLE_INPUT);
List<String> matches = new ArrayList<>();
while (matcher.find()) {
    matches.add(matcher.group(1));
}

assertThat(matches)
  .containsExactly("First", "Capital", "Words", "I", "Found");

여기에서 우리 Matcher 를 생성하기 위해  Patternmatcher 함수를  사용합니다 . 그런 다음 모든 일치 항목을 반복하기 위해 true  반환을 중지할 때까지 루프에서 find 메서드를  사용합니다 .

때마다  찾기 반환 사실이 는  매처 객체의 상태는 현재의 경기를 나타내는 설정됩니다. group(0) 으로 전체 일치를  검사하거나 1 기반 인덱스로 특정 캡처 그룹을 검사할 수 있습니다. 이 경우 원하는 조각 주위에 캡처 그룹이 있으므로 group(1)사용 하여 일치 항목을 List에 추가합니다.

2.3. 의 검사 Matcher를 좀 더

지금까지 처리할 단어를 찾았습니다.

그러나 이러한 각 단어가 교체하려는 토큰인 경우 결과 문자열을 작성하려면 일치 항목에 대한 추가 정보가 필요합니다. 도움이 될 수 있는 Matcher의 다른 속성을 살펴보겠습니다  .

while (matcher.find()) {
    System.out.println("Match: " + matcher.group(0));
    System.out.println("Start: " + matcher.start());
    System.out.println("End: " + matcher.end());
}

이 코드는 각 경기가 어디에 있는지 보여줍니다. 또한 캡처된 모든 항목인 group(0) 일치를 보여줍니다 .

Match: First
Start: 0
End: 5
Match: Capital
Start: 8
End: 15
Match: Words
Start: 16
End: 21
Match: I
Start: 37
End: 38
... more

여기에서 각 일치 항목에 우리가 기대하는 단어만 포함되어 있음을 알 수 있습니다. 시작 속성을 보여줍니다 경기의 제로로부터 시작되는 인덱스 문자열 내에서.  직후 문자의 인덱스를 보여줍니다. 이것은 우리가 substring(start, end-start) 을 사용하여 원본 문자열에서 각 일치 항목을 추출 할 수 있음을 의미 합니다. 이것이 본질적으로 그룹 메서드가 우리를 위해 수행하는 방식입니다.

이제 find 를 사용하여 일치 항목을 반복 할 수 있으므로 토큰을 처리해 보겠습니다.

3. 매치를 하나씩 교체하기

알고리즘을 사용하여 원래 문자열의 각 제목 단어를 해당하는 소문자로 교체하여 예제를 계속하겠습니다. 이것은 테스트 문자열이 다음으로 변환됨을 의미합니다.

"first 3 capital words! then 10 TLAs, i found"

패턴 과  매처 우리는 알고리즘을 구성 할 수 있도록 클래스는, 우리를 위해이 작업을 수행 할 수 없습니다.

3.1. 대체 알고리즘

알고리즘에 대한 의사 코드는 다음과 같습니다.

  • 빈 출력 문자열로 시작
  • 각 경기에 대해:
    • 매치 이전과 이전 매치 이후에 나온 모든 것을 출력에 추가합니다.
    • 이 일치를 처리하고 출력에 추가하십시오.
    • 모든 일치 항목이 처리될 때까지 계속
    • 마지막 일치 이후에 남은 것을 출력에 추가

이 알고리즘의 목적은 일치하지 않는 모든 영역찾아 출력 에 추가하고 처리된 일치 항목을 추가하는 것입니다.

3.2. Java의 토큰 교체기

간단한 변환 방법을 작성할 수 있도록 각 단어를 소문자로 변환하려고 합니다.

private static String convert(String token) {
    return token.toLowerCase();
}

이제 일치 항목을 반복하는 알고리즘을 작성할 수 있습니다. 출력에 StringBuilder사용할 수 있습니다 .

int lastIndex = 0;
StringBuilder output = new StringBuilder();
Matcher matcher = TITLE_CASE_PATTERN.matcher(original);
while (matcher.find()) {
    output.append(original, lastIndex, matcher.start())
      .append(convert(matcher.group(1)));

    lastIndex = matcher.end();
}
if (lastIndex < original.length()) {
    output.append(original, lastIndex, original.length());
}
return output.toString();

우리는주의해야 모두 StringBuilder가 의 편리한 버전을 제공  APPEND 문자열을 추출 할 수 있습니다 . 이것은 Matcherend 속성  과 잘 작동  하여 마지막 일치 이후 일치하지 않는 모든 문자를 선택할 수 있습니다.

4. 알고리즘 일반화

이제 일부 특정 토큰을 교체하는 문제를 해결했으므로 일반적인 경우에 사용할 수 있는 형식으로 코드를 변환하지 않겠습니까? 구현마다 다른 것은 사용할 정규식과 각 일치 항목을 대체 항목으로 변환하는 논리뿐입니다.

4.1. 기능 및 패턴 입력 사용

Java  Function<Matcher, String> 개체를 사용하여 호출자가 각 일치를 처리하는 논리를 제공할 수 있습니다. 그리고 모든 토큰을 찾기 위해 tokenPattern 이라는 입력을 받을 수 있습니다 .

// same as before
while (matcher.find()) {
    output.append(original, lastIndex, matcher.start())
      .append(converter.apply(matcher));

// same as before

여기서 정규식은 더 이상 하드 코딩되지 않습니다. 대신 변환기 함수는 호출자가 제공하며 찾기 루프 내에서 각 일치 항목에 적용됩니다 .

4.2. 일반 버전 테스트

일반적인 방법이 원본만큼 잘 작동하는지 봅시다.

assertThat(replaceTokens("First 3 Capital Words! then 10 TLAs, I Found",
  TITLE_CASE_PATTERN,
  match -> match.group(1).toLowerCase()))
  .isEqualTo("first 3 capital words! then 10 TLAs, i found");

여기에서 코드를 호출하는 것이 간단하다는 것을 알 수 있습니다. 변환 함수는 람다로 표현하기 쉽습니다. 그리고 테스트는 통과합니다.

이제 토큰 교체 도구가 있으므로 다른 사용 사례를 시도해 보겠습니다.

5. 일부 사용 사례

5.1. 특수 문자 이스케이프

Let's imagine we wanted to use the regular expression escape character \ to manually quote each character of a regular expression rather than use the quote method. Perhaps we are quoting a string as part of creating a regular expression to pass to another library or service, so block quoting the expression won't suffice.

If we can express the pattern that means “a regular expression character”, it's easy to use our algorithm to escape them all:

Pattern regexCharacters = Pattern.compile("[<(\\[{\\\\^\\-=$!|\\]})?*+.>]");

assertThat(replaceTokens("A regex character like [",
  regexCharacters,
  match -> "\\" + match.group()))
  .isEqualTo("A regex character like \\[");

For each match, we prefix the \ character. As \ is a special character in Java strings, it's escaped with another \.

Indeed, this example is covered in extra \ characters as the character class in the pattern for regexCharacters has to quote many of the special characters. This shows the regular expression parser that we're using them to mean their literals, not as regular expression syntax.

5.2. Replacing Placeholders

A common way to express a placeholder is to use a syntax like ${name}. Let's consider a use case where the template “Hi ${name} at ${company}” needs to be populated from a map called placeholderValues:

Map<String, String> placeholderValues = new HashMap<>();
placeholderValues.put("name", "Bill");
placeholderValues.put("company", "Baeldung");

All we need is a good regular expression to find the ${…} tokens:

"\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}"

is one option. It has to quote the $ and the initial curly brace as they would otherwise be treated as regular expression syntax.

At the heart of this pattern is a capturing group for the name of the placeholder. We've used a character class that allows alphanumeric, dashes, and underscores, which should fit most use-cases.

However, to make the code more readable, we've named this capturing group placeholder. Let's see how to use that named capturing group:

assertThat(replaceTokens("Hi ${name} at ${company}",
  "\\$\\{(?<placeholder>[A-Za-z0-9-_]+)}",
  match -> placeholderValues.get(match.group("placeholder"))))
  .isEqualTo("Hi Bill at Baeldung");

Here we can see that getting the value of the named group out of the Matcher just involves using group with the name as the input, rather than the number.

6. Conclusion

이 기사에서는 강력한 정규식을 사용하여 문자열에서 토큰을 찾는 방법을 살펴보았습니다. find 메소드가 Matcher 와 함께 작동 하여 일치 항목을 표시 하는 방법을 배웠습니다 .

그런 다음 토큰별로 교체할 수 있는 알고리즘을 만들고 일반화했습니다.

마지막으로 문자를 이스케이프하고 템플릿을 채우는 몇 ​​가지 일반적인 사용 사례를 살펴보았습니다.

항상 그렇듯이 코드 예제는 GitHub 에서 찾을 수 있습니다 .

Generic footer banner