이번에 살펴볼 것은 자연어처리(Natural Language Processing)에서 가장 기본이 되는 REGULAR EXPRESSION에 대해서 다뤄볼까 합니다. 자연어처리를 할 때에 필히 사용되는 것이 바로 문자열 내에 특정 pattern으로 검색하는 것인데요, 예제들을 통해서 어떤 마법같은 기능들을 할 수 있는지 알아보겠습니다.

먼저 기본적으로 regular expression(regex)를 사용하기 위해서 re라는 라이브러리를 import해야합니다.

import re

그리고 re.search를 이용해서 등장하는 첫 부분에 대한 정보를 return 받으실 수 있습니다. 만약 그런 패턴이 없다면 None을 리턴받게 됩니다.

##Basic regular expressions
>>> pairs = [(r'woodchucks', 'interesting links to woodchucks and lemurs'),
         (r'a', "Mary Ann stopped by Mona's"),
         (r'Claire says,', 'Dagmar, my gift please, Claire says,'),
         (r'song', 'all our pretty songs'),
         (r'!', "You've left the burglar behind again! said Nori")]

>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     print s.start(), s.end(), string[s.start():s.end()]
21 31 woodchucks
1 2 a
24 36 Claire says,
15 19 song
36 37 !

위의 결과들을 보시면 아시겠지만, pattern들이 등장하는 첫번째에 대한 정보를 s로 받아와서 s.start()s.end()를 통해서 그에 대한 위치 정보를 확인할 수 있죠?

그런데 다음과 같은 상황을 가정해 봅시다. ‘woodchuck’이란 단어가 문장의 가장 처음에 사용될 수도 있습니다. 이때에는 ‘Woodchuck’으로 사용되겠죠? 그러면 이 두가지 경우를 동일시해서 한번에 찾을 수 있는 방법은 없을까요? 이 때 사용되는 것이 바로 [] square bracket입니다. []안의 문자열은 or로 생각되어서 그 안에 있는 모든 문자열 중 하나라도 있으면 찾게 되는 것입니다. 그럼 예제를 살펴보죠.

