코린이의 소소한 공부노트

정규표현식 본문

Python

정규표현식

무지맘 2021. 7. 23. 15:22

네이버나 구글 같은 검색 엔진에서 단어를 검색할 때 대부분 원하는 단어나 문장을 정확히 입력한다.

그런데 왜 한 번씩 그럴 때 있잖아요, c로 시작해서 t로 끝나는 단어인데 중간이 생각이 안 나서 검색하기 힘들 때..

이런 경우를 위해 검색 연산자를 지원한다. 예를 들어 cat인데 가운데 a가 생각이 안나면 c*t로 검색하면 * 자리에 들어갈 수 있는 모든 경우가 검색이 된다. 중간에 들어가는 것이 2글자라면 c**t로 검색하면 된다.

파이썬에도 비슷한것 같다고 혼자서 우기는 기능이 있다. 정규표현식이라는 것이다. 이번 글에서는

1. 정규표현식이 무엇인지

2. 문자열 패턴은 어떤 식으로 나타내는지

3. 반복되는 패턴은 어떻게 쓰면 되는지

4. 문자열 패턴을 어떤 함수들과 사용 가능한지 알아보도록 하겠다.


1. 정규표현식 regular expression과 서치 메소드 search method


정규표현식은

1) 복잡한 문자열을 특정 패턴으로 표현하는 방법이다.

2) 내가 찾고자 하는 패턴과 일치하는 문자열을 검색, 치환, 제거하는 기능을 지원한다.

3) 안쓰면 작업이 불완전해지거나 작업의 cost가 높아진다.

 예시) 이메일 형식 판별, 전화번호 형식 판별 등

정규표현식에서 중요한 것은 내가 원하는 패턴을 어떻게 표현하느냐는 것이다.

 

가장 기본적으로 생각할 수 있는 것은 원하는 패턴을 그대로 입력하는 것이다.

어떤 고급진(?) 표현식으로 쓴 게 아니라 날것 그대로의 문자열이라는 뜻으로 로우 스트링 raw string이라고 한다.

로우 스트링은

1) 문자열 앞에 알파벳 r을 붙인 것이다.

2) 이스케이프 문자도 제 기능 대신 본래 문자 그대로 나온다.

a = 'abc\n'
print(a)
>> abc
		# 이 줄은 \n의 결과로 나온 것
b = r'abc\n'
print(b)
>> abc\n    # \n이 제 기능을 잃고 본래 문자열로 출력됨

 

로우 스트링을 이용해서 내가 원하는 패턴이 문자열에 있는지 서치 메소드로 확인해볼 수 있다.

서치 메소드를 이용해서 '아빠도 펭하! 엄마도 펭하!' 문자열에서 '펭하'라는 패턴을 찾아보았다.

import re # 정규표현식 모듈
re.search(r'펭하','아빠도 펭하! 엄마도 펭하!')
>> <re.Match object; span=(4, 6), match='펭하'>

결과를 분석해보면

- re.Match object: 서치 메소드의 결과로 Match라는 객체가 반환되었다. 패턴을 찾지 못하면 None을 반환한다.

- span=(4, 6): 찾고 있는 패턴은 (4, 6)에 있다. (따지고 보면 4번째 이상 6번째 미만이므로 [4, 6)이 맞는 표현이다.)

- match='펭하': 찾고 있던 패턴이다. 뒤에 펭하가 또 나오지만, 가장 첫 번째로 찾은 패턴만 매칭 시켜준다.

 

지금부터 직접 적는 패턴 말고, 조금 더 고급지게(?) 패턴을 입력해서 찾아보는 일을 해보자!


2. 기본 패턴


기본 패턴은 문자 1개에 대한 패턴을 나타내는 것으로,

1) a, B, 1 등의 문자들은 정확히 해당 문자와 일치한다.

 - 기본적으로 대소문자는 구별하나, 구별하지 않도록 설정할 수도 있다.

