이 포스팅은 Regular Expression 시리즈 12 편 중 9 번째 글 입니다.
목차
개요
Lookahead Assertions는 이해하기가 어렵다. 하지만 예시와 함께라면 어떤 경우에 이를 사용해야 하는지 알 수 있을 것이다.
>>> p = re.compile(".+:")
>>> m = p.search("http://google.com")
>>> print(m.group())
http:
정규식 .+:
과 일치하는 문자열로 http:
를 돌려주었다. 만약 http:
라는 검색 결과에서 :
을 제외하고 출력하려면 어떻게 해야 할까? 위 예는 그나마 간단하지만 훨씬 복잡한 정규식이어서 그루핑은 추가로 할 수 없다는 조건까지 더해진다면 어떻게 해야 할까?
이럴 때 사용할 수 있는 것이 바로 전방 탐색이다. 전방 탐색에는 긍정(Positive)과 부정(Negative)의 2종류가 있고 다음과 같이 표현한다.
- 긍정형 전방 탐색(
(?=...)
) -...
에 해당되는 정규식과 매치되어야 하며 조건이 통과되어도 문자열이 소비되지 않는다. - 부정형 전방 탐색(
(?!...)
) -...
에 해당되는 정규식과 매치되지 않아야 하며 조건이 통과되어도 문자열이 소비되지 않는다.
긍정형 전방 탐색
긍정형 전방 탐색을 사용하면 http:
의 결과를 http
로 바꿀 수 있다.
>>> p = re.compile(".+(?=:)")
>>> m = p.search("http://google.com")
>>> print(m.group())
http
정규식 중 :에 해당하는 부분에 긍정형 전방 탐색 기법을 적용하여 (?=:)
으로 변경하였다. 이렇게 되면 기존 정규식과 검색에서는 동일한 효과를 발휘하지만 :
에 해당하는 문자열이 정규식 엔진에 의해 소비되지 않아(검색에는 포함되지만 검색 결과에는 제외됨) 검색 결과에서는 :
이 제거된 후 돌려주는 효과가 있다.
여기서 알 수 있는 사실은, Lookahead Assertions이 zero-width assertion이라는 사실이다. 실제 검색에 적용되기 위해서는 엔진이 이를 가지고 있어야 하는데 그렇지는 않고, 그저 있는지 확인만 해주는 역할이다. 이를 소비된다 라는 표현으로 적었다는 것은 앞 글에서 배웠다.
부정적 전방 탐색
자, 이번에는 다음 정규식을 보자.
.*[.].*$
이 정규식은 파일 이름 + . + 확장자
를 나타내는 정규식이다. 이 정규식은 foo.bar
, autoexec.bat
, sendmail.cf
같은 형식의 파일과 매치될 것이다.
이 정규식에 확장자가 “bat인 파일은 제외해야 한다”는 조건을 추가해 보자. 가장 먼저 생각할 수 있는 정규식은 다음과 같다.
.*[.][^b].*$
이 정규식은 | 메타 문자를 사용하여 확장자의 첫 번째 문자가 b가 아니거나 두 번째 문자가 a가 아니거나 세 번째 문자가 t가 아닌 경우를 의미한다. 이 정규식에 의하여 foo.bar 는 제외되지 않고 autoexec.bat 은 제외되어 만족스러운 결과를 돌려준다. 하지만 이 정규식은 sendmail.cf 처럼 확장자의 문자 개수가 2개인 케이스를 포함하지 못하는 오동작을 하기 시작한다. |
따라서 다음과 같이 바꾸어야 한다.
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
확장자의 문자 개수가 2개여도 통과되는 정규식이 만들어졌다. 하지만 정규식은 점점 더 복잡해지고 이해하기 어려워진다.
그런데 여기에서 bat
파일말고 exe
파일도 제외하라는 조건이 추가로 생긴다면 어떻게 될까? 이 모든 조건을 만족하는 정규식을 구현하려면 패턴은 더욱더 복잡해질 것이다. 이런 상황에서 우리는 부정적 전방 탐색을 사용한다.
.*[.](?!bat$).*$
확장자가 bat
가 아닌 경우에만 통과된다는 의미이다. bat
문자열이 있는지 조사하는 과정에서 문자열이 소비되지 않으므로 bat
가 아니라고 판단되면 그 이후 정규식 매치가 진행된다.
exe
역시 제외하라는 조건이 추가되더라도 다음과 같이 간단히 표현할 수 있다.
.*[.](?!bat$|exe$).*$