콘텐츠로 건너뛰기
Home » 정규 표현식의 조건절

정규 표현식의 조건절

흔히 쓰이는 경우는 아니지만 정규 표현식에도 조건절을 사용할 수 있다. 참고로 모든 정규식 엔진이 조건절을 지원하는 것은 아니다. (javascript 정규식은 조건절을 지원하지 않는다.) 만약 현재 사용하는 편집기나 언어의 정규식 엔진에서 조건절을 지원한다면, 조건절을 사용해서 보다 정교한 매칭을 하는 것이 가능하다. 오늘은 정규표현식의 조건절 사용법에 대해 살펴보도록 하겠다.

기본 문법

정규 표현식의 조건절에서 조건은 특정한 캡쳐링 그룹의 여부이다. 앞에서 특정한 패턴으로 그룹을 정의하고, 해당 그룹이 매칭되었는지 여부에 따라서 A 혹은 B 패턴을 적용하는 방식으로 사용된다. 가장 간단한 형태는 다음과 같다.

(Z)?.*(?(1)A|B)

위 패턴에서 “Z”는 기준이 되는 조건 패턴이고, A는 Z그룹이 매치할 때 적용할 패턴, B는 Z그룹이 매치하지 않을 때 적용할 패턴이다. 이 때 ?(1) 의 1 대신에 다음과 같은 것들이 올 수 있다.

  • 앞에서 언급한 캡쳐링 그룹의 번호
  • 캡쳐링 그룹에 이름을 지정할 수 있는 경우, 해당 그룹 이름.
  • 숫자에 부호를 붙이는 경우에는 그룹의 상대 위치를 offset 방식으로 쓸 수 있다.
  • look-around 표현식
  • 서브루틴 콜
  • 재귀 매칭

서브루틴 콜이나 재귀 매칭은 어차피 이해하기도 어렵고 (나도 잘 모르겠…) 펄 정규식 외에는 거의 지원도 안돼서 딱히 알아보지 않으려고 한다. 또한 조건절을 쓰는 패턴은 OR 패턴으로도 대체가 거의 가능하다. (대신에 패턴이 매우 길어지거나 복잡해질 수는 있다.)

캡쳐링 그룹 번호를 조건으로 쓰는 경우

정규표현식에서 가장 일반적으로 사용하는 조건절은 특정한 그룹의 매치 여부에 따라 뒤쪽의 패턴이 달라지는 것이다. 다음 예는 연속된 숫자값이 있는 라인을 찾을 때 사용하는 패턴인데, 두 가지 경우가 있다고 가정한다.

  • “START”로 시작하고 숫자가 연속되며 다시 “END”로 끝나는 경우
  • “START”와 “END”가 모두 없고 라인 전체가 숫자로만 이루어지는 경우

이 규칙에 의하면 “START”로 시작했지만, 뒤에 “END”가 없거나, “START”가 없는데 뒤에 “END”가 붙은 라인은 모두 무효로 봐야하며, 결국 “START”의 매치 여부에 따라서 “END”를 체크할 것인지가 결정된다. 이 때 사용할 수 있는 패턴은 다음과 같다.

^(START)?(\d+)(?(1)END|\b)$
^
 (START)?                   -- START로 시작했는지 그룹 1에 캡쳐
         (\d+)              -- 연속된 숫자를 그룹2에 캡쳐
              (?(1)END|\b)  -- 그룹1이 있다면 END에 매치, 없다면 \b에 매치
                          $ -- 문자열의 끝
START012314352454END  -- 매치된다. 
0123134354556         -- 매치되지 않는다.
START0143452456E      -- 뒤에 END가 없으므로 매치되지 않는다.
12235345345END        -- START없이 END만 있으므로 매치되지 않는다.

사실 이 패턴은 아주 단순해서, 조건절을 지원하지 않는 정규식으로도 다음과 같이 바꿔 쓸 수 있다. ^(?:START(\d+)END|(\d+))$ 하지만 이 코드는 매치 여부는 동일하지만, 결과가 조금 다르다. 조건절을 사용한 패턴에서는 연속된 숫자 부분만 얻으려면 항상 GROUP 2를 확인하면 되지만, 조건절을 지원하지 않는 후자의 패턴에서는 START~END가 있으면 GROUP1에, 없으면 GROUP2를 확인해야 한다.