2) 특별한 의미로 사용되는 문자들도 있다. 이것들을 메타 문자라고 한다. 대괄호 [ ] 안에 또는 문자열 ' '안에 패턴을 입력해서 사용한다.

 ㄱ) [ ] 대괄호: 문자 1개의 패턴을 나타낼 때 사용한다.

re.search(r'[bce]at','battle') # bat 또는 cat 또는 eat을 찾아라
>> <re.Match object; span=(0, 3), match='bat'>

# - 하이픈을 이용해서 문자의 범위를 설정할 수 있다. 
re.search(r'[A-Z]4', 'A1B2C3D4E5') # 4 앞에 대문자 1개가 있는 패턴을 찾아라
>> <re.Match object; span=(6, 8), match='D4'>

# [ ] 내부의 메타문자는 그 문자 자체를 의미한다.
# . 마침표는 메타문자이다. (바로 아래에서 설명)
re.search(r'3[ab.]14', '파이는 약 3.14') # 3a14 또는 3b14 또는 3.14를 찾아라
>> <re.Match object; span=(6, 10), match='3.14'>

 ㄴ) . 마침표: \n을 제외한 어떤 문자 1개를 나타낸다.

re.search(r'.and','what a wonderful land') # and 앞에 문자 1개가 더 있는 패턴을 찾아라.
>> <re.Match object; span=(17, 21), match='land'>

 ㄷ) \ 백슬래시 또는 원화: 메타문자 앞에 붙여 문자 자체를 나타낸다.

re.search(r'\.and','what a wonderful land') # and 앞에 마침표가 있는 패턴을 찾아라.
>>                     # 그런 패턴은 없으므로 None을 반환(결과 자체가 나타나지 않음)

 ㄹ) \w: 알파벳 또는 숫자 1개를 나타낸다. = [a-zA-Z0-9_]

re.search(r'\w00', 'I have 100 bags.')
>> <re.Match object; span=(7, 10), match='100'>

 ㅁ) \s: 공백문자 1개를 나타낸다.

re.search(r'\s100', 'I have 100 bags.')
>> <re.Match object; span=(6, 10), match=' 100'>

 ㅂ) \d: 숫자 1개를 나타낸다. = [0-9]

re.search(r'\d\d', 'I have 100 bags.')
>> <re.Match object; span=(7, 9), match='10'>
# 10, 00 둘 다 가능한 패턴이지만 제일 먼저 찾은 10을 반환한다.

 ㅅ) ^: 패턴의 앞에 붙여서 해당 패턴의 여집합을 나타낸다.

re.search(r'[^a-d]at', 'bat cat eat fat hat') # aat, bat, cat, dat 말고!
>> <re.Match object; span=(8, 11), match='eat'>

 ㅇ) \W: 알파벳, 숫자가 아닌 문자 1개를 나타낸다. = [^a-zA-Z0-9_]

re.search(r'\W\w', 'pi = 3.14') # (알파벳, 숫자 아닌 문자 1개) + (알파벳, 숫자 1개)
>> <re.Match object; span=(4, 6), match=' 3'>

 ㅈ) \S: 공백이 아닌 문자 1개를 나타낸다.

re.search(r'\S\s', 'pi = 3.14') # 공백 아닌 문자 1개 + 공백 문자 1개
>> <re.Match object; span=(1, 3), match='i '>

 ㅊ) \D: 숫자가 아닌 문자 1개를 나타난다. = [^0-9]

re.search(r'\D\d\d', 'pi = 3.14') # 숫자 아닌 문자 1개 + 숫자 2개
>> <re.Match object; span=(6, 9), match='.14'>

3. 반복 패턴


반복 패턴은 문자 1개에 대한 패턴(대괄호[ ]로 묶은 패턴 포함)을 반복 사용하여 패턴을 나타내고 싶을 때 쓰는 방법이다.

1) +: 패턴 뒤에 붙여 해당 패턴이 1번 이상 발생하는 경우를 찾고 싶을 때 사용한다.

