Regex : 괄호 뒤에 오는 물음표가 의미하는 바는? (abcd)? 에 숨겨진 뜻.

이렇게 알게된 지식을 오래 오래 간직하면 좋으련만..
그나마 여기에 이렇게 적어놓는 게 큰 도움이 되리라고 굳게 믿으며 글을 시작한다.

며칠 째 Regex 에서 헤어나지 못하고 있다. 나이가 들어서인지 집중을 오래 하면 여기 저기 삭신이 쑤신다.
이건 나이가 들어서가 아니라 운동을 안해서일테지만..

자!

이런 Regex 를 보자.
Python 으로 실험해봤다.

test1 = 'aabbcc'
mat1 = '(aa)(bb)(cc)'
re.search(mat1, test1)
<_sre.SRE_Match object; span=(0, 6), match='aabbcc'>
re.search(mat1, test1).group(1)
'aa'

어려운 건 하나도 없다.

다음 문제.

mat2 = '(aa)(bb)?(cc)'
test2 = 'aacc'
re.search(mat2,test2)
<_sre.SRE_Match object; span=(0, 4), match='aacc'>
re.search(mat2,test2).group(1)
'aa'
re.search(mat2,test2).group(2)
re.search(mat2,test2).group(3)
'cc'

여기도 어려울 게 없다.
물음표를 사용해서, (bb) 그룹이 있거나, 없어도 매칭이 되게끔 했다.
aacc 에는 (aa)는 있고, (bb) 는 없으나, (cc) 가 있으므로, 최종 aacc 가 선택됐다.

아래처럼 해도 마찬가지다. (aa) 그룹 뒤에 물음표를 붙였다.

mat3 = '(aa)?(bb)?(cc)'
test2 = 'aacc'
re.search(mat3,test2)
<_sre.SRE_Match object; span=(0, 4), match='aacc'>
re.search(mat3,test2).group(1)
'aa'
re.search(mat3,test2).group(2)
re.search(mat3,test2).group(3)
'cc'

그런데.. 여기서부터 헷갈린다.

mat4 = '(aa)?(bb)?(cc)'
test4 = 'aaxxcc'

이것의 결과는??
group(1) 은 aa 가 되고, group(2)는 None 이고, group(3) 은 cc 가 되리라고 믿고 있던 내 지식에, 얄미운 비웃음만 날아왔다.
Regex 는, aabbcc, bbcc, cc, aacc 등만을 매칭시킨다. (또 있나?) 다시 말해서, aa□□cc 는 되지 않는다.

그렇게 하려면 다음과 같아야 한다.

mat3 = '(aa)?(\w?\w?)?(cc)'
test4 = 'aaxxcc'
re.search(mat3, test4)
<_sre.SRE_Match object; span=(0, 6), match='aaxxcc'>
re.search(mat3, test4).group(1)
'aa'
re.search(mat3, test4).group(2)
'xx'
re.search(mat3, test4).group(3)
'cc'

만약, 앞 그룹 존재 여부에 따라 다음 그룹도 나올지 여부는 Regex 조건을 사용해야 하는데..
간단한 구문이라면 괜찮겠지만, 복잡한 구문이 되면, 사용하기가 꽤나 까다로와진다.
구문도 난잡해질 뿐만 아니라 가독력이 확 떨어지므로, 그다지 권장할만한 방법은 못된다.

모든 것을 Regex 로 해결할 필요는 없는 법!


그런데, 위에 있는 것들 보다, 아래가 더 문제다.

mat5 = '(aoeu)?'
test4 = 'aaxxcc'
re.search(mat5, test4)
 <_sre.SRE_Match object; span=(0, 0), match=''>

어라??
매치가 됐다고?

aaxxcc 에는 (aoeu)? 는 없다.
만약 이렇게 바꿔써보면 매칭이 되지않았음을 볼 수 있다.

mat5 = 'aoeu?'
test4 = 'aaxxcc'
re.search(mat5, test4)
 (결과없음)

(aoeu)? 는 매칭이 됐다고 나오고, 그 결과는 ” 이다. 아무것도 없는 Null 문자가 매칭이 됐다.
이것이 무슨 x소리인가..???

바꾸기를 실행해보면 좀 더 명확하게(?) 뭘 찾았는지를 알 수 있다.

mat5 = '(aoeu)?'
test4 = 'abcdefg'
re.search(mat5, test4)
<_sre.SRE_Match object; span=(0, 0), match=''>
re.sub(mat5, 'z', test4)
'zazbzczdzezfzgz'

abcdefg 각 글자 앞에 z 가 추가된 형상이 돼 버렸다.
내 추리(????????????????????)는 이렇다.

이걸 이해하려면 Regex 엔진이 움직이는 원리를 알아야 하는데, 원리를 정확히는 모르니 그저 짐작만 또 해본다.
Regex 는 Matching 이 되거나 되지 않았을 때, 한글자씩 오른쪽으로 움직인다.
예전 글에서도 언급한 적이 있는데, Regex 엔진은, 글자 왼쪽에 커서(I Beam 이라 생각하는게 편하다.)를 두고 매칭을 시도한다.

여기서 보자면, 첫 글자 a 바로 앞에서 Regex 가 (aoeu)를 찾기 시작한다.
a는 맞았지만, o 이하로 틀리기 때문에 그 다음 글자인 b 바로 옆으로 넘어가서 다시 (aoeu) 를 찾아야겠지만??
그냥 aoeu 가 아니고, 그룹으로 묶인 aoeu 이고, 그 뒤에 물음표가 있다는게 큰 걸림돌이 된다.
Regex 는 생각한다. (aoeu) 자체가 없어도 된다? 그렇다면 결국, “아무 것도 없어도” 매칭이 될 수 있단 말이 된다.
(aoeu) 는 결국 Null 문자가 된다. (이게 혹시 Zero Assertion 인가?)

따라서, Regex 는 a 왼쪽에서 (aoeu)를 찾는데 실패했어도 오른쪽 문자로 넘어가질 않는다. 아직 완전히 실패한 게 아니기 때문이다. 마지막 남은 ? 와 매칭될 게 있는지 확인한다. ? 는 그 앞에 있는 Null 문자를 뜻하게 되고, 결국 원치 않는 결과가 나타나게 된다.

쳐다볼 엄두가 잘 나지 않는 Mastering Regular Expressions 나, Regular Expressions Cookbook 에 이런 내용이 틀림없이 있을 듯도 한데..

아무튼 결론은!
그룹을 사용할 때, 그 뒤에 ? 를 넣을 때는, 그 그룹이 존재하지 않을 가능성을 반드시 염두에 둬야 한다!!
그렇지 않으면 매 글자마다 매칭이 되는 짜증나는 결과를 얻게된다!

Tags:

안녕하세요. 글 남겨주셔서 고맙습니다.