본문 바로가기

WEB_Programming/Pure Java

Regular Expression > Quantifiers


uantifiers 는 매치될 특정 문자의 발생되는 회수를 지정하는 지정자이다. 일반적으로 Pattern API에는 3가지 섹션으로 greedy, reluctant, possessive 회수 지정자를 아래와 같이 이용하고 있다. 처음에 개략 나타나는 내용을 보면 X?, X??, X?+로 이것은 모두 같은 의미이다. 모두 약속된 매치문자 X에 매치를 수행하며, 한번 나타나거나 아예 나타나지 않는것을 의미한다.
이러한 구현사이에는 미묘한 차이가 잇으며, 이장 끝 부분에서 다시 언급 하도록 할 것이다.

 Quantifiers
 Meaning
 Greedy  Reluctant  Possessive
 X?  X??  X?+  X, once or not at all
 X*  X*?  X*+  X, zero or more times
 X+  X+?  X++  X, one or more times
 X{n}  X{n}?  X{n}+  X, exactly n times
 X{n,}  X{n,}?  X{n,}+  X, at least n times
 X{n,m}  X{n,m}?  X{n,m}+  X, at least n but not more than m times

3개의 서로다른 정규식 표현(?, *, +)에서 greedy를 우선적으로 살펴보고자 한다. 입력 문자를 ""로 공백 문자를 테스트에 입력하게 되었을 경우 확인해보자.

Enter your regex: a?
Enter input string to search:
I found the text "" starting at index 0 and ending at index 0.

Enter your regex: a*
Enter input string to search:
I found the text "" starting at index 0 and ending at index 0.

Enter your regex: a+
Enter input string to search:
No match found.

Zero-Length Matches

이전 예에서 첫번째 두 케이스에서는 매치가 성공적으로 이루어진다. 이러한 이유는 a?와 a* 둘다 a가 한번도 나타나지 않는것을 허용하기 때문이다. 시작과 종료 인덱스가 모두 0임을 확인할 수 있을 것이다. 비어있는 공백 문자 "" 는 길이가 ㅇ벗다. 그래서 테스트는 인덱스 0에서 테스트를 수행하게 된다. 이러한 매치를 zero-length matches라고 한다. zero-length match는 몇몇 클래스에 타나나게 되는데 입력 문자가 비어 있거나, 입력된 문자 스트링의 시작 인덱스에서, 입력된 문자의 마지막 캐릭터에서 그리고 입력된 스트링의 두 문자 사이에서 발생하게 된다. Zero-length 매치는 쉽게 구분할 수 있다. 외냐하면 항상 시작 인덱스와 종료 인덱스가 같기 때문이다.

zeor-length 매치를 몇가지 예를 통해서 확인해보자. 입력 스트링이 단일문자 "a"를 입력했을때, 무언가 흥미로운 부분을 발견하게 될 것이다.

Enter your regex: a?
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.

Enter your regex: a*
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.

Enter your regex: a+
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

"a"에 대해서 3가지 수량 지정자를 확인해보자, 첫번째 2개에서는 zero-length match가 인덱스 1에서 발견되었다. 입력된 마지막 문자에서 발견된 것이다. 기억해야할 것은 "a" 문자가 인덱스 0과 인덱스 1 사이에 있는 셀에 존재하고 잇다는 것이다. 우리의 test harness는 더이상 매치가 발견되기 전까지 반복을 수행한다. 이것은 수량 지정자를 사용하는가에 따라서 그 값이 결정된다.  nothing의 표현이 마지막 문자 이후에 처리가 되는지 아니면 처리되지 않느지에 여부 말이다.

이제 "a"를 5번 발생하도록 입력 스트링을 바꿔보고 테스트를 수행해보자.

Enter your regex: a?
Enter input string to search: aaaaa
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 1 and ending at index 2.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "a" starting at index 3 and ending at index 4.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.

Enter your regex: a*
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.

Enter your regex: a+
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.

a?라는 표현식은 각각 문자 하나하나마다 매칭을 수행한다. "a"가 0 혹은 한번 이상 발생한다. 표현식 a*는 2개의 구분된 부분에서 발생한다. 전체 문자 a에 대해서 처음 발생해서, 마지막 문자인 인덱스 5지점까지 수행하며, zero-length-match는 끝에서 발생한다. 마지막으로 a+매치는 모든 발생하는 "a"에서 수행되며, "nothing"매치는 마지막 부분에서 수행하지 않는다.

포인트는 첫번째 2개의 수량 지정자를 이용했을때 전체 "a"에 대해서 매치를 수행하지 않는다는것이다. 예를 들어 입력 문자를 "ababaaab"라고 했을때 "b"를 만나면 어떻게 될까?
Enter your regex: a?
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "a" starting at index 5 and ending at index 6.
I found the text "a" starting at index 6 and ending at index 7.
I found the text "a" starting at index 7 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.