re.search(r'b\w+a','banana') # b + 알파벳 또는 숫자 1개 이상 + a
>> <re.Match object; span=(0, 6), match='banana'>
# bana도 되지만 가장 긴 banana를 반환한다. -> 반복패턴은 greedy하게(maximal) 매칭

2) *: 패턴 뒤에 붙여 해당 패턴이 0번 이상 발생하는 경우를 찾고 싶을 때 사용한다.

re.search(r'pi+g','pg') # p + i 1번 이상 + g
>>                      # 패턴 매칭 실패 -> None 반환

re.search(r'pi*g','pg') # p + i 0번 이상 + g
>> <re.Match object; span=(0, 2), match='pg'>

3) ?: 패턴 뒤에 붙여 해당 패턴이 0번 또는 1번 발생하는 경우를 찾고 싶을 때 사용한다.

re.search(r'dolls?','I need giant Muzi&Pengsoo dolls.') # doll 또는 dolls를 찾아라
>> <re.Match object; span=(26, 31), match='dolls'>

 

패턴 매칭을 쓰다 보면 주어진 문자열의 맨 앞부터 또는 맨 뒤부터 매칭 되는 경우를 찾고 싶을 때가 있다. 그럴 때 쓰는 메타문자도 있다.

1) ^: 패턴 앞에 붙여 해당 문자로 시작하는 패턴을 찾고 싶을 때 사용한다.

2) $: 패턴 뒤에 붙여 해당 문자로 끝나는 패턴을 찾고 싶을 때 사용한다.

구체적인 예시를 보면 이해하는 데 도움이 될 것이다.

re.search(r'v\w+a','havana')  # v로 시작해서 a로 끝나는 패턴
>> <re.Match object; span=(2, 6), match='vana'> # v가 주어진 문자열 중간에 와도 상관 없다

re.search(r'^v\w+a','havana') # 주어진 문자열이 v가 아닌 h로 시작하기 때문에
>>                            # 매칭되는 패턴이 없다

re.search(r'v\w+a$','havana') # 주어진 문자열이 a로 끝나기 때문에
>> <re.Match object; span=(2, 6), match='vana'> # 매칭되는 패턴이 있다

 

찾고자 하는 패턴에 여러 반복 패턴이 들어간다면, 그루핑 grouping을 이용해서 패턴을 그룹별로 관리할 수 있다.

# 주어진 문자열에서 메일 주소를 찾아보자!

m = re.search(r'(\w+)@(.+)','메일 주소: test@mail.com') # 패턴을 작성한 후 ()로 그루핑을 해준다.
# 첫 번째 그룹: 알파벳, 숫자가 1번 이상 나오는 패턴
# 두 번째 그룹: 어떤 문자가 1번 이상 나오는 패턴(메일 주소에 .도 있기 때문에 \w을 사용하지 않음)

print(m.group()) # 찾은 패턴
>> test@mail.com

print(m.group(1)) # 첫 번째 그룹
>> test

print(m.group(2)) # 두 번째 그룹
>> mail.com

print(m.group(0)) # m.group()과 같음
>> test@mail.com

 

반복 패턴을 이용할 때, 반복되는 횟수를 정해놓고 싶으면 중괄호 { }를 이용하면 된다.

# 직접 반복 횟수를 정할 수 있다.
re.search('pi{2}g','piigpiiiiig') # piig를 찾아라
>> <re.Match object; span=(0, 4), match='piig'>

# 반복 횟수의 범위를 설정할 수도 있다.
re.search('ca{3,5}t','cat caat caaat') # c + a 3~5번 반복 + t
>> <re.Match object; span=(9, 14), match='caaat'>

 

지금까지 설명한 것 외에 여러 메타문자가 있지만, 3가지만 추가로 설명하겠다.

- |(shift \): 또는(OR) 연산

re.search(r'[bc]at','battle') # bat 또는 cat을 찾아라
>> <re.Match object; span=(0, 3), match='bat'>

