데이터 중에 문자를 분석하는 경우가 있을 것이다.
복잡한 문자열을 처리할 때 빠질 수 없는 것이 "정규 표현식" 이다.
정규 표현식에 대해 알아보자.
정규표현식을 제대로 들어가기 전에 어떤 기능을 사용하는지 예시를 살펴보자.
# 주민등록번호 뒷자리를 * 로 변경하기
minsu = '950213-1390192'
한 명의 주민등록번호가 있다고 가정할 때 뒷자리(7개)를 '*' 로 변경하고 싶다고 하자.
정규표현식을 사용하지 않는다면 꽤 긴 코드가 나올 것이다.
하지만 정규표현식을 사용한다면?
import re
setting = re.compile("(\d{6})[-]\d{7}")
print(setting.sub("\g<1>-*******", minsu))
------------------------------------------
950213-*******
다음과 같이 잘 나오는 것으로 알고 있다. 정규표현식은 어떠한 언어든 동일한 형식을 가지고 있기 때문에 파이썬이 아니더라도 유용하게 사용할 수 있다.
잠깐 보아도 지금 알아야 할 것은 "import re" 와 "compile" 그리고 "sub" 가 보인다. 형식에 대해서도 많이 배워야 한다.
첫 번째 : 문자 다루기
#1 문자는 대괄호 !
문자를 파싱할 때는 대괄호[ ] 를 사용합니다. 만약 [abcde] 가 있다고 가정했을 때
- "a" 가 있다면 매치 성공
- "enjoy" 가 있다면 'e' 가 존재하므로 매치 성공
- "xyz" 가 있다면 어느하나 매치되지 않으므로 패스가 됩니다.
#2 연속되는 문자는 '-' !
그렇다면 소문자 모두를 매치하고 싶을 때 [abcdefghijklmnopqrstuvwxyz] 로 한다면 너무 길어지기 때문에 이 때 사용하는 것이 "-" 입니다. 소문자 전체를 매치하고 싶다면 [a-z] 로 하면 됩니다. 대문자를 하고 싶다면 [A-Z] 가 될 것입니다. 소문자와 대문자 전체를 하고 싶다면? [a-zA-Z]가 됩니다.
- [a-z] : 영어 소문자 전체
- [A-Z] : 영어 대문자 전체
- [0-9] : 숫자 전체
#3 모든 문자와 매치할 때는 Dot(.) !
만약 어느 문자나 숫자 등이 올 지 모를 때 사용하는 것이 바로 dot(.) 이다. dot 은 줄바꿈(\n)을 제외한 모든 문자와 매치가 가능하다. 예를 들어 'a.c' 라고 정규표현식을 썼을 때 "abc" 도 가능하고 "adc", "a2c" 또한 매치에 성공한다.
#4 반복 문자 매치할 때는 별표(*) !
몇 번이 연속되는 문자도 매치하고 싶을 떄 사용하는 것이 별표(*) 이다. 별표 이전에 있는 문자를 판단한다. 예를 들어 [ab*c] 라는 정규표현식을 썼을 때 대상은 b 가 될 것이다. b가 0번 이상 반복된다면 매치에 성공한다. 특이한 점이 바로 0번도 포함된다는 것이다. 0번이 포함되지 않는 반복 매치는 플러스(+) 를 사용하면 된다.
- "ac" : b가 0번 반복 되었다. 매치 성공
- "abc" : b가 1번 반복 되었다. 매치 성공
- "abbbbbbc" : b가 6번 반복 되었다. 매치 성공
- "abbbb" : c 가 없으므로 매치 실패.
#5 반복되는 문자수를 지정하고 싶을 때는 중괄호 사용하기!
만약 내가 'abbbc' 라고 b 가 3번만 반복되는 문자를 찾고 싶을 때 사용하는 것은 중괄호{ } 이다.
표현식은 [ab{3}c] 이다. 이렇게 한다면 b가 3번 반복 이외에는 매치되지 않는다.
- 표현식 'ab{3}c' --> "abbbc" 매치 성공
- 표현식 'ab{3}c' --> "abc" 매치 실패
- 표현식 'ab{1,3}c' --> "abc" 매치 성공 (1,3 이므로 1이상 3이하이다.)
- 표현식 'ab?c' --> "ac" 매치 성공 (물음표는 0개 또는 1개 있을 때를 말한다. {0,1} 과 같음)
# 기타 문자 클래스
정규 표현식 | 설명 |
\d | 숫자를 매치한다. [0-9] 와 같음 |
\D | 숫자가 아닌 것과 매치한다. [^0-9] 와 같음 |
\s | 공백문자. space 나 tab 같은 공백과 매치한다. \t, \n, \r, \f, \v 와 같음. |
\S | \s에 해당 안되는 것과 매치된다. |
\w | 문자 + 숫자와 매치된다. [a-zA-Z0-9_]와 같다. |
\W | \w 와 반대된다. |
이제 파이썬을 통해 실제 정규표현식을 사용하는 시간을 가져보자.
import re
setting = re.compile('ab?c')
기본적으로 re 를 import 하고 compile 안에 지금까지 배운 표현식을 넣습니다.
그리고 표현식을 통해 매치하는 4가지 방법을 실행하겠습니다.
(1) match
p = re.compile('ab?c')
m = p.match('1 abbbbc')
print(m)
------------------------
None
import re
p = re.compile('ab?c')
m = p.match('1 abc')
print(m)
------------------------
None
match는 들어오는 문자열 전체가 표현식에 해당하지 않는다면 None 값을 반환합니다.
match 객체의 주요 메소드는 다음과 같습니다.
- start( ) : 매치된 문자열의 시작 위치 리턴
- end( ) : 매치된 문자열의 마지막 위치 리턴
- group( ) : 매치된 문자열 리턴
- span( ) : 매치된 문자열의 (시작 위치, 마지막 위치) 값을 튜플형태로 리턴
p = re.compile('[a-z]+')
m = p.match('minsu')
print(m.start())
print(m.end())
print(m.group())
print(m.span())
-------------------------
0
5
minsu
(0, 5)
(2) search
p = re.compile('ab?c')
m = p.search('1 abbbbc')
print(m)
--------------------------
None
p = re.compile('ab?c')
m = p.search('1 abc')
print(m)
--------------------------
<_sre.SRE_Match object; span=(2, 5), match='abc'>
match 와는 다르게 1 이 있지만 abc는 표현식에 해당되므로 'abc'가 반환되는 모습을 볼 수 있습니다.
(3) findall (return list)
p = re.compile('[a-z]+')
m = p.findall('Hi my name is minsu')
print(m)
------------------------------------
['i', 'my', 'name', 'is', 'minsu']
findall 은 표현식에 매치되는 문자를 모두 가져오는 방식이다.
위의 표현식은 반복이 가능한 영어 소문자를 표현했고, 예시문 "Hi my name is minsu" 에서 H 만 대문자 이므로 반환되지 않았다.
(4) finditer
p = re.compile('[a-z]+')
m = p.finditer('Hi my name is minsu')
print(m)
------------------------------------------------
<callable_iterator object at 0x00000235E76E7208>
p = re.compile('[a-z]+')
m = p.finditer('Hi my name is minsu')
for i in m:
print(i)
--------------------------------------
<_sre.SRE_Match object; span=(1, 2), match='i'>
<_sre.SRE_Match object; span=(3, 5), match='my'>
<_sre.SRE_Match object; span=(6, 10), match='name'>
<_sre.SRE_Match object; span=(11, 13), match='is'>
<_sre.SRE_Match object; span=(14, 19), match='minsu'>
finditer 는 반환값으로 iterator object 가 되기 때문에 출력값을 보고 싶을 때는 하나하나 출력해야 한다.
[ compile 옵션 알아보기 ]
re.compile( ) 을 사용하면서 표현식을 넣는 것 이외에 다른 기능도 있다.
옵션 | 설명 | 예시 |
DOTALL 또는 S | dot(.) 은 줄바꿈은 제외하지만 DOTALL 을 사용하면 줄바꿈을 포함한다. |
p = re.compile('a.c', re.DOTALL) m = p.match('a\nb') |
IGNORECASE 또는 I | 대소문자 구별을 무시하고 매치시켜준다. | p = re.compile('[a-z]', re.I) print(p.match('minsu')) # 매치 성공 print(p.match('Minsu')) # 매치 성공 print(p.match('MINSU')) # 매치 성공 |
MULTILINE 또는 M | 새로운 줄마다 새로운 시작으로 생각함. | p = re.compile('^name', re.M) words = '''name hello # 매치 hungry man name what # 매치 ''' |
VERBOSE 또는 X | 정규표현식이 길어질 때 나누어 쓰는 기능 | p = re.compile('''정규표현식 .... ... ...''', re.VERBOSE) |
[ 메타 문자 알아보기 ]
1) ' | ' (shift + \)
p = re.compile('name|minho')
m = p.match('nameminsu')
print(m)
----------------------------
<_sre.SRE_Match object; span=(0, 4), match='name'>
OR 의 뜻으로 자주 쓰이는 ' | ' 이다. compile 구문에서 'name' 또는 'minho' 가 있다면 매치에 성공한다.
2) ^
p = re.compile('^name')
m = p.search('name minsu')
n = p.search('minsu name')
print(m)
print(n)
--------------------------
<_sre.SRE_Match object; span=(0, 4), match='name'>
None
'^' 문자는 처음 시작되는 문자인지 알아보는 것으로 예시 m 에서는 name 이 첫 단어로 나왔으므로 정상적으로 반환되었고 두 번째 예시문 n 에서는 name 이 첫 단어가 아니기 때문에 None 값을 반환하였습니다.
3) $
p = re.compile('name$')
m = p.search('name minsu')
n = p.search('minsu name')
print(m)
print(n)
----------------------------
None
<_sre.SRE_Match object; span=(6, 10), match='name'>
'^' 문자와는 반대로 마지막 단어가 무엇인지 판단하는 문자입니다.
4) \b
p = re.compile(r'\bname\b')
m = p.search('myname minsu')
n = p.search('minsu name')
o = p.search('name is minsu')
print(m)
print(n)
print(o)
-------------------------------
None
<_sre.SRE_Match object; span=(6, 10), match='name'>
<_sre.SRE_Match object; span=(0, 4), match='name'>
\b는 공백을 의미합니다.
r 을 통해 raw string 이란 것을 선언해주고 첫 번째 예시 m 은 name앞에 공백이 없으므로 None 값 반환.
예시 n 은 name 앞 뒤로 공백이 있으므로 정상 값 반환. (끝에 아무것도 없어도 공백이라고 판단함을 알 수 있다.)
예시 o 는 name 앞 뒤로 공백이 있으므로 정상 값 반환. (처음에 아무것도 없어도 공백이라고 판단함을 알 수 있다.)
# 탐색
p = re.compile(".+@")
m = p.search("minsu@naver.com")
print(m)
--------------------------------
<_sre.SRE_Match object; span=(0, 6), match='minsu@'>
만약 이메일 형식이 있을 때 아이디(minsu) 만을 뽑아내고 싶을 때를 생각해보자.
@ 문자 이전은 아이디에 해당한다. 그래서 정규표현식으로 ".+@" 를 통해 아무문자든지 반복하고 @으로 끝나는 것을 정했지만 @까지 반환되었다. 이럴 때 사용하는 것이 전방 탐색이다.
p = re.compile(".+(?=@)")
m = p.search("minsu@naver.com")
print(m)
----------------------------------
<_sre.SRE_Match object; span=(0, 5), match='minsu'>
(?=) 를 사용하게 되면 조건에는 인식하지만 결과에 포함되지 않음을 알 수 있다.
p = re.compile(".*[@](?!hanmail).*$")
m = p.search("minsu@naver.com")
n = p.search("minsu@hanmail.com")
o = p.search("minsu@gmail.com")
print(m)
print(n)
print(o)
----------------------------------------
<_sre.SRE_Match object; span=(0, 15), match='minsu@naver.com'>
None
<_sre.SRE_Match object; span=(0, 15), match='minsu@gmail.com'>
이번 예시는 한메일을 제외하는 것이다.
(?!) 를 이용하여 hanmail 만을 걸러내었다. 어떠한 것을 제외하고 싶을 때 쓰인다.
# 문자열 교체하기
p = re.compile("minsu")
m = p.sub("chulsu", "my name is minsu")
print(m)
---------------------------------------
my name is chulsu
minsu 라는 단어를 chulsu 라는 단어로 교체할 때 sub 를 사용합니다.