Enter your regex: a*
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.

Enter your regex: a+
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.

1, 3, 8 위치에 "b"라는 단어가 나타났을때, 이러한 위치에 zero-length 매치가 이러한 위치에서 발생한다. 정규 표현식에서 a?은 "b"를 특별히 찾아야 하는 문자로 생각하지 않는다. 단지 "a"라는 단어에 대해서 검사할 뿐이다. 만챡 "a"가 0번 나타나는 것을 허용한다고 하면, "a"가 아니면 어떠한 문자도 zero-lenghtn 매치를 수행할 것이다. 나머지 매치는 이전에 예제에서 처럼 수행될 것이다.

정확하게 n번 나타나는 검사를 수행할려면 단순하게 {} 에 넣어주면 된다.

Enter your regex: a{3}
Enter input string to search: aa
No match found.

Enter your regex: a{3}
Enter input string to search: aaa
I found the text "aaa" starting at index 0 and ending at index 3.

Enter your regex: a{3}
Enter input string to search: aaaa
I found the text "aaa" starting at index 0 and ending at index 3.

a{3}이라는 정규식 표현은 row에서 "a"라는 단어가 3번 발생했는지 찾는다. 첫번째 테스트는 실패한다. 입력된 문자가 매치되는 문자와 충분히 매칭이 되지 않기 때문이다. 두번째 테스트는 정확하게 3번 a가 발생했으므로 성공한다. 세번째 테스트는 처음부터 3개의 a에 대해서 검사를 수행한다. 중요한 것은 {} 바로 앞에 있는 한 단어에 대해서 매치되는 회수를 검사한다는 것이다.
Enter your regex: a{3}
Enter input string to search: aaaaaaaaa
I found the text "aaa" starting at index 0 and ending at index 3.
I found the text "aaa" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.

적어도 n번 발생하는 체크를 수행하고자 한다면 숫자 다음에 comma를 입력하면 된다.

Enter your regex: a{3,}
Enter input string to search: aaaaaaaaa
I found the text "aaaaaaaaa" starting at index 0 and ending at index 9.

동이한 입력 문자를 넣었을때 오직 한번의 매치작업만 수행한다. 왜냐하면 9개의 a가 있는 로에서 적어도 3번의 a라는 조건을 만족하기 때문이다.

마지막으로 특정 상위 제한을 가지고자 한다면 다음과 같이 넣어주면 된다.

Enter your regex: a{3,6} // find at least 3 (but no more than 6) a's in a row
Enter input string to search: aaaaaaaaa
I found the text "aaaaaa" starting at index 0 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.

첫번재 매치는 상위 6개 문자 이후에서는 매치를 멈춘다. 두번째 매치는 남은 문자에 대해서 매치를 다시 수행한다. 이 매치를 위해서 최소 문자 수를 지정한만큼 매치를 검사한다. 만약 입력 문자가 한문자 작다면 두분째 매치는 이루어 지지 않을 것이다.

캡쳐 그룹과 캐릭터 클래스와 수량의 관계

지금까지 우리는 오직 입력된 문자에서 오직 한단어에 대해서만 매치되는 회수를 검사했다. 사실 수량 지정자는 한번에 오직 하나의 문자만을 검사하고 잇다. "abc+"라고 한다면 이것은 a다음에 b가오고, c가 한번 혹은 그 이상 오는지 검사하는 것이다. 이것은 "abc"라는 단어가 한번 혹은 이상 나오는지 검사하는 것이 아니다. 그러므로 수량 지정자는 오직 Character Classes 와 Capturing Group 에 지정할 수있게 된다. 이러한 방법은 [abc]+로 지정하여 a혹은 , b혹은, c중의 값이 한번 혹은 이상 나오는 것을 의미하고, (abc)+는 abc라는 단어가 한번 혹은 그 이상 나오는지 검사하는 것이다.

다음 특정 그룹인 (dog)의 예를 한번 확인해보자.

Enter your regex: (dog){3}
Enter input string to search: dogdogdogdogdogdog
I found the text "dogdogdog" starting at index 0 and ending at index 9.
I found the text "dogdogdog" starting at index 9 and ending at index 18.

Enter your regex: dog{3}
Enter input string to search: dogdogdogdogdogdog
No match found.

첫번째 예제에서는 3번 매치되는지 검사하는 것으로 capturing group 전체를 검사하도록 하는 것이다. 만약 괄호를 없앤다면 이것은 g라는 단어가 3번 나오는지 검사하는것으로 테스트에 실패하게 된다.

유사하게 우리는 전체 character class에 대해서 검사를 해 볼 수 있다.
Enter your regex: [abc]{3}
Enter input string to search: abccabaaaccbbbc
I found the text "abc" starting at index 0 and ending at index 3.
I found the text "cab" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
I found the text "ccb" starting at index 9 and ending at index 12.
I found the text "bbc" starting at index 12 and ending at index 15.

