21세기소년, Python, 스터디

regex conditional

정규식의 조건절

조건절은 정규식에서는 흔히 쓰이는 표현은 아니다. 게다가 모든 정규식 엔진이 이를 지원하는 것도 아니다. 조건절을 사용해야 하는 경우라면 대부분의 경우 프로그래밍 로직으로 이를 보완하는1 형태로 많이 쓰이고 있고, 정규식 자체의 조건절이 꼭 필요한 케이스가 널리 알려져 있지 않기도 하다.

기본 문법

조건절의 IF...THEN...ELSE의 구성은 정규식에서 다음과 같이 표현한다.

(?(C)A|B)

여기서 C는 조건, A는 조건이 참일 때 매치하는 패턴, B는 조건이 거짓일 때 매치하는 패턴이다. 조건 자체가 패턴이 되지는 않고 다음과 같은 내용이 올 수 있다. (물론 해당 언어에서 지원한다면 말이다.)

  • 이미 세팅된 캡쳐링 그룹의 번호
  • 이미 세팅된 캡쳐링 그룹의 이름(이름이 지정된 경우)
  • 캡쳐링 그룹의 상대위치. 숫자에 부호를 붙여서 쓴다.
  • look-around 표현식
  • 서브루틴 콜
  • 재귀 매칭

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

정규식에서 캡쳐링 그룹(괄호로 둘러싼)에 매칭된 내용이 있는지 여부를 조건으로 많이 사용한다. 예를 들어 (?(1)foo|bar)는 앞서 캡쳐링 그룹으로 지정한 패턴에 매치되는 그룹이 있는 경우에 foo에 매치하려하고, 없다면 bar에 매치하려 할 것이다.

패턴 ^(START)?\d+(?(1)END|\b)를 보면

START012314352454END
^^^^^^^^^^^^^^^^^^^^ matched
0123134354556
^^^^^^^^^^^^^ matched
START0143452456E
                 not matched
12235345345END
               not matched

이런 결과를 찾게 된다. 즉 ^(?:START\d+END|\d+\b)와 같은 결과를 찾게 될 것이다.

캡쳐링 그룹 이름으로 쓰는 경우2

번호와 동일하나, 이름을 쓴다는 정도가 되겠다. 패턴 ^(?P<UC>[A-Z])?\d+(?(UC)_END)$는 다음과 같이 매치한다.

W1231435_END
^^^^^^^^^^^^
E12335
------
X1355_END
^^^^^^^^
ME12335_END
-----------
1232354
^^^^^^^
34636_END
---------

이 패턴은 다음과 같이 해석된다.

^               # 라인의 시작
(?P<UC>[A-Z])?  # 대문자 1개를 그룹 UC에 매칭한다. 있을 수도 없을 수도 있다.
d+             # 연속된 숫자가 온다.
(?(UC)_END)     # 그룹에 매칭된 경우에는 _END로 끝나야 한다.
$               # 라인의 끝

look-around 표현식 (*pcre3)

look-around 표현식을 사용, 부합하는 조건일때와 그렇지 않을 때의 패턴을 구분할 수 있다. 예시패턴 (?(?=.*_fruits)(?:apple|banana)|(?:potato|coffee))을 보자.

^               # 문장시작
(?              # 조건절 시작
  (?=.*_fruit)  # Look-ahead로 `_fruit`가 포함되어 있는지를 검사한다. 
  (?:apple|banana) # 만약 있다면 'apple', 'banana' 중 하나와 매치되는 문자열을 찾는다. 
  |             #  else
  (?:potato|coffee) # '_fruit'가 없다면 'potato'나 'coffee'와 매치한다. 
)

# 결과 
apple_fruits
^^^^^       
apple_nuts
     xxxxx
potato_fruits
      xxxxxxx : _fruits가 붙어있기 때문에 potato에 매치하지 않는다. 
coffee_nuts
^^^^^^

만약 조건절을 사용하지 않는다면 | 연산자를 사용해서 다음과 같이 구현한다.

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

응용: 구분자 밸런싱

파싱해야할 부분을 둘러싸는 구분자가 여러 종류인 경우 시작 구분자를 그룹핑하여 그에 맞는 종료 구분자까지를 매치하도록 한다. 만약 BEGIN:~:END 혹은 {{~}}으로 둘러싸지는 영역을 찾는다고 하면 다음과 같은 패턴이 유용할 수 있다.

(?:(BEGIN:)|({{)).*(?(1):END|(?(2)}}))

위 식은 다음과 같이 구분한다. 
(?:             # 캡쳐링하지 않는 그룹
(BEGIN:)|({{) # 'BEGIN:'이 있으면 1번 그룹, "{{"이 있으면 2번 그룹에 매치한다. 
)               # non-capturing 그룹 닫기
.*              # 모든 문자 매치
(?(1):END       # 만약 'BEGIN:'으로 시작했다면 ':END'로 끝나도록 
|(?(2)}})     # 그렇지않고(|) "{{"로 시작했다면 "}}"로 끝나도록 
)               # 조건절 종료

# 결과
BEGIN:asdfagdggf:END
^^^^^^^^^^^^^^^^^^^^
{{adgagfgdh}}
^^^^^^^^^^^^^
{ddfsdfh}

asdfasg}}:END

{{asdfasdgg:END}}
^^^^^^^^^^^^^^^^^
BEGIN:{{adasgdfg}}:END
^^^^^^^^^^^^^^^^^^^^^^
{{adasgdfg:END}}
^^^^^^^^^^^^^^^^
BEGIN:{{adasgdfg:END}}
^^^^^^^^^^^^^^^^^^^^
BEGIN:asdgafgfd}}

dafsegd:END

사실 이건 BEGIN:.*?:END|\{\{.*?\}\}으로 구현해도 된다….


  1. 자바스크립트 정규식은 조건절을 지원하지 않는다. 심지어 look behind도 지원하지 않잖아… 
  2. 캡쳐링 그룹에 이름을 쓰는 경우 파이썬에서는 (?P<foo> ... ) 와 같이 이름을 부여할 수 있다. 
  3. 파이썬에서는 지원하지 않음