이번 주말에는 가벼운 마음으로 정규 표현식에 대한 책을 읽었다.
늘 사용하는 것이긴 하지만 한번쯤 정리해 둘 필요도 있을 것 같기도 하고
oro(apache project) 를 사용하지 않고, java.util.regex 패키지 내에 포함되어 있는
Matcher, Pattern 만을 사용해서 정규표현식을 어느 정도까지 처리할 수 있는 지도 알아 두고 싶었다.
참고 서적 정보는 아래와 같다.
도서명: 손에 잡히는 정규 표현식
출판사: 인사이트
벤 포터 지음, 김경수 옮김.
개행문자 처리
(.*) 표현식을 사용할 경우 개행문자가 포함되어 있으면 매칭되지 않는다.
그런데 개행문자를 포함하여 매칭하고자 하면 Pattern 옵션에 Pattern.DOTALL 플래그를 지정하면 개행문자를 포함하여 매칭한다.
표현식 앞 부분에 (?s)를 포함시키면 DOTALL 플래그를 설정하는 것과 동일하다.
[javadoc 원문]
In dotall mode, the expression
. matches any character,
including a line terminator. By default this expression does not match
line terminators.
Dotall mode can also be enabled via the embedded flag
expression
(?s). (The
s is a mnemonic for
"single-line" mode, which is what this is called in Perl.)
String ln = System.getProperty("line.separator", "\r\n");
@Test
public void testCdataSectionRemover() {
String query = "<![CDATA[ select ACCOUNT_ID, NAME, ALIAS, EMAIL " + ln "from U_SAMPLE_ACCOUNT ]]>";
System.out.println("query: " + query);
// Pattern p = Pattern.compile("<!\\[CDATA\\[(.*)\\]\\]>", Pattern.DOTALL);
Pattern p = Pattern.compile("(?s)<!\\[CDATA\\[(.*)\\]\\]>");
Matcher matcher = p.matcher(query);
if (matcher.find()) {
System.out.println("match");
System.out.println(matcher.group(1));
} else {
System.out.println("not match");
}
}
수량자 (Quantifiers)
수량자는 패턴이 입력 텍스트를 병합하는 방법을 나타내는 것으로 '?', '*', '+', {min,max}를 표준 수량자라고 한다. 이러한 수량자를 정규표현식에 넣어서 사용함으로써 더 복잡하고 다양한 match가 가능하다.
Greed: 수량자는 greedy하다. 우리말로는 탐욕스럽다는 의미로서, 최대한 많은 수의 문자를 match시키기 때문이다. 표현식에 '?', '*', '+', {min,max}가 포함되어 있을 경우이다.
Reluctant: 표현식에 물음표(?)를 추가하면 그 패턴을 만족시키는 데 필요한 최소한의 문자 수가 match된다. lazy, 최소매칭, non-greedy, ungreedy라고도 한다.
Possesive: 현재 자바에서만 가능한 것으로 더 진보된 것이다. 정규표현식이 문자열에 적용될 때는 match가 실패할 경우 되돌아 갈 수 있도록 (backtrack) 많은 상태(state)를 생성한다.
package test.regexp;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
public class LazyTest {
@Test
public void testGreedy() throws Exception {
System.out.println();
System.out.println("testGreedy");
InputStream in =
this.getClass().getResourceAsStream("lazy-input1.txt");
String input =
InputLoader.load(in);
System.out.println("input: ");
System.out.println(input);
String regex = "<[Bb]>.*</[Bb]>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
while(matcher.find()) {
System.out.println(matcher.group());
}
}
@Test
public void testLazy() throws Exception {
System.out.println();
System.out.println("testLazy");
InputStream in =
this.getClass().getResourceAsStream("lazy-input1.txt");
String input =
InputLoader.load(in);
System.out.println("input: ");
System.out.println(input);
String regex = "<[Bb]>.*?</[Bb]>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
while(matcher.find()) {
System.out.println(matcher.group());
}
}
}
아래는 greedy수량자와 lazy수량자를 사용한 결과를 비교한 것이다.
testGreedy
input:
This offer is not avaliable to customers
living in <B>AK</B> and <B>HI</B>.
<B>AK</B> and <B>HI</B>
testLazy
input:
This offer is not avaliable to customers
living in <B>AK</B> and <B>HI</B>.
<B>AK</B>
<B>HI</B>
이와 같은 결과는 별표(*)와 더하기(+) 같은 메타 문자가 탐욕(greedy)스럽기 때문인데, 이는 가능한 한 가장 큰 덩어리를 찾으려 한다는 뜻이다. 이런 메타 문자는 찾으려는 텍스트를 앞에서부터 찾는 게 아니라, 텍스트 마지막에서 시작해 거꾸로 찾는다. 이는 의도적으로 수량자(quantifier)를 탐욕적으로 설계했기 때문이다.
경계지정하기
단어 경계 지정하기
단어 경계를 지정하고자 하는 경우에는 메타문자 \b를 사용한다. \b는 패턴으로 검색된 단어 주변에 공백으로 되어 있는 경우에 검색된다.
아래의 예제는 'cat'이 포함되어 있는 패턴을 검색할 때 단어 경계를 지정하는 예제다.
@Test
public void testBoundary() throws Exception {
String input = "The cat scattered his food all over the room.";
System.out.println("input: " + input);
System.out.println();
String regex_1 = "cat";
System.out.println("regex: " + regex_1);
Pattern pattern1 = Pattern.compile(regex_1);
java.util.regex.Matcher matcher1 = pattern1.matcher(input);
while(matcher1.find()) {
String group = matcher1.group();
System.out.println("group: " + group + ", start index: " + matcher1.start());
}
System.out.println();
String regex_2 = "\\bcat\\b";
System.out.println("exgex: " + regex_2);
Pattern pattern2 = Pattern.compile(regex_2);
Matcher matcher2 = pattern2.matcher(input);
while(matcher2.find()) {
String group = matcher2.group();
System.out.println("group: " + group + ", start index: " + matcher2.start());
}
System.out.println();
String regex_3 = "\\Bcat\\B";
System.out.println("exgex: " + regex_3);
Pattern pattern3 = Pattern.compile(regex_3);
Matcher matcher3 = pattern3.matcher(input);
while(matcher3.find()) {
String group = matcher3.group();
System.out.println("group: " + group + ", start index: " + matcher3.start());
}
}
결과는 아래와 같다.
단순희 'cat'으로 패턴을 지정하는 경우는
index가 4인 cat과 index가 9인 scattered의 cat이 모두 검색된다.
그러나 \bcat\b으로 패턴을 지정하는 경우는 단어로 경계를 지정하였기 때문에
index가 4인 cat만 검색이 된다.
\B는 \b와 반대되는 개념으로 \Bcat\B로 지정하였다면 cat 앞뒤에 단어의 경계가 없는 경우다.
따라서 scattered 안에 포함되어 있는 cat만 검색하게 된다.
아래의 결과에 start index를 포함하여 로그를 출력하였으므로 함께 살펴보기 바란다.
input: The cat scattered his food all over the room.
regex: cat
group: cat, start index: 4
group: cat, start index: 9
exgex: \bcat\b
group: cat, start index: 4
exgex: \Bcat\B
group: cat, start index: 9
문자열 경계 정의하기
단어 경계는 단어의 위치(단어의 시작, 단어의 마지막, 단어 전체 등)를 기반으로 위치를 찾는다. 문자열 경계는 단어 경계와 기능은 비슷하지만, 전체 문자열의 시작이나 마지막 부분과 패턴을 일치시키고자 할 때 사용한다. 문자열 경계는 메타 문자 가운데 캐럿(^)으로 문자열의 시작을, 달러 기호($)로 문자열의 마지막을 나타낸다.
아래의 예제 소스는 XML의 시작부에 선언되는 XML 선언자를 검색하는 패턴이다. 두 번째 예제에서는 XML 선언자 앞에 유효하지 않은 문자열이 포함되어 있어 XML이 잘못된 경우이지만 정상적으로 검색된 것처럼 작동한다. 따라서 캐럿을 포함시켜 검색을 해서 XML이 유효하지 않음을 찾을 수 있도록 해야 한다.
@Test
public void testStringBoundary() throws Exception {
String ln = System.getProperty("line.separator");
String input = "<?xml version='1.0' encoding='utf-8'?>" + ln + "<message>" + ln + "</message>";
System.out.println("INPUT");
System.out.println(input);
System.out.println();
String regex_1 = "<\\?xml.*\\?>";
System.out.println("regex: " + regex_1);
Pattern pattern1 = Pattern.compile(regex_1);
Matcher matcher1 = pattern1.matcher(input);
if(matcher1.find()) {
System.out.println("group: " + matcher1.group());
}
System.out.println();
input = "This is bad, real bad!" + ln + input;
System.out.println("INPUT");
System.out.println(input);
System.out.println();
System.out.println("regex: " + regex_1);
Matcher matcher2 = pattern1.matcher(input);
if(matcher2.find()) {
System.out.println("group: " + matcher2.group());
}
System.out.println();
System.out.println();
input = "This is bad, real bad!" + ln + input;
System.out.println("INPUT");
System.out.println(input);
System.out.println();
String regex_3 = "^\\s*<\\?xml.*\\?>";
System.out.println("regex: " + regex_3);
Matcher matcher3 = Pattern.compile(regex_3).matcher(input);
System.out.println("FIND: " + matcher3.find());
}
INPUT
<?xml version='1.0' encoding='utf-8'?>
<message>
</message>
regex: <\?xml.*\?>
group: <?xml version='1.0' encoding='utf-8'?>
INPUT
This is bad, real bad!
<?xml version='1.0' encoding='utf-8'?>
<message>
</message>
regex: <\?xml.*\?>
group: <?xml version='1.0' encoding='utf-8'?>
INPUT
This is bad, real bad!
This is bad, real bad!
<?xml version='1.0' encoding='utf-8'?>
<message>
</message>
regex: ^\s*<\?xml.*\?>
FIND: false
다중행 모드 사용하기
대개 캐럿(^)은 문자열의 시작과 일치하고, 달러 기호($)는 문자열의 마지막과 일치한다. 예외적으로 두 메타 문자의 동작을 바꾸는 방법이 있다. 많은 정규표현식 구현은 다른 메타 문자의 동작을 변경하는 특수한 메타 문자를 지원하는데, 그 중 하나가 (?m)으로 다중행(multiline)을 지원한다. 다중행 모드로 변경하면 강제로 정규 표현식 엔진이 줄바꿈 문자를 문자열 구분자로 인식한다. 캐럿(^)은 문자열 시작이나 줄바꿈 다음(새로운 행)에 나오는 문자열의 시작과 일치하고, 달러 기호($)는 문자열의 마지막이나 줄바꿈 다음에 나오는 문자열의 마지막과 일치한다. (?m)은 항상 패턴 제일 앞에 두어야 한다.
java.util.regex.Pattern에서 다중행 모드를 사용하는 방법은 두 가지가 있다. 첫 번째 방법은 Pattern.compile(regex, flags)의 두번재 인자인 flags에 Pattern.MULTILINE을 지정하는 방식이고, 두번 째 방법은 패턴에 (?m)을 사용하는 것이다.
방식1.
String regex1 = "^\\s*//.*$";
Pattern pattern1 = Pattern.compile(regex1, Pattern.MULTILINE);
방식2.
String regex2 = "(?m)^\\s*//.*$";
아래의 예제 소스는 자바스크립트 소스에서 인라인 주석(//)을 검색하는 패턴이다.
위의 두가지 방식으로 검색하는 예제가 모두 포함되어 있다.
@Test
public void testMultiLine() throws Exception {
String input = "function doSpellCheck(form, field) {" + ln +
" // Make sure not empty" + ln +
" if(field.value == '') {" + ln +
" return false;" + ln +
" }" + ln +
" // Init" + ln +
"}" + ln;
System.out.println("INPUT");
System.out.println(input);
String regex1 = "^\\s*//.*$";
Pattern pattern1 = Pattern.compile(regex1, Pattern.MULTILINE);
Matcher matcher1 = pattern1.matcher(input);
System.out.println("regex: " + regex1);
System.out.println("== RESULT ==");
while(matcher1.find()) {
System.out.println("find: '" + matcher1.group() + "'");
}
System.out.println();
System.out.println();
String regex2 = "(?m)^\\s*//.*$";
System.out.println("regex: " + regex2);
Pattern pattern2 = Pattern.compile(regex2);
Matcher matcher2 = pattern2.matcher(input);
while(matcher2.find()) {
System.out.println("find: '" + matcher2.group() + "'");
}
}
두 가지 방식으로 검색을 하더라도 결과가 동일함을 확인할 수 있다.
INPUT
function doSpellCheck(form, field) {
// Make sure not empty
if(field.value == '') {
return false;
}
// Init
}
regex: ^\s*//.*$
== RESULT ==
find: ' // Make sure not empty'
find: ' // Init'
regex: (?m)^\s*//.*$
find: ' // Make sure not empty'
find: ' // Init'
하위 표현식 Sub expression
하위 표현식은 ()를 사용하여 표현한다.
각각의 하위 표현식은 group으로 표현된다. group(0)은 검색된 전체 문자열이 리턴되고, 1부터 groupCount까지는 각각 하위표현식에 의해서 검색된 문자열이 리턴된다.
아래의 예제는 아이피의 각 바이트 값을 읽어 내는 예제 소스다. 정규표현식이 그다지 정교하다고 할 수는 없지만, 하위표현식으로 group의 값을 읽는 예제로 작성되었음을 이해하고 소스코드를 살펴보기 바란다.
하위 표현식으로 검색할 때 group()과 group(0)이 동일한 값을 리턴한다.
@Test
public void testSubExpression() throws Exception {
String input = "Pinging hog.forta.com [12.159.46.200]" + ln + "with 32 bytes of data:";
String regex = "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})";
System.out.println("INPUT");
System.out.println(input);
System.out.println();
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
if(matcher.find()) {
int groupCount = matcher.groupCount();
System.out.println("group count: " + groupCount);
System.out.println("group: " + matcher.group());
System.out.println("group[0]: " + matcher.group(0));
for(int i = 1; i <= groupCount; i++) {
System.out.println(String.format("group[%1$d]: %2$s", i, matcher.group(i)));
}
}
}
출력 결과
INPUT
Pinging hog.forta.com [12.159.46.200]
with 32 bytes of data:
group count: 4
group: 12.159.46.200
group[0]: 12.159.46.200
group[1]: 12
group[2]: 159
group[3]: 46
group[4]: 200