Enter your regex: abc{3}
Enter input string to search: abccabaaaccbbbc
No match found.
여기에 지정된 수량 지정자 {3} 은 첫번째 엘리먼트에서 전체 캐릭터 클래스에 해당하는 내용을 찾응 것이며, 두번째는 c에 대해서 회수를 검사하게 된다.

Greedy, Reluctant, Possessive 수량 지정자의 차이점

이 세개 greedy, reluctant, possessive 수량 지정자에는 미묘한 차이점이 있다.

Greedy 수량 지정자는 "greedy"를 고려하고 있다. 이것은 읽어들인 문자에 대해서 매우 강력하게 검사를 수행한다. 그리고 전체 입력된 스트링에서 첫번째 매치를 시도하는것을 중요하게 생각한다. 만약 첫번째 매치를 수행할때 실패를 하게 되면, 매처는 입력된 문자에서 한문자를 건너뛰어 다시 검사를 수행한다. 이러한 검사를 반복적으로 수행하여, 더이상 검사할 문자가 없을때까지 이동하면서 검사한다. 이것은 표현식에 따라서 수량지정자에 따라 달라지며, 1 혹은 0문자까지 검사를 시도한다.

reluctant 수량 지정자는 전혀 다른 접근을 수행한다. 입력된 스트링의 시작 위치에서 검사를 수행하고, 매치되는 문자를 찾기위해서 한문자를 삼키고 매치를 시도한다. 마지막으로 입력 문자 전체를 검사하도록 시도하게 된다.

마지막으로 possessive 수량지정자는 항상 전체 입력된 문자에 대해서 오직 한번만 매치를 수행하게 된다. greedy와는 다르게 possessive 수량지정자는 절대로 한단어씩 넘어서 검사를 수행하지 않는다. 만챡 전체 단어에서 매칭이 수행되는 경우에는 정상적인 매칭이 이루어 질것이다.

다음예는 xfooxxxxxxfoo에 대해서 고려한 예제이다.

Enter your regex: .*foo // greedy quantifier
Enter input string to search: xfooxxxxxxfoo
I found the text "xfooxxxxxxfoo" starting at index 0 and ending at index 13.

Enter your regex: .*?foo // reluctant quantifier
Enter input string to search: xfooxxxxxxfoo
I found the text "xfoo" starting at index 0 and ending at index 4.
I found the text "xxxxxxfoo" starting at index 4 and ending at index 13.

Enter your regex: .*+foo // possessive quantifier
Enter input string to search: xfooxxxxxxfoo
No match found.

처음 예는 greedy 수량 지정자의 예로 .*는 "anythind" 의 의미를 아예 없거나, 더 많은 수의 값을 검사하게 되고 다음으로 "f" "o" "o"에 의해서 검사를 수행한다. 수량 지정자가 greedy이기 때문에 .* 부분은 입력된 첫번째 문자를 삼키게 된다. 이 포인트에서 전체 표현식으로 매치작업은 진행되지 않는다. 마지막 단어가 ("f", "o", "o")가 이미 소모되어 버렸기 때문이다. 그렇기 때문에 매처는 처음부터 가장 오른쪽의 foo가 발생할때까지 매치 작업을 수행해 나가게 된다.
The first example uses the greedy quantifier .* to find "anything", zero or more times, followed by the letters "f" "o" "o". Because the quantifier is greedy, the .* portion of the expression first eats the entire input string. At this point, the overall expression cannot succeed, because the last three letters ("f" "o" "o") have already been consumed. So the matcher slowly backs off one letter at a time until the rightmost occurrence of "foo" has been regurgitated, at which point the match succeeds and the search ends.

두번째 예제는 reluctant의 예이다. 그러므로 이것은 처음 "nothing"을 소모하면서 시작이 된다. "foo"는 문장의 처음에 나타나지 ㅇ낳기 때문에 첫번째 단어 "x"를 가져와서 처리를 수행하게 되고, 0 에서 4까지 인덱스에 매치를 수행하게 된다. 우리의 test harness는 입력 스트링이 다할때까지 이러한 작업을 진행하게 된다. 마지막으로 다른 매칭을 4, 13번 인덱스에서 매치 작업을 수행하게 된다.

세번째 예제는 매치를 실패하게 된다. 왜냐하면 posessive이기 때문이다. 이 케이스에서는 전체 입력 스트링이 .*+에 의해서 모두 소모되어 버렸고, foo를 만족하는 문자열을 찾으려해도 대상 스트링이 없게 된다. posessive 수량 한정자를 이용하면 한자씩 검사하는 작업을 처리하지 않게 된다. 이것은 greedy와 비교해볼때 성능은 뛰어나지만 직접적으로 정확히 매칭되지 않을때는 찾을 수없는 단점도 있다.