자연어처리(NLP) 5일차(정규 표현식)

정민수
15 min readJun 7, 2019

--

19.06.07

본 글은 https://wikidocs.net/book/2155을 참고하여 작성되었음을 밝힙니다.

학습키워드

  • 정규 표현식(Regualr Expression)
  • 정규 표현식 모듈 함수
  • 정규 표현식을 이용한 토큰화

정규 표현식(Regular Expression)

텍스트 데이터를 전처리하다보면, 정규 표현식은 아주 유용한 도구로서 사용된다. 이번 챕터에서는 파이썬에서 지원하고 있는 정규 표현식 모듈 re의 사용법과 NLTK를 통한 정규 표현식을 이용한 토큰화에 대해서 알아본다.

1. 정규 표현식 문법과 모듈 함수

정규 표현식 모듈 re를 사용하면 특정 규칙이 있는 텍스트 데이터를 빠르게 정제할 수 있다. 본격적으로 정규 표현식에 대해서 실습하기 전에 앞서 정규 표현식을 위해 사용되는 특수 문자와 모듈 함수에 대해서 알아보자.

1) 정규 표현식 문법

정규 표현식 문법에는 역 슬래쉬(\)를 이용하여 자주 쓰이는 문자 규칙들이 있다.

2) 정규표현식 모듈 함수

정규표현식 모듈에서 지원하는 함수

앞으로 진행될 실습에서는 re.compile()에 정규 표현식을 컴파일하고, re.search()를 통해서 해당 정규 표현식이 입력 텍스트와 매치되는지를 확인하면서 각 정규 표현식에 대해서 이해해보도록 한다. re.search() 함수는 매치된다면 Match Object를 리턴하고, 매치되지 않으면 아무런 값도 출력되지 않는다.

2. 정규 표현식 실습

1) .

.은 한 개의 임의의 문자를 나타낸다. 예를 들어 정규 표현식이 a.c라고 한다면, a와 c사이에 어떤 1개의 문자라도 올 수 있다는 뜻이다.

import re
r = re.compile("a.c")
r.search("kkk")
# 아무런 결과도 출력되지 않는다.
r.search("abc")
<re.Match object; span=(0, 3), match='abc'>

2) ?

?는 ? 앞의 문자가 존재할 수도 있고, 존재하지 않을 수도 있는 경우를 나타낸다. 예를 들어서 정규 표현식이 ab?c라고 한다면, b는 있다고 취급할 수도 있고 없다고 취급할 수도 있다. 즉, abc와 ac 모두 매치할 수 있다.

r = re.compile("ab?c")
r.search("abbc")
# 아무것도 출력되지 않음
r.search("abc")
<re.Match object; span=(0, 3), match='abc'>
# b가 있는 것으로 판단하여 abc를 매치함.
r.search("ac")
<re.Match object; span=(0, 2), match='ac'>
# b가 없는 것으로 판단하여 ac를 매치함.

3) *기호

*은 바로 앞의 문자가 0개 이상일 경우를 나타낸다. 앞의 문자는 존재하지 않을 수도 있으며, 또는 여러 개일 수도 있다. 예를 들어 정규 표현식이 abc라고 한다면 ac, abc, abbc, abbbc 등과 매치할 수 있다.

r = re.compile("ab*c")
r.search("a")
# 아무것도 출력되지 않음.
r.search("ac")
<re.Match object; span=(0, 2), match='ac'>
r.search("abc")
<re.Match object; span=(0, 3), match='abc'>
r.search("abbc")
<re.Match object; span=(0, 4), match='abbc'>

4) + 기호

+는 *와 유사하다. 하지만 다른 점은 앞의 문자가 최소 1개 이상이어야 한다. 예를 들어 ab+c라고 하면, ac는 매치되지 않는다.

r = re.compile('ab+c')
r.search("ac")
# 아무것도 출력되지 않는다.
r.search("abc")
<re.Match object; span=(0, 3), match='abc'>

5) ^기호

^는 시작되는 글자를 지정한다. 가령 정규표현식이 ^a라면 a로 시작되는 문자열만을 찾아낸다.

r = re.compile('^a')
r.search('bbc')
# 아무것도 출력되지 않음
r.search('ab')
<re.Match object; span=(0, 1), match='a'>

6) {숫자} 기호

문자에 해당 기호를 붙이면, 해당 문자를 숫자만큼 반복한 것을 나타낸다. 예를 들어 정규 표현식이 ab{2}c라면 a와 c사이에 b가 존재하면서 b가 2개인 문자열에 대해서 매치한다.

r = re.compile("ab{2}c")
r.search("ac")
# 아무것도 출력되지 않는다.
r.search("abc")
# 아무것도 출력되지 않는다.
r.search("abbc")
<re.Match object; span=(0, 4), match='abbc'>
r.search("abbbbbc")
# 아무것도 출력되지 않는다.

