[ 정규표현식 시리즈 ]
몰라서 못 썼던 정규 표현식을 알아보자 ④
# 욕심 수량자와 겸허 수량자
*, +, ? 등의 수량자가 포함된 경우 서브 매치의 할당은 앞서 포스팅한 왼쪽부터 오른쪽이라는 원칙으로 설명할 수 있었지만, 욕심 수량자와 겸허 수량자의 차이를 이해한다면 이 원칙을 벗어날 수 있습니다.
수량자 종류 | 욕심 | 겸허 |
스타 | * | *? |
플러스 | + | +? |
물음표 | ? | ?? |
범위 수량자 | {n}, {n,m} | {n}?, {n,m}? |
위 표를 보면 알 수 있듯이 기본적으로 연산자는 욕심의 성질을 가지고 있고 뒤에 물음표을 붙이면 겸허 연산자가 됩니다. 둘의 차이는 캡션에 대하는 동작으로 최대한 많은 패턴을 일치시키려는 욕심 수량자와는 다르게 겸허 수량자는 최대한 적은 글자수를 매칭시키려고 한다고 이해하면 됩니다.
# 욕심 수량자와 겸허 수량자의 동작 차이
'aaa' 라는 문자열을 (a*)(a*) 라는 정규 표현식에 일치시켜 봅시다. 다만, 이것 그대로 한번만 일치시키는 것이 아닌 욕심 수량자와 겸허 수량자로 가능한 경우 모두 일치시키면 아래와 같은 결과가 나옵니다.
public static void main(String[] args) {
String r1 = "(a*)(a*)";
String r2 = "(a*?)(a*)";
String r3 = "(a*)(a*?)";
String r4 = "(a*?)(a*?)";
String example = "aaa";
Matcher m1 = Pattern.compile(r1).matcher(example);
Matcher m2 = Pattern.compile(r2).matcher(example);
Matcher m3 = Pattern.compile(r3).matcher(example);
Matcher m4 = Pattern.compile(r4).matcher(example);
}
}
(a*)(a*) 의 경우, 왼쪽부터 오른쪽이라는 원칙에 맞게 1번 그룹에 aaa 가 전부 매칭되는 것을 볼 수 있습니다. 한편 왼쪽이 겸허 수량자일 때는 이 원칙이 적용되지 않습니다. 왼쪽에 있는 겸허 수량자가, 최소의 패턴인 공백 문자열은 선택하고 우측에 있는 욕심 수량자가 남은 문자열을 가져가게 됩니다.
(a*?)(a*?) 의 경우 양쪽 모두 겸허 수량자로 어느 것도 매치가 되지 않는 것을 알 수 있습니다. 자바의 경우 matcher.find() 는 부분 일치 하기에 공백 문자열도 일치하게 되고 최대한 본인이 적게 매칭을 가져가려는 겸허 수량자의 특성과 맞물려 아무것도 매칭되지 않는 상황이 된 것 입니다.
그렇다면 완전 일치를 시키면 어떻게 나올까요?
// 방법 1.
String r1 = "^(a*)(a*)$";
String r2 = "^(a*?)(a*)$";
String r3 = "^(a*)(a*?)$";
String r4 = "^(a*?)(a*?)$";
// 방법 2.
String r1 = "(a*)(a*)";
String r2 = "(a*?)(a*)";
String r3 = "(a*)(a*?)";
String r4 = "(a*?)(a*?)";
Matcher m1 = Pattern.compile(r1).matcher(example);
if (matcher.matches()) { // matches() 메소드 활용
/* ... */
}
4번째 정규 표현식 (a*?) 에 'aaa' 가 매칭됨을 볼 수 있습니다. 두 겸허 연산자는 최대한 적은 문자열을 본인에게 매칭 시키기 위해서 서로 양보합니다.
하지만, 정규 표현식의 매칭의 중요 법칙인 왼쪽부터 오른쪽에 따라서, 왼쪽에 있는 겸허 연산자의 요청이 더 우선순위가 높게 적용되어 오른쪽의 캡쳐에 다 할당되는 것 입니다.
# 겸허 수량자의 사용 예시
겸허 수량자는 큰 따옴표(") 로 감싼 문자열을 정규 표현식을 이용해서 파싱할 때 편리하게 작용합니다.
(".*") 라는 정규 표현식에 "apple", "banana", "melon" 이라는 문자열을 일치 시키면, 따로 나오는 것이 나닌 문자열 전체 덩어리가 캡쳐되어 나왔습니다.
"apple", "banana", "melon"
붉은색이 ", 회색이 .* 에 일치하는 모습입니다. 정규 표현식 입장에서 틀린 것은 아닌지만 개발자의 의도를 정확히 표현하지 못한 것이지요. 겸허 수량자를 활용한 해결 방법으로는 아래와 같이 겸허 수량자와 while (matcher.find()) 를 통해 매칭 결과를 출력할 수 있습니다.
// 겸허 수량자의 사용 예시
String re1 = "(\".*\")";
String re2 = "(\".*?\")";
String example_2 = "\"apple\", \"banana\", \"melon\"";
Matcher m5 = Pattern.compile(re1).matcher(example_2);
Matcher m6 = Pattern.compile(re2).matcher(example_2);
if (m5.find()) {
System.out.println("match[1] : " + m5.group(1));
}
int matchIdx = 1;
while (m6.find()) {
System.out.print("match[" + matchIdx + "] : " + m6.group() + " ");
matchIdx++;
}
# 정규 표현식을 활용한 문자열 치환
정규 표현식은 문자열을 치환하는데도 사용됩니다. 자바에서는 String.replaceAll() 메소드를 통해 치환 기능을 제공합니다.
첫번째 매개변수에는 정규 표현식(regex)을 넣고 두번째 매개변수에는 치환할 문자열을 넣으면 됩니다.
대체할 문자열에는 아래의 문자들을 사용해서 사용할 수도 있습니다.
패턴 | 기능 |
$$ | $라는 문자열 자체를 삽입합니다. |
$n | n번째 캡처를 삽입합니다. (n <= 99) |
$<name> | 이름 있는 캡처로 가져온 서브패턴을 삽입합니다. |
아래는 사용 예시를 정리했습니다.
public static void main(String[] args) {
// 문자열 치환 예시
String originalString = "The quick brown fox jumps over the lazy dog.";
// 정규 표현식을 사용하여 "quick"을 "slow"로 치환
String newStr = originalString.replaceAll("quick", "slow");
System.out.println("Original: " + originalString);
System.out.println("Modified: " + newStr);
// 정규 표현식에서 $ 기능 사용하기
String regexWithCapture = "(\\w+) fox";
String replacementWithCapture = "$1 cat"; // $1은 첫 번째 캡처 그룹을 참조
String modifiedWithCapture = originalString.replaceAll(regexWithCapture, replacementWithCapture);
System.out.println("Modified with capture: " + modifiedWithCapture);
// 문자열 치환 예시
String originalString_1 = "abc12345#$*%";
// 정규 표현식: (nondigits)(digits)(non-alphanumerics)
String regex = "^(?<alpha>[\\D]*)(?<number>\\d*)(?<special>[^\\w]*)$";
// replaceAll을 사용하여 치환
String newString = originalString_1.replaceAll(regex, "$1 - $2 - $3");
String newString_1 = originalString_1.replaceAll(regex, "${alpha} - ${number} - ${special}");
// 결과 출력
System.out.println(newString); // abc - 12345 - #$*%
System.out.println(newString_1); // abc - 12345 - #$*%
}
참고자료
https://kasterra.github.io/regex4-lazy-operator-and-string-replacement/
'시리즈 > 자바' 카테고리의 다른 글
[TEST] 테스트를 위한 JUnit 5 와 AssertJ ② (0) | 2024.10.25 |
---|---|
[TEST] 테스트를 위한 JUnit 5 와 AssertJ ① (1) | 2024.10.24 |
[JAVA] 몰라서 못 썼던 정규 표현식을 알아보자 ③ (0) | 2024.10.21 |
[JAVA] 몰라서 못 썼던 정규 표현식을 알아보자 ② (1) | 2024.10.20 |
[JAVA] 몰라서 못 썼던 정규 표현식을 알아보자 ① (5) | 2024.10.19 |