2008.4.10 translation & editing by gilbird
원문: wiki-snapshot.pdf  pp. 263-266

검색결과 집합 (Capture Groups)

그루비에서 가장 유용한 특징 중의 하나로 정규식으로 데이터에서 정규식 검색 결과 집합을 구성하는 기능이 있다. 예를 들어 다음과 같이  England의 Liverpool의 위치 데이터를 추출한다고 해보자.

locationData = "Liverpool, England: 53° 25? 0? N 3° 0? 0?"

데이터 추출을 위하여 string의 split()함수를 써서 Liverpool과 England 사이의 쉼표, 기타 특수문자를  제거한다. 아니면 정규식 한 줄로 해결할 수도 있는데 식을 만들고 나면 다소 복잡해 보일 수도 있다.  우선 관심있는 것들을 모두 괄호로 묶어 놓는다.

myRegularExpression = /([a-zA-Z]+), ([a-zA-Z]+): ([0-9]+). ([0-9]+). ([0-9]+). ([A-Z])
([0-9]+). ([0-9]+). ([0-9]+)./
다음은 ~= 연산자로 "matcher"를 정의한다.
matcher = ( locationData =~ myRegularExpression )

matcher변수는 그루비에서 향상시킨 java.util.regex.Matcher를 내장하고 있다. 데이터는 Matcher 객체로부터 자바에 있는 것처럼 액세스할 수 있다. 그루비 개발자가 데이터를 가져오는 방법은 matcher를 써서 2차원 배열인 것처럼 사용하는 것이다. 2차원 배열은 배열의 배열이다. 위의 경우에 배열의 첫번째는 문자열이 정규식에 각각 일치한 값들이다. 이 예에서 정규식은 한번만 일치하며 2차원 배열의 첫번째 엘리먼트는 한개 뿐이다. 다음 코드를 보자.

matcher[0]

위 식의 결과는 아래와 같이 나와야 한다.

["Liverpool, England: 53° 25? 0? N 3° 0? 0?", "Liverpool", "England", "53", "25", "0", "N",
"3", "0", "0"]

그리고 배열의 두번째 차원을 액세스해서 찾아보고자하는 검색 결과 집합(capture group)을 액세스 할 수 있다.

if (matcher.matches()) {
    println(matcher.getCount()+ " occurrence of the regular expression was found in the string.");
    println(matcher[0][1] + " is in the " + matcher[0][6] + " hemisphere. (According to: " 
        + matcher[0][0] + ")")
}

정규식을 이용하여 얻을 수 있는 이점은 데이터가 규칙적(well-formed)인지 볼 수 있다는 것이다. 즉  locationData가 "Could not find location data for Lima, Peru"라면 if문 안을 실행하지 않을 것이다.

검색결과외 집합 (Non-matching Groups)

검색 결과 이외의 집합을 만드는 식을 사용하는 것이 좋을 수도 있는데 괄호안의 맨앞에 ?: 두 글자를 넣으면 된다. 예를 들어 몇몇 사람의 중간 이름이 무것이든지 없애고 맨앞과 맨뒤 글자를 뒤바꿔 출력 하고자 한다면 아래와 같이 한다.

names = [
    "Graham James Edward Miller",
    "Andrew Gregory Macintyre"
]
printClosure = {
    matcher = (it =~ /(.*?)(?: .+)+ (.*)/); // 중간에 검색결과외 집합이 있다.
    if (matcher.matches())
        println(matcher[0][2]+", "+matcher[0][1]);
}
names.each(printClosure);

위의 결과는 아래와 같다.

Miller, Graham
Macintyre, Andrew

위와 같이 하면 성이 두번째 일치 집합에 들어 있음을 알 수 있다.

치환 (Replacement)

정규식으로 할 수 있는 가장 간단하고 유용한 작업은 문자열의 일부를 치환하는 것인데 java.util.regex.Matcher (이 객체는 myMatcher = ("a" += /b/)와 같은 식을 사용하면 나온다)의 replaceFirst()와 replaceAll() 함수를 쓰면 된다.

Harry Potter 이름을 전부  Tanya Grotter로 치환해서 J.K Rewlings의 책을 재판매하고 싶다고 해보자 (누군가 이런 짓을 했다. 궁금하면 검색해보라).

excerpt = "At school, Harry had no one. Everybody knew that Dudley's gang hated that odd Harry Potter "+
    "in his baggy old clothes and broken glasses, and nobody liked to disagree with Dudley's gang.";