7) {숫자1, 숫자2} 기호

문자에 해당 기호를 붙이면, 해당 문자를 숫자1 이상 숫자2 이하만큼 반복한다. 예를 들어 정규 표현식이 ab{2,8}c라면 a와 c사이에 b가 존재하면서 b는 2개 이상 8개 이하인 문자열에 대해서 매치한다.

r = re.compile("ab{2,8}c")
r.search('ac')
# 아무런 결과도 출력되지 않음
r.search("abc")
# 아무런 결과도 출력되지 않음
r.search("abbc")
<re.Match object; span=(0, 4), match='abbc'>
r.search("abbbbc")
<re.Match object; span=(0, 6), match='abbbbc'>
r.search("abbbbbbbbbbbbbc")
# 아무것도 출력되지 않음.

8) {숫자,} 기호

문자에 해당 기호를 붙이면 해당 문자를 숫자 이상만큼 반복한다. 예를 들어 정규 표현식이 a{2,}bc라면 뒤에 bc가 붙으면서 a의 갯수가 2개 이상인 경우인 문자열과 매치한다. 또한 만약 {0,}을 쓴다면 *와 동일한 의미가 되며, {1,}을 쓴다면 +와 동일한 의미가 된다.

r = re.compile("a{2,}bc")
r.search("bc")
# 아무런 결과도 출력되지 않음
r.search("aa")
# 아무런 결과도 출력되지 않음
r.search("aabc")
<re.Match object; span=(0, 4), match='aabc'>
r.search("aaaaabc")
<re.Match object; span=(0, 7), match='aaaaabc'>

9) [] 기호

[]안에 문자들을 넣으면 그 문자들 중 한 개의 문자와 매치라는 의미를 가진다. 예를 들어 정규 표현식이 [abc]라면, a 또는 b 또는 c가 들어가 있는 문자열과 매치된다. 범위를 지정하는 것도 가능하다. [a-zA-Z]는 알파벳 전부를 의미하며, [0–9]는 숫자 전부를 의미한다.

r = re.compile("[abc]")
r.search('zzz')
# 아무것도 출력되지 않음
r.search('a')
<re.Match object; span=(0, 1), match='a'>
r.search('acg')
<re.Match object; span=(0, 1), match='a'>
r.search('babo')
<re.Match object; span=(0, 1), match='b'>

이번에는 알파벳 소문자에 대해서만 범위를 지정하여 정규표현식을 만들어보고 문자열과 매치한다.

r = re.compile("[a-z]")
r.search('AAA')
# 아무것도 출력되지 않음
r.search('aA')
<re.Match object; span=(0, 1), match='a'>

10) [^문자] 기호

[^문자]는 5)에서 설명한 ^와는 완전히 다른 의미로 쓰인다. 여기서는 ^ 기호 뒤에 붙은 문자들을 제외한 모든 문자를 매치하는 역할을 한다. 예를 들어 [^abc]라는 정규 표현식이 있다면, a 또는 b 또는 c가 들어간 문자열을 제외한 모든 문자열을 매치한다.

r = re.compile('[^abc]')
r.search("a")
# 아무것도 출력되지 않음
r.search("ahoho")
<re.Match object; span=(1, 2), match='h'>
r.search("1st")
<re.Match object; span=(0, 1), match='1'>

3. 정규 표현식 모듈 함수 예제

re.compile()과 re.search() 이외에도 다른 여러 정규 표현식 모듈 함수에 대해서 알아보자.

(1) re.match()와 re.search()의 차이

search()가 정규 표현식 전체에 대해서 문자열이 매치하는지를 본다면, match()는 문자열의 첫 부분부터 정규표현식과 매치하는지를 확인한다. 문자열 중간에 찾을 패턴이 있다고 하더라도, match 함수는 문자열의 시작에서 패턴이 일치하지 않으면 찾지 않는다.

r = re.compile("ab.")
r.search("kkkabc")
<re.Match object; span=(3, 6), match='abc'>
r.match("kkkabc")
# 아무것도 출력되지 않음
r.match("abckkk")
<re.Match object; span=(0, 3), match='abc'>

위의 경우 ab.이기 때문에 ab뒤에 어떤 한 글자가 존재할 수 있다는 패턴을 의미한다. search 모듈 함수에 kkkabc라는 문자열을 넣어 매치되는지 확인한다면 abc라는 문자열에서 매치되어 Match object를 리턴한다. 하지만 match 모듈 함수의 경우 앞 부분이 ab.와 매치되지 않기 때문에, 아무런 결과도 출력하지 않는다. 하지만 abckkk로 시도하면 시작 부분과 매치되었기 때문에 정상적으로 Match object를 리턴한다.

(2) re.split()

