정규 표현식의 Greedy & Lazy

장동혁·2021년 11월 16일
1

개요

대부분의 엑셀 관련 프로그램에서 여러 셀을 복사하면 클립보드에는 부가적인 정보(셀 스타일, 셀 병합(rowspan, colspan) 등)를 포함해야 되기 때문에 HTML소스가 저장된다. 데이터 시트(엑셀) 관련 라이브러리를 커스터마이징 하는 도중 다른 엑셀 프로그램에서 데이터를 복사한 뒤 클립보드에서 가져와야 했고 td태그의 rowspan값을 알아내야 했기 때문에 HMLT 문자열을 파싱해야 했고 이 과정에서 발견한 Greedy & Lazy 개념에 대해서 정리하고자 한다.

이슈

정규식을 사용하여 여러 tr 태그들의 컨텐츠만 긁어 모아야 했는데 이 때 tr태그 안의 모든 컨텐츠를 선택해야 했기 때문에 다음과 같은 정규식을 사용해야 했다.

const trs = pastedHTML.match(/(?<=<tr.*>).*(?=<\/tr>)/g); 
/*
--- pastedHTML ----
'...<tr style="..."><td>...</td><td><span ...>...</span></tr><tr>...</tr><tr>..</tr>...'

--- trs ---
['<td>...</tr><tr>...</td>']
*/

결과를 보면 tr 태그가 3개임에도 하나의 문자열만 매칭되었고 전방 & 후방 탐색을 하였는데도 불구하고 안에 tr 태그도 포함되어 있다. (정규식에서 탐색을 위한 키워드는 매칭되지 않는다)
이렇게 된 이유는 정규식에 사용된 .*때문이다. 정규식에서는 Quantifier, 수량자 : *, +, ?, {}라는 개념이 존재하여서 반복하거나 선택적으로 문자열을 매칭시킬 수 있다. 이러한 수량자들은 Greedy 하게 동작하기 때문에 후방탐색에 사용된</tr>조차 .*에 의해 매칭되어 버렸기 때문이다.

해결

const trs = pastedHTML.match(/(?<=<tr.*>).*?(?=<\/tr>)/g); 
/*
--- pastedHTML ----
'...<tr style="..."><td>...</td><td><span ...>...</span></tr><tr>...</tr><tr>..</tr>...'

--- trs ---
['<td>...<td/><td><span...>...</span></td>', '<tr>', '<tr><td>...</td>']
*/

.* 뒤에 ?를 붙여 조건에 맞을 경우 .* 최대한 적게 탐색하게 만들었다. 그렇게 되니 tr태그의 갯수 만큼 매칭이 되었다.

Greedy & Lazy

위에서 언급한 Greedy하게 동작한다는 의미는 무엇일까? 한 마디로 표현하자면 "최대한 많이" 라고 할 수 있을 것 같다. 그렇다면 반대인 Lazy는 "최대한 적게" 라고 할 수 있을 것이다. Quantifier, 수량자 : *, +, ?, {}는 Greedy로 동작하고 뒤에 ?를 추가하면 (*?, +?, ??, {}?) Lazy로 동작한다.

// Greedy
'010-1234-5678'.match(/-\d+/g); // ['-1234', '-5678']

// Lazy
'010-1234-5678'.match(/-\d+?/g); // ['-1','-5']

위 예제를 보면 Greedy Quantifire인 +와 Lazy Quantifire인 +?차이를 알 수 있다.
정규표현식에서 +{1,}과 같다. 즉 최소 한 개 이상 최대 무한 개 까지 매칭한다. Greedy이기 때문에 "최대한 많이" 매칭하기 때문에 ['-1234', '-5678']가 매칭되었다. 하지만 Lazy하게 동작하도록 뒤에 ?를 추가하면 (+?, {1,}?) 한 개만 매칭되어도 정규표현식을 만족하기 때문에 ['-1','-5']가 매칭된다.

이렇게 보면 \d+?가 뜻하는게 \d{1,1} => \d과 같다고 착각할 수 있는데 어면히 다르다.

// 사용
'010-1234-5678'.match(/-\d+?-/g); // ['-1234-']

//미사용
'010-1234-5678'.match(/-\d-/g); // null

-\d+?-는 앞 뒤에 -이 오며 가운데 최소 한 개 최대 무한 개의 숫자를 매칭하겠다 이므로 최소한으로 매칭되는 쪽으로 노력하지만 없다면 최대한으로도 매칭시키긴 한다는 뜻이다. 즉, Lazy Quantifire는 "최소한으로만 매칭하겠다." 가 아닌 "최대한 적게" 매칭하겠다이기 때문에 노력은 하되 게으르게 (Lazy) 노력하는 Quantifire인 셈이다.

??

정규표현식에서 ?키워드의 의미는 사용되는 곳에 따라 달라진다. (?=...), (?<=...) 처럼 탐색을 뜻하기도하고 \d?처럼 0개 또는 1개 ({0,1})를 뜻하기도 한다. 그리고 Quantifire뒤에 와서 (*?, +?, ??, {}?) Lazy하게 만들기도 한다. ??는 Lazy한 ? Quantifire를 뜻하는데 어떤경우에 사용될까?

/(-)?(.*)/g.exce('-foo') 
// ['-apple', '-', 'apple', index: 0, input: '-apple', groups: undefined];
/(-)??(.*)/g.exce('-foo') 
// ['-apple', undefined, '-apple', index: 0, input: '-apple', groups: undefined];

?를 사용한 경우에는 - 기호가 greedy하게 매칭되었기 때문에 첫 번째 그룹으로 배정되었지만 ??를 사용한 경우에는 - 기호가 lazy하게 매칭되어서 두 번째 그룹으로 배정되었다. 다양한 케이스를 생각해보고 찾아 봤지만 이렇게 그룹핑 관련된 상황이 아니면 특별히 사용되는 경우는 없을 것 같다.

profile
기록하는 습관

0개의 댓글