# 위의 문장을 |을 이용해서 나타내면
re.search(r'bat|cat','battle')
>> <re.Match object; span=(0, 3), match='bat'>

- \t, \n: tab, new line

s = '''apple\tbanana
coconut\tdewberry'''
print(s)
>> apple	banana
   coconut	dewberry
   
re.search(r't\t',s) # 코코넛 다음 탭
>> <re.Match object; span=(19, 21), match='t\t'>

re.search(r'na\n',s) # 바나나 다음 줄바꿈
>> <re.Match object; span=(10, 13), match='na\n'>

# 예시로 보여주려고 억지로 만들었더니 이상하군
# 잘 안쓰게 될거같은 느낌이 들쥬?

4. 정규표현식과 관련된 여러 기능들


1) 그리디 매칭 greedy(maximal) matching

위에서 잠깐 언급했지만, 패턴 매칭은 기본적으로 greedy하게 된다. 원래 greedy의 뜻은 '욕심이 많은'이지만, 여기서는 욕심껏 매칭 한다는 뜻으로 가능한 가장 긴 패턴을 매칭 시켜준다는 말로 쓰고 있다.

re.search(r'a[bc]*b','abcbcccb') # a + b 또는 c 0번 이상 + b
>> <re.Match object; span=(0, 8), match='abcbcccb'>

가능한 매치는 'ab', 'abcb', 'abcbcccb' 이렇게 3개지만 가장 긴 것을 반환했다.

여기서 주의할 것은 주어진 문자열 앞에서부터 봤을 때 원하는 패턴이 매칭 되는 그 시작점부터 가장 긴 패턴을 매칭 시켜준다는 것이다. 위의 예시는 시작점이 다 똑같으니까 크게 신경 쓸 것이 없었지만 아래 예시는 신경을 써야 한다. 이게 무슨 뜻인가 하면..

re.search(r'co+l','coolcooool') # c + o 1번 이상 + l
>> <re.Match object; span=(0, 4), match='cool'>

문장을 봤을 때 주어진 조건에 알맞은 패턴은 0번째부터 시작하는 'cool'와 4번째부터 시작하는 'cooool'이 있다. greedy하게 매칭 한다면 'cooool'을 반환해야 할 것 같지만, 'cool'를 반환했다. 맨 앞부터 알맞은 패턴을 찾을 때 처음 찾은 패턴은 0번째에 위치한 것이기 때문에 0번째부터 주어진 조건을 만족하는 가장 긴 패턴인 'cool'를 반환한 것이다.

 

2) 미니멈 매칭 minimum(non-greedy) matching

하다 보면 가장 짧은 패턴을 찾고 싶을 때도 있지 않을까? 그럴 때는 반복 패턴 메타문자 뒤에 ?를 붙여주면 된다.

# ?를 안 붙이면 < + 문자 1번 이상 + > 중 가장 긴 것 매칭
re.search(r'<.+>','<html>haha</html>')
>> <re.Match object; span=(0, 17), match='<html>haha</html>'>

# ?을 붙이면 < + 문자 1번 이상 + > 중 가장 짧은 것 매칭
re.search(r'<.+?>','<html>haha</html>')
>> <re.Match object; span=(0, 6), match='<html>'>

위에서 본 반복 횟수 지정해주는 중괄호 { }에서도 미니멈 매칭을 쓸 수 있다.

# ?를 안 붙이면 가장 긴 패턴 매칭
re.search('ca{1,3}t','cat caat caaat') # c + a 1~3번 반복 + t
>> <re.Match object; span=(9, 14), match='caaat'>

# ?를 붙이면 가장 짧은 패턴 매칭
re.search('ca{1,3}?t','cat caat caaat') # c + a 1~3번 반복 + t
>> <re.Match object; span=(0, 3), match='cat'>

 

3) search method

이미 1. 정규표현식 regular expression과 서치 메소드 search method에서 설명했고, 많은 예시를 들었기 때문에 설명은 생략하겠다!

 