split() 함수는 입력된 정규 표현식을 기준으로 문자열들을 분리하여 리스트로 리턴한다. 자연어 처리에 있어서 가장 많이 사용되는 정규 표현식 함수 중 하나인데, 토큰화에 유용하게 쓰일 수 있기 때문이다.

text = "사과 딸기 수박 멜론 바나나"
re.split(" ",text)
['사과', '딸기', '수박', '멜론', '바나나']
text = """사과
딸기
수박
멜론
바나나"""
re.split("\n", text)
['사과', '딸기', '수박', '멜론', '바나나']
text = "사과+딸기+수박+멜론+바나나"
re.split("\+", text)
['사과', '딸기', '수박', '멜론', '바나나']

(3) re.findall()

findall() 함수는 정규 표현식과 매치되는 모든 문자열들을 리스트로 리턴한다. 단, 매치되는 문자열이 없다면 빈 리스트를 리턴한다.

text = """이름 : 김철수
전화번호 : 010 - 1234 - 1234
나이 : 30
성별 : 남"""
re.findall("\d+", text)
['010', '1234', '1234', '30']
re.findall("\d+", "문자열입니당.")
[] # 빈 리스트를 리턴했다.

(4) re.sub()

sub() 함수는 정규 표현식 패턴과 일치하는 문자열을 찾아 다른 문자열로 대체할 수 있다.

text="Regular expression : A regular expression, regex or regexp[1] (sometimes called a rational expression)[2][3] is, in theoretical computer science and formal language theory, a sequence of characters that define a search pattern."re.sub('[^a-zA-Z]',' ',text)
'Regular expression A regular expression regex or regexp sometimes called a rational expression is in theoretical computer science and formal language theory a sequence of characters that define a search pattern '

위와 같은 경우, 영어 문장에 각주 등과 같은 이유로 특수 문자가 섞여 있다. 자연어 처리를 위해 특수 문자를 제거하고 싶다면 알파벳 외의 문자는 공백으로 처리하는 등의 사용 용도로 쓸 수 있다.

5. 정규 표현식 텍스트 전처리 예제

text = """100 John    PROF
101 James STUD
102 Mac STUD"""
re.split('\s+', text)
['100', 'John', 'PROF', '101', 'James', 'STUD', '102', 'Mac', 'STUD']
## '\s+' 는 공백 여러 개를 기준으로 split한다는 의미.re.findall('\d+', text)
['100', '101', '102']
## '\d+' 숫자로 이루어진 문자열을 리스트로 반환.
re.findall('[A-Z]', text)
['J', 'P', 'R', 'O', 'F', 'J', 'S', 'T', 'U', 'D', 'M', 'S', 'T', 'U', 'D']
## 대문자에 해당하는 문자를 리스트로 반환. 하지만 PROF, STUD 등을 알고 싶다.
re.findall('[A-Z]{4}',text)
['PROF', 'STUD', 'STUD']
re.findall('[A-Z][a-z]+', text)
['John', 'James', 'Mac']
## 대문자로 시작하고, 소문자가 이어지는 문자열을 반환 (이름)
letters_only = re.sub('[^a-zA-Z]', ' ', text)
letters_only
' John PROF James STUD Mac STUD'
## 영문자가 아닌 문자는 전부 공백으로 치환

6. 정규 표현식을 이용한 토큰화

NLTK에서는 정규 표현식을 사용해서 단어 토큰화를 수행하는 RegexpTokenizer를 지원한다. RegexpTokenizer()에서 괄호 안에 원하는 정규 표현식을 넣어서 토큰화를 수행하는 것이다.

import nltk
from nltk.tokenize import RegexpTokenizer
tokenizer = RegexpTokenizer("[\w]+")print(tokenizer.tokenize("Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop"))
['Don', 't', 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', 'Mr', 'Jone', 's', 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop']

\w+ 는 문자 또는 숫자가 1개 이상인 경우를 인식하는 코드이다. 그렇기 때문에 이 코드는 문장에서 구두점을 제외하고, 단어들만 가지고 토큰화를 수행한다.

tokenizer = RegexpTokenizer("[\s]+", gaps=True)print(tokenizer.tokenize("Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop"))
["Don't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name,', 'Mr.', "Jone's", 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop']

위 코드에서 gaps =True 는 해당 정규 표현식을 토큰으로 나누기 위한 기준으로 사용한다는 의미다. 따라서 \s+ 는 공백을 의미하므로 공백을 기준으로 토큰화된 결과값을 출력한다. gaps=True를 설정하지 않으면 공백만이 출력된다.(공백으로 시작하는 것들을 찾음) 그 전 결과와 비교한다면 어퍼스트로피나 온점을 제외하지 않고, 토큰화가 수행된 것을 확인할 수 있다.

참고자료

--

--

정민수
정민수

No responses yet