본문 바로가기
프로그래밍/정규표현식

[REGEX] 조건 달기

by 별준 2022. 4. 10.

References

  • Learning Regular Expressions

Contents

  • 조건 (conditions)
  • 역참조를 사용하는 조건 처리
  • 전후방탐색을 사용하는 조건 처리

드물게 사용하는 기능이긴 하지만, 정규표현식은 표현식 내에 조건 처리를 포함시킬 수 있습니다. 이번 포스팅에서는 이와 관련된 내용을 살펴보겠습니다.

 

 


왜 조건을 사용하는가?

(123)456-7890과 123-456-7890은 모두 올바른 전화번호 형식입니다. 1234567890, (123)-456-7890, (123-456-7890은 숫자의 수는 맞지만, 형식이 바르지 않습니다.

정규표현식을 어떻게 작성하면 아래의 예문에서 올바른 형식의 전화번호만을 찾을 수 있을까요 ?

123-456-7890
(123)456-7890
(123)-456-7890
(123-456-7890
1234567890
123 456 7890

간단한 문제는 아닌데, 우선 다음의 패턴을 사용해보겠습니다.

'\(?\d{3}\)?-?\d{3}-\d{4}'

사용한 패턴에서 '\(?'는 여는 괄호가 없거나 하나인 경우에 일치합니다. 이때 여는 괄호를 이스케이프해야 한다는 것에 주의합니다. '\d{3}'은 처음에 나오는 세 자리 숫자와 일치하며, '\)?'는 닫는 괄호가 없거나 하나인 경우에 일치합니다. 그리고 '-?'는 하이픈이 없거나 하나인 경우에 일치하며, '\d{3}-\d{4}'는 하이픈으로 구분된 나머지 일곱 숫자와 일치합니다. 이 패턴은 확실히 예문의 마지막 두 행과는 일치하지 않습니다. 하지만 세 번째와 네 번째 행과는 일치하는데, 두 전화번호 모두 올바른 형식이 아닙니다. 세 번째 전화번호에는 닫는 괄호와 하이픈이 둘 다 있고, 네 번째는 괄호의 짝이 맞지 않습니다.

 

'\)?-?'를 '[\)-]?'로 바꾸면, 오직 닫는 괄호나 하이픈 가운데 하나만 일치하여 세 번째 줄을 제거할 수는 있지만, 네 번째 줄은 제거할 수 없습니다. 이 패턴은 여는 괄호가 있을 때만 닫는 괄호와 일치해야 합니다. 괄호 한 쌍이 없다면, 하이픈(-)을 찾아야 하는데, 이런 패턴은 조건 처리를 사용하지 않고는 구현할 수 없습니다.

 


조건 사용하기

정규표현식 조건은 물음표(?)를 사용하여 정의합니다. 사실 이미 몇 가지 특정 조건들을 알고 계시리라 생각됩니다.

  • 물음표(?)는 바로 앞에 문자나 표현식이 존재한다면, 그 문자 또는 표현식과 일치한다
  • '?='와 '?<='는 만약 존재한다면 앞(전방탐색)이나 뒤(후방탐색)의 텍스트와 일치한다

조건을 다는 구문 또한 '?'를 사용하는데, 넣을 수 있는 조건은 아래의 나열된 것과 같습니다.

  • 역참조를 사용하는 조건 처리
  • 전후방탐색을 사용하는 조건 처리

 

역참조 조건

역참조 조건(backreference conditions)는 이전 하위표현식이 검색에 성공했을 경우에만 다시 그 표현식을 검사합니다. 이해를 위해서 예제를 통해 살펴보겠습니다. 아래 예문은 HTML 코드이며, 본문 안에 있는 <img> 태그를 모두 찾아야 하고, 링크 태그 <a>와 </a>로 감싸져 있을 때는 이 링크 태그까지 일치시켜야 한다고 가정해보겠습니다.

<!-- Nav bar -->
<div>
<a href="/home"><img src="/images/home.gif"></a>
<img src="/images/spacer.gif">
<a href="/search"><img src="/images/search.gif"></a>
<img src="/images/spacer.gif">
<a href="/help"><img src="/images/help.gif"></a>
</div>

정규표현식에서 조건을 표현하는 구문은 다음과 같습니다.

'(?(backreference)true)'

'?'로 조건을 시작하고 괄호 안에 역참조를 지정한 다음, 역참조가 존재하는 경우에만 평가될 표현식이 바로 뒤에 나옵니다.

이러한 구문을 적용하여 다음의 패턴을 사용해보겠습니다.

'(<[Aa]\s+[^>]+>\s*)?<[Ii][Mm][Gg]\s+[^>]+>(?(1)\s*<\/[Aa]>)'

(javascipt에서는 해당 패턴이 먹히지 않는 것으로 보이며, C++에서도 해당 기능을 지원하지 않는지는 확인하지 못했으나, 위의 패턴을 사용하면 syntax error 예외가 발생합니다. 파이썬에서는 위 패턴이 정상적으로 동작하는 것을 확인하였습니다.)

Python 결과

사용한 패턴에서 '(<[Aa]\s+[^>]+>\s*)?'은 <A>나 <a> 시작 태그와 일치합니다. 이때 속성이 있다면 속성도 함께 일치하며, 여기서 마지막에 있는 물음표는 이 패턴이 없어도 되고, 있다면 일치한다는 것을 의미합니다. 그리고 나서 '<[Ii][Mm][Gg]\s+[^>]+>'는 <img> 태그를 대소문자 구분없이, 속성도 모두 포함해 일치시킵니다.

그리고 나머지 '(?(1)\s*<\/[Aa]>)'는 조건으로 시작하는데, '?(1)'은 역참조 1(<A> 시작 태그)이 있을 때만 수행하라는 의미입니다. 만약 (1)이 있다면, '\s*<\/[Aa]>'는 </A>가 나오기 전까지 모든 문자를 일치 영역에 넣어 줍니다.

'?(1)'은 역참조 1이 있는지 없는지 검사합니다. 여기서 역참조 번호(예제에서는 1)를 조건에서 이스케이프할 필요는 없습니다. 즉, '?(1)'이 올바른 표현이며, '?(\1)'은 잘못된 표현입니다. 하지만 후자도 동작은 합니다.

 

앞서 사용한 '(?(1)\s*<\/[Aa]>)' 패턴은 조건이 충족되었을 때만 수행됩니다. 조건은 else 표현식을 써서 나타낼 수도 있는데, else 표현식은 역참조가 존재하지 않을 경우에만 수행되는 표현식을 의미하며, 이 조건을 나타내는 문법은 다음과 같습니다.

'(?(backreference)true|false)'

 

 

이 문법을 사용하여 포스팅 초반에 나온 전화번호와 관련된 문제를 다음의 패턴을 사용하여 해결할 수 있습니다.

'(\()?\d{3}(?(1)\)|-)\d{3}-\d{4}'

이 패턴은 정상적으로 동작하는 것 같습니다. 이전에 사용한 패턴과 마찬가지로 '(\()?' 패턴으로 처음에 여는 괄호가 있는지 검사하지만, 이번에는 괄호로 감싸 그 결과를 하위표현식으로 만들었습니다. '\d{3}'은 숫자 세 개로 이루어진 지역번호와 일치합니다. '(?(1)\|-)'는 조건을 만족하는지에 따라 닫는 괄호(')') 혹은 하이픈('-')과 일치합니다. 만약 (1)이 있다면(즉, 여는 괄호가 있다면), '\)'와 일치하고, 없다면 '-'와 일치합니다. 이런 식으로 괄호는 항상 짝을 이루어야 하고, 지역번호와 숫자를 구분하는 하이픈은 괄호가 없을 때만 일치합니다.

네 번째 줄의 결과에서는 여는 괄호와 일치하는 쌍이 없으므로, 여는 괄호가 관련없는 텍스트로 간주되어 무시되므로 일치하였습니다.

 

 

전후방탐색 조건

전후방탐색 조건은 전방탐색과 후방탐색 명령이 성공했는지에 따라 표현식을 수행할지 결정합니다. 전후방탐색 조건 문법은 역참조(괄호 안에 넣는 숫자)가 전후방탐색 표현식으로 대체되는 것만 빼고는 역참조 조건과 동일합니다.

 

간단하게 미국 우편번호를 예로 살펴보겠습니다. 12345처럼 표현된 다섯 자리 ZIP이거나 12345-6789처럼 표현된 ZIP+4 코드입니다. 하이픈은 오직 뒤에 숫자 4개가 더 있을 때만 사용합니다.

11111
22222
33333-
44444-4444

'\d{5}(-\d{4})?' 패턴을 사용하면 다음의 결과를 확인할 수 있습니다.

'\d{5}'는 첫 다섯 자리 숫자와 일치하고, '(-\d{4})?'는 -와 네 자리 숫자로 이루어진 문자열이 있다면 일치합니다.

 

하지만, 형식이 잘못된 우편번호는 제외하고 찾으려고 한다면 어떻게 해야 할까요?

결과에서 세 번째 줄을 보면 없어야 할 '-'이 붙어 있습니다. 앞서 사용한 패턴은 하이픈을 제외하고 숫자와 일치하였지만, 잘못된 형식이라서 전체 ZIP 코드와 일치하지 않도록 해주어야 합니다. 조금 억지스러울 수는 있지만 전후방탐색 조건을 설명하기에는 좋습니다. 다음 패턴을 사용해보도록 하겠습니다.

'\d{5}(?(?=-)-\d{4})'

'\d{5}'는 앞부분에 있는 다섯 자리 숫자와 일치합니다. 그리고 나서 '(?(?=-)-\d{4})' 같은 형태를 한 조건이 나타납니다. 이 조건은 전방탐색 '?=-'를 사용해서 하이픈을 찾아내지만 소비하지는 않습니다. 그리고 하이픈이 있다는 조건을 만족하면, '-\d{4}'는 그 하이픈과 이어 나오는 숫자 4개와 일치합니다. 이 패턴을 사용하면 33333-은 일치하지 않습니다.

 

 

전방탐색과 후방탐색(positive와 nagative)은 조건으로 사용할 수도 있고, else 표현식('|')으로도 사용할 수 있습니다.

 

사실 더 간단한 방법으로 비슷한 결과를 얻을 수 있으므로 전후방탐색 조건은 자주 사용하지는 않습니다.

댓글