matcher = (excerpt =~ /Harry Potter/);
excerpt = matcher.replaceAll("Tanya Grotter");
matcher = (excerpt =~ /Harry/);
excerpt = matcher.replaceAll("Tanya");
println("Publish it! "+excerpt);

이 경우 Harry Potter의 이름, 성+이름 두 가지에 대해서 치환이 된다.

최소동작 연산자 (Reluctant Operators)

?, +, *는 게걸스러운 연산자라서 가능한 많은 입력을 일치 시키려고 한다. 때로는 이 방법이 원하지 않는 것일 수도 있다. 아래는 15세기 교황 리스트를 가지고 이야기 해보자.

popesArray = [
    "Pope Anastasius I 399-401",
    "Pope Innocent I 401-417",
    "Pope Zosimus 417-418",
    "Pope Boniface I 418-422",
    "Pope Celestine I 422-432",
    "Pope Sixtus III 432-440",
    "Pope Leo I the Great 440-461",
    "Pope Hilarius 461-468",
    "Pope Simplicius 468-483",
    "Pope Felix III 483-492",
    "Pope Gelasius I 492-496",
    "Pope Anastasius II 496-498",
    "Pope Symmachus 498-514"
]

위 예에서 (일련번호나 수식어구 없는) 이름만을 파싱해 내는 정규식을 만들어 보자.

/Pope (.*)(?: .*)? ([0-9]+)-([0-9]+)/

위 식은 아래와 같이 나누어 볼 수 있다.

/ Pope (.*) (?: .*)? ([0-9]+) - ([0-9]+) /
정규식 시작 Pope 0개 이상의 글자 검색결과외 집합: 공백문자와 0개 이상의 글자 숫자 - 숫자 정규식 끝

위의 모든 예에서 첫번째 일치 집합은 교황의 이름만이 나올 것으로 기대 했으나 실제로 나온 결과는 수식어 까지 인식이 되버린다. 예를 들어 첫번째 교황은 다음과 같이 쪼개진다.

/ Pope (.*) (?: .*)? ([0-9]+) - ([0-9]+) /
정규식 시작 Pope Anstasius I   399 - 401 정규식 끝

위 사실로 보아 첫번째 결과 집합에서 너무 많은 인식을 하고 있다. 첫번째 결과 집합에서는 Anatasius 만 가져오고 수식어구는 두번째 결과 집합에 들어가길 원했다. 방법을 바꾸어 첫번째 결과 집합에 최소한의 입력만 인식하도록 해보자. 이 경우는 공백 전까지의 모든 것이 해당된다. 자바 정규식에서는 *, +, ? 연산자의 최소 동작(reluctant) 버전을 지원한다. *+? 세가지 연산자 중 하나를 최소로 동작하게 하려면 간단하게 연산자 다음에 ?를 넣으면 된다(*?, +?, ??). 그래서 새로운 정규식은 아래와 같다.

/Pope (.*?)(?: .*)? ([0-9]+)-([0-9]+)/

이제 새로운 정규식으로 난이도 높은 입력의 하나로 Pope Hilarius를 처리해 보도록 하자. 결과는 아래와 같다.

/ Pope (.*) (?: .*)? ([0-9]+) - ([0-9]+) /
정규식 시작 Pope Leo I the Great 440 - 461 정규식 끝

이 결과가 바로 우리가 원하는 것이다.


테스트를 위하여 다음 코드를 실행해 보기 바란다.

popesArray = [
    "Pope Anastasius I 399-401",
    "Pope Innocent I 401-417",
    "Pope Zosimus 417-418",
    "Pope Boniface I 418-422",
    "Pope Celestine I 422-432",
    "Pope Sixtus III 432-440",
    "Pope Leo I the Great 440-461",
    "Pope Hilarius 461-468",
    "Pope Simplicius 468-483",
    "Pope Felix III 483-492",
    "Pope Gelasius I 492-496",
    "Pope Anastasius II 496-498",
    "Pope Symmachus 498-514"
]
myClosure = {
    myMatcher = (it =~ /Pope (.*?)(?: .*)? ([0-9]+)-([0-9]+)/);
    if (myMatcher.matches())
        println(myMatcher[0][1]+": "+myMatcher[0][2]+" to "+myMatcher[0][3]);
}
popesArray.each(myClosure);
 
위 코드의 결과 비교차원에서 처음 제시한 정규식도 넣어서 실행해 보기 바란다.