캡쳐링하는 그룹이 많다면 번호를 가지고 지정하는 그룹이 헷갈리기 쉽기 때문에, 캡쳐링 그룹에 이름을 지정할 수 있다. (?P<name> pattern) 과 같은 식으로 ?P<name> 을 사용해서 그룹에 이름을 지정할 수 있다. PCRE 정규식에서는 (?<name>)(?P<name>) 이 모두 지원되나, 파이썬에서는 후자의 패턴만 허용된다. 참고로 이 때 그룹이름은 영문자여야 하고 숫자로 시작될 수 없다.

선후방 검색에 의한 조건절

선후방 검색에 의한 조건절을 사용하는 방법도 있다. 그룹에 의해 조건절을 사용하는 방식은 조건으로 사용되는 그룹이 앞부분에 나와야 한다는 한계가 있다. 하지만 전방탐색을 사용하여 뒤쪽에 나올 패턴을 미리 확인해서 앞에서 조건에 따른 패턴을 적용하는 방식이다. 참고로 선후방 검색에 의한 조건절은 PCRE 정규식에서만 사용 가능하며, 파이썬에서는 사용할 수 없다.

이 예제에서는 “이름_종류”가 올바르게 연결된 문자열에서 이름에 매치하는 패턴을 살펴보겠다.

(?(?=.*_fruit)(?:apple|banana)|(?:potato|coffee))
(?(?=.*_fruit)            -- 나중에 "_fruit"가 나온다면 참이다.
       (?:apple|banana)   -- 참이라면 "apple", "banana" 중에서 매치한다.
      |(?:potato|coffee)) -- 거짓이라면 "potato", "coffee" 중에서 매치한다.
apple_fruit  - "apple"에 매치
apple_nuts   - 매치안함
potato_fruit - 매치안함
coffee_nuts  - "coffee"에 매치

파이썬에서는 다음과 같이 OR 패턴으로 위 식을 대체할 수 있기는 하다.

^(?:(?:apple|banana)(?=.*_fruits))|(?:(?:potato|coffee)(?!.*_fruits$))

예제 – 핸드폰 번호 매칭하기

핸드폰 번호를 찾는 패턴을 작성해보자. 핸드폰 번호의 규칙에는 다음과 같은 규칙이 있다.

  1. 세자리 – 세(네)자리 – 네자리 형태로 구성된다.
  2. 각 마디 사이에는 – 이나 . 이 들어갈 수 있다.
  3. 첫 마디에는 010, 011, 016, 017, 018, 019 가 올 수 있다.
  4. 첫 마디가 010인 경우에는 가운데 마디가 항상 4자리이다. 그외의 식별 번호는 가운데 마디가 3혹은 4자리이다.

첫번째 마디가 010인지 여부에 따라서 가운데 마디의 패턴이 달라진다. 이 경우는 정규 표현식의 조건절을 사용하면 비교적 간단? 하게 체크할 수 있다.

^(?:(?P<a>010)|01[16789])(?:(?P<b>[\-\.])?(?(a)\d{4}|\d{3,4}))(?:(?(b)(?P=b))\d{4})

^(?:(?P<a>010)|01[16789])  - 첫마디는 010 이거나, 01[16789]로 시작한다.
 (?:(?P<b>[\-\.])?          - 두번째 마디 앞에는 - 이나 . 이 올 수 있다. 
                            - 이 구분 문자는 b라는 이름의 그룹으로 캡쳐한다.
    (?(a)\d{4}|\d{3,4}))   - 첫마디가 010이었다면 둘째마디는 4자리, 
                           - 그외에는 3~4자리가 된다.
 (?:(?(b)(?P=b))           - 앞 마디에 구분문자(b)가 있었다면 똑같은 문자가 온다
     \d{4})                - 마지막 4자리가 온다.

이 패턴으로는 다음과 같은 번호 패턴들은 매치된다.

010-1234-1234
011-1234-1234
010.1234.5678
011.123.2134
01012345678
01112345678
0111235678

다음과 같은 패턴들은 매치되지 않는다.

010-123-1234  : 010인데 가운데가 3자리임
014-123-1235  : 식별번호가 틀림
010.1234-5678 : 마디 구분 문자가 일관되지 않음
0101234-5678  : 마디 구분 문자가 뒤에만 쓰임
010-12345678  : 마디 구분 문자가 앞에만 쓰임