전체 페이지뷰

2016년 12월 6일 화요일

Regular expression with Python, Look ahead

지금까지 우리는 문자를 매칭하는 방법들에 대해 배워왔습니다. 그러나 그것은 동시에 그것은 매칭된 문자들을 버리는 방법이기도 했습니다. 무슨 말인가 하면 특정 패턴에 한번 매칭된 문자들은 그 연산 내에서는 다시 비교될 수 없다는 것입니다.


그 예외로 소위 zero-width assertion이라 불리는 몇가지 메타캐릭터에 대해 공부했는데, 그것이 바로 문자열의 처음을 지정하는 ^과 마지막을 뜻하는 $입니다. 그러나 이것도 단순히 비교되는 위치만을 지정할 뿐이지 문자열 내의 특정 위치에서부터 검색을 하라고 지정해 주는 것은 아닙니다.
(zero-width 라는 것은 폭이 0이라는 뜻입니다. 다시 말해 공간을 차지하지 않아 검색하고자 하는 문자열 내에서 어떤 문자를 특별히 소모하지 않고 위치 인덱스 만을 나타냅니다)

보다 강력한 방법이 look around(전후방탐색)입니다. 
Look around는 look ahead(전방탐색), 과 look behind(후방탐색)으로 나뉩니다.
이 둘은 또 각기 positive(긍정형), negative(부정형)의 두가지로 나뉘어 집니다.

Positive look ahead: (?=regex)의 문법으로 표현합니다. regex(정규표현식)이 매치되는 앞부분을 탐색하여 매칭시키라는 뜻입니다.
Negative look ahead: (?!regex) regex와 문자열이 일치하지 않을때만 전방을 탐색하라는 뜻입니다.
Positive look behind: (?<=regex) regex와 매칭되는 후방부분을 탐색하라는 뜻입니다.
Negative look behind: (?<!regex) regex와 매칭되지 않을 때 후방부분을 탐색하라는 뜻입니다.

지금은 이해가 잘 가지 않는 부분이 있습니다. 하나씩 살펴보도록 하겠습니다.

Look ahead

Positive look ahead

첫 번째로 전방탐색부터 알아봅시다. 아주 간단한 예로부터 시작하겠습니다.

>>> pattern = re.compile(r'fox')
>>> result = pattern.search("The quick brown fox jumps over the lazy dog")
>>> print (result.start(), result.end())
16 19

/fox/라는 리터럴을 매칭시켰습니다. 16에서 시작되어 19를 인덱스로 갖는 패턴 매칭이 이루어졌습니다.

이 때 (r'fox')(r'(?=fox)')로 바꾸면 어떻게 될까요?
>>> pattern = re.compile(r'(?=fox)')
>>> result = pattern.search("The quick brown fox jumps over the lazy dog")
>>> print (result.start(), result.end())
16 16

인덱스 시작과 끝이 16입니다. 지정된 패턴 전체를 가리키는게 아니라 시작점만을 가리킵니다.

다음 예를 들어봅시다.

>>> pattern = re.compile(r'\w+,')
>>> pattern.findall("They were three: Felix, Victor, and Carlos.")
['Felix,', 'Victor,']

문자 뒤에 콤마가 오는 패턴을 일치시켰습니다. 결과에 ','까지 포함되어 있습니다.
이제 패턴을 전방검색형으로 바꿔주면,

>>> pattern = re.compile(r'\w+(?=,)')
>>> pattern.findall("They were three: Felix, Victor, and Carlos.")
['Felix', 'Victor']

결과에서 컴마가 빠져있음을 알 수 있습니다.
앞의 패턴에서 우리는 문자+','로 이루어진 패턴을 매칭시키도록 요구한 것이고,
뒤에서는 컴마가 오는 앞쪽 문자들을 매칭하도록 요구한 것입니다.

조금더 바꿔봅시다.

>>> pattern = re.compile(r'\w+(?=,|\.)')
>>> pattern.findall("They were three: Felix, Victor, and Carlos.")
['Felix', 'Victor', 'Carlos']

패턴을 조금 바꾸어 컴마, 혹은 마침표(마침표는 메타캐릭터이므로 백슬래시로 escape 처리했습니다) 앞에 오는 영문자들을 매칭시키도록 한 결과입니다. 간단히 추출하고자 하는 내용만을 뽑아낼 수 있었습니다.

Negative look ahead

앞에서 말한 바와 같이 (?!regex)의 문법을 사용하며 매치되지 않을 때 전방을 검색합니다.
이 방식은 제외하고자 하는 결과가 있을 때에 유용합니다.

예를 들어 'John'이라는 사람들을 검색하고 싶은데 그 중  'John Smith'만을 제외하고 싶다면 다음과 같이 합니다.

>>> pattern = re.compile(r'John(?!\sSmith)')
>>> result = pattern.finditer("I would rather go out with John McLane than with John Smith or John Bon Jovi")
>>> for i in result:
print(i.start(), i.end())

27 31
63 67

John 다음에 공백+Smith가 오는 결과는 건너뛰게 됩니다.

Look around and substitutions


Look around의 zero-width 속성은 치환 연산에서 특별히 유용합니다.
그 전형적인 예가 숫자로만 이루어진 '1234567890'을 자리수 구분 컴마가 들어간 '1,234,567,890'으로 바꾸는 것입니다.

한번 그 과정을 따라가 봅시다.

일단 세자리 수로 구분해줘야 할 것입니다.
>>> pattern = re.compile(r'\d{1,3}')
>>> pattern.findall("The number is: 12345567890")
['123', '455', '678', '90']

실패로군요. 우측에서부터 세자리씩 끊어야 하는데 좌측에서부터 끊었습니다.
컴마의 가장 좌측 부분은 1,2,3개의 숫자가 모두 올수 있습니다. 뒤에 올 숫자의 개수에 따라 유동적이죠.  말로 표현하자면 1~3개의 숫자가 온 뒤에 컴마가 오고 세자리 수들이 오고 끝 부분은 더 이상 숫자가 없는 것으로 마무리 지어 집니다.

이 표현을 정규식으로 바꿔 봅시다.

\d{1,3}(?=(\d{3})+(?!\d))

편의상 뒤에서부터 생각해 보면,
(?!\d) 마지막은 숫자가 아니고
(\d{3})+ 세자리수가 하나 이상 나오는
(?= ) 패턴이 있다면 그 앞 부분을
\d{1,3} 1개에서 3개까지의 숫자로 매칭시키라는 표현입니다.


실제로 쉘에서 적용해 봅시다.
>>> pattern = re.compile(r'\d{1,3}(?=(\d{3})+(?!\d))')
>>> results = pattern.finditer('1234567890')
>>> for result in results:
print(result.start(), result.end())

0 1
1 4
4 7

이제 1,234,567,890으로 잘 나뉘어 졌음을 알 수 있습니다.

이제 여기에 컴마를 넣어 봅시다.

>>> pattern.sub(r'\g<0>,', "1234567890")
'1,234,567,890'

성공입니다.

댓글 없음:

댓글 쓰기