4) match method

search와 비슷하지만, match는 주어진 문자열의 맨 앞부터 매칭 되어야 한다는 게 가장 큰 차이점이다.

# 1. 숫자 3개 패턴 search
re.search(r'\d\d\d','pi = 3.141592...')
>> <re.Match object; span=(7, 10), match='141'>

# 2. 숫자 3개 패턴 match
re.match(r'\d\d\d','pi = 3.141592...')
>>                       # 주어진 문자열이 p로 시작하기 때문에 None 반환

# 3. ^를 이용해 2번을 search로 구현한 것
re.search(r'^\d\d\d','pi = 3.141592...')
>>                       # 주어진 문자열이 p로 시작하기 때문에 None 반환

 

5) findall method

search는 맨 처음 매칭 되는 위치에서 시작해서 가장 긴 패턴 1개만 반환했다면, findall은 맨 처음 위치 외에도 패턴이 겹치지 않는 선에서 매칭되는 결과를 리스트 형태로 반환해준다.

# search를 쓰면 가장 긴 패턴 1개만 반환한다.
re.search(r'a[abc]*b','abcbaccbcb')
>> <re.Match object; span=(0, 10), match='abcbaccbcb'>

# findall을 쓰면 가능한 패턴이 리스트로 반환된다.
re.findall(r'a[bc]*b','abcbaccbcb')
>> ['abcb', 'accbcb']
# 가장 긴 'abcbaccbcb'는 앞에서부터 찾은 'abcb'와 겹치기 때문에 포함되지 않았다.

0번째 a부터는 가능한 패턴은 'abcb' 하나뿐이고, 4번째 a부터 가능한 패턴은 'abbc'와 'accbcb' 2개이다. 그런데 왜 결과 리스트에는 'abcb'가 없을까? 그 이유는 여전히 greedy하기 때문이다. 패턴 시작점 1개당 가장 긴 패턴 1개만 채택해서 결과 리스트에 포함시킨다. (이래서 1등을 해야 하는 것인가...)

 

6) sub method

sub은 주어진 문자열에서 원하는 패턴과 일치하는 모든 패턴을 교체(replace)하고 그 문자열을 반환한다.

# 3.14를 pi로 바꾸기
re.sub(r'3\.14','pi','2.7 3.14 math 2.7 3.14 math')
>> '2.7 pi math 2.7 pi math'

# count = 0 또는 명시하지 않음: 모든 패턴을 교체
# count = a: 매칭되는 패턴 중 a개만 교체
re.sub(r'3\.14','pi','2.7 3.14 math 2.7 3.14 math', count=1)
>> '2.7 pi math 2.7 3.14 math'  # count = 1이므로 1번만 교체

sub 메소드의 2번째 인자에는 바꾸고자 하는 문자열 대신 함수가 들어갈 수도 있다. 예시를 만들어보려고 했는데 아직 공부가 부족한듯하다.. 나중에 기회가 되면 이해하기 쉬운 예제를 만들어보겠다.

 

7) compile method

동일한 정규표현식을 계속 쓰는 번거로움을 해결해 줄 수 있는 메소드이다. 표현식을 re.RegexObject 객체로 저장한다.

email_reg = re.compile(r'[\w-]+@[\w.]+') # 메일주소 패턴을 저장
email_reg.search('test@gmail.com haha test2@gmail.com nice test test') # 패턴 인자 안 써도 된다
>> <re.Match object; span=(0, 14), match='test@gmail.com'>

<요약>

 

1. 정규표현식: 패턴 표현, 검색, 치환, 제거 기능

2. 패턴: 알파벳, 숫자, 특수문자, 메타문자로 표현

 - [ ]: 문자 1개의 범위

 - *, +, ?, { }: 패턴 반복 설정

3. 여러 가지 기능

 - 기본적으로 greedy matching

 - 검색, 모두 검색, 교체, 패턴 저장 등