#여러 문자열을 or조건으로 검색하는 방법
>>> pairs = [(r'[wW]oodchuck', 'Woodchuck'),
         (r'[abc]', "In uomini, in soldati"),
         (r'[1234567890]', 'plenty of 7 to 5')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     print s.start(), s.end(), string[s.start():s.end()]
0 9 Woodchuck
18 19 a
10 11 7

위의 결과에서 확인할 수 있듯이, [wW]의 조건을 통해서 첫글자가 대소문자 관계없는 woodchuck을 검색할 수 있었구요, [1234567890]을 통해서 숫자(0~9까지 아무거나)도 검색할 수 있었습니다. 그런데 이렇게 일일이 숫자를 다 쳐서 넣으니까 번거로우시죠? Square bracket 안에 -를 통해서 일정 범위의 문자열을 쉽게 표현할 수 있습니다.

# [] 안에 '-'로 range를 나타낼 수 있습니다.
>>> pairs = [(r'[A-Z]', 'we should call it "Drenched Blossoms"'),
         (r'[a-z]', 'my beans were important to be hoed!'),
         (r'[0-9]', 'Chapter 1: Down the Rabbit Hole')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     print s.start(), s.end(), string[s.start():s.end()]
19 20 D
0 1 m
8 9 1

첫번째 예제는 [A-Z]를 통해서 대문자를 정의하고 있구요, 두번째 예제는 [a-z]를 통해서 소문자를, 마지막 예제는 [0-9]를 통해서 숫자를 정의하고 있습니다. 아주 신기방기하지 않나요? :)

반면에 또 square bracket 안에 ^(caret)을 통해서 ‘특정 패턴을 제외하고’의 의미를 담을 수도 있습니다.

# [] 안에 '^'로 제외의 의미를 나타낼 수 있습니다.
>>> pairs = [(r'[^A-Z]', 'Oyfn pripetchik'),
         (r'[^Ss]', "I have no exquisite reason for't"),
         (r'[^\.]', "our resident Djinn"),
         (r'[e^]', "look up ^ now"),
         (r'a\^b', "look up a^b now")]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     print s.start(), s.end(), string[s.start():s.end()]
1 2 y
0 1 I
0 1 o
8 9 ^ #square bracket의 마지막에 '^'이 들어가면 문자 자체를 검색하고,
8 11 a^b #아니면 패턴에 '\'를 붙이게 되면 또 문자 그대로의 '^'을 검색하게 됩니다.

첫번째 예제는 대문자를 제외하는 것이고, 두번째는 ‘S’나 ‘s’를 제외하는 것, 세번째 예제는 ‘.’를 제외하는 것이고, 네번째, 다섯번째 예제는 있는 그대로 ‘^’문자열을 찾는 방법이었습니다.

이제는 다음과 같은 경우를 상상해 봅시다. ‘woodchuck’은 셀 수 있기 때문에 ‘woodchucks’로 등장할 수도 있습니다. 또한 ‘color’도 어쩌다 보면 ‘colour’로도 표현되기도 합니다. 이럴 때 0개 또는 1개가 나타날 수도 있다는 의미로 ?가 사용됩니다.

# '?'를 통해서 하나 또는 nothing을 표현할 수 있습니다.
>>> pairs = [(r'woodchucks?', 'woodchuck'),
         (r'colou?r', 'colour')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     print s.start(), s.end(), string[s.start():s.end()]
0 9 woodchuck
0 6 colour

신기하죠?? :) 또한 어떤 연속된 문자의 패턴은 어떻게 하면 찾을 수 있을까요? 예를들면, ‘baaaaaaaaaaaa’이런 문자열이 있을 때 말이죠. 정확히 몇 개의 ‘a’가 등장할 지 모를때에 사용되는 것이 바로 *입니다. *는 0개 또는 그 이상의 연속된 문자열을 잡아내게 됩니다.

# '*'를 통해서 0개 이상의 연속된 문자를 표현할 수 있습니다.
>>> pairs = [(r'a*', 'baaaa!'),
         (r'aa*', 'baaaa!'),
         (r'[ab]*', 'baaaa!'),
         (r'[0-9][0-9]*', '123a45')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     print s.start(), s.end(), string[s.start():s.end()]
0 0 
1 5 aaaa
0 5 baaaa
0 3 123

첫번째 예제는 ‘b’로 시작하는데, 이 때는 ‘a’가 0개 사용된 것이 맨 앞에 있다고 생각할 수 있으므로 위와 같은 결과를 보이는 것입니다. 만약 ‘a’가 1개 이상 등장하면서 연속된 것을 찾고 싶으면 위와 같이 ‘aa*‘로 검색을 하셔야 하구요, [ab]*는 ‘a’또는 ‘b’가 연속된 패턴을 찾아내게 됩니다. 맨 마지막 예제는 연속된 숫자를 찾아내는 패턴이 되겠지요?

그러면 굳이 이렇게 두번씩 써가면서 체크를 해야될까요? 아닙니다. 바로 +를 이용해서 최소 1개 이상의 문자가 연속된 것을 확인할 수 있습니다.

# '+'를 통해서 최소 1개 이상의 연속된 문자를 표현할 수 있습니다.
>>> pairs = [(r'baa+!', 'baaaaa!'),
         (r'baa+!', 'ba'),
         (r'[0-9]+', '123a4')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     if s:
...         print s.start(), s.end(), string[s.start():s.end()]  
...     else:
...         print 'Not Found'
0 7 baaaaa!
Not Found
0 3 123

그럼 우리가 어떤 문자인지는 모르겠는데 그냥 한 문자를 나타내려면 어떻게 해야할까요? .를 통해서 간단하게 나타낼 수 있습니다.

# '.'은 wildcard 문자로서 어떤 글자든지 한 글자를 대신 표현합니다.
>>> pairs = [(r'beg.n', 'begin'),
         (r'beg.n', "beg'n"),
         (r'beg.n', "begun")]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     if s:
...         print s.start(), s.end(), string[s.start():s.end()]  
...     else:
...         print 'Not Found'
0 5 begin
0 5 beg'n
0 5 begun

앞서 ‘^’(caret)이 문자열을 제외하거나 ‘^’자체를 의미할 때 쓰인다고 했는데요, 이것이 시작 문자열을 매치할 때에도 사용되게 됩니다. 또한 ‘$’는 끝 문자열을 매치하는데 사용됩니다.

# '^'는 시작 문자열을 매치할때도 사용됩니다. 그리고 '$'는 끝 문자열을 매치하는데 사용됩니다.
>>> pairs = [(r'^The', 'The dog'),
         (r'^The', "I'm The"),
         (r'dog$', 'The dog'),
         (r'dog$', 'The dog.'),
         (r'^The dog\.$', 'The dog.'),
         (r'^The dog\.$', 'The dog. man.')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     if s:
...         print s.start(), s.end(), string[s.start():s.end()]  
...     else:
...         print 'Not Found'

마지막으로 ‘\b’는 단어의 boundary 조건을 부여해주는 문자입니다. 예를 들면, 우리가 순수한 단어 ‘the’를 찾고 싶고, ‘other’나 ‘another’에 들어간 ‘the’는 찾고싶지 않다면 ‘\bthe\b’를 검색함으로써 순수한 ‘the’를 찾아낼 수 있습니다.

# '^'는 시작 문자열을 매치할때도 사용됩니다. 그리고 '$'는 끝 문자열을 매치하는데 사용됩니다.
>>> pairs = [(r'\bthe\b', 'here is the cup'),
         (r'the', 'you are another person'),
         (r'\bthe\b', 'you are another person')]
>>> for pattern, string in pairs:
...     s = re.search(pattern, string)
...     if s:
...         print s.start(), s.end(), string[s.start():s.end()]  
...     else:
...         print 'Not Found'
8 11 the
11 14 the
Not Found

이렇게 기본적인 regular expression에 대해서 알아봤는데요, 우리가 지금까지 배운것만으로도 꽤 멋진 문자열 검색을 할 수 있답니다 :)