
2008.4.10 translation & editing by gilbird
원문: wiki-snapshot.pdf pp. 263-266
그루비에서 가장 유용한 특징 중의 하나로 정규식으로 데이터에서 정규식 검색 결과 집합을 구성하는 기능이 있다. 예를 들어 다음과 같이 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문 안을 실행하지 않을 것이다.
검색 결과 이외의 집합을 만드는 식을 사용하는 것이 좋을 수도 있는데 괄호안의 맨앞에 ?: 두 글자를 넣으면 된다. 예를 들어 몇몇 사람의 중간 이름이 무것이든지 없애고 맨앞과 맨뒤 글자를 뒤바꿔 출력 하고자 한다면 아래와 같이 한다.
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
위와 같이 하면 성이 두번째 일치 집합에 들어 있음을 알 수 있다.
정규식으로 할 수 있는 가장 간단하고 유용한 작업은 문자열의 일부를 치환하는 것인데 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의 이름, 성+이름 두 가지에 대해서 치환이 된다.
?, +, *는 게걸스러운 연산자라서 가능한 많은 입력을 일치 시키려고 한다. 때로는 이 방법이 원하지 않는 것일 수도 있다. 아래는 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);