파이썬 정규표현식(re) 사용법 - 03. OR, 반복
22 Jul 2018 | Regex re목차
파이썬 정규표현식(re) 사용법 - 01. Basic
파이썬 정규표현식(re) 사용법 - 02. 문자, 경계, flags
파이썬 정규표현식(re) 사용법 - 03. OR, 반복
파이썬 정규표현식(re) 사용법 - 04. 그룹, 캡처
파이썬 정규표현식(re) 사용법 - 05. 주석, 치환, 분리
파이썬 정규표현식(re) 사용법 - 06. 치환 함수, 양방탐색, 조건문
파이썬 정규표현식(re) 사용법 - 07. 예제(숫자)
파이썬 정규표현식(re) 사용법 - 08. 예제(단어, 행)
파이썬 정규표현식(re) 사용법 - 09. 기타 기능
이 글에서는 정규표현식 기초와 python library인 re
패키지 사용법에 대해서 설명한다.
본 글에서 정규표현식은 regex
와 같이, 일반 문자열은 ‘regex’와 같이 표시하도록 한다.
정규표현식의 기초: OR, 반복
|
: 다자택일
단어 ‘one’, ‘two’, ‘three’ 중 하나에 대응하고 싶다면 |
를 쓰면 된다(백슬래시 또는 원화로 되어 있는 \
키의 shift 버전이다).
matchObj = re.findall('one|two|three', 'one four two three zero')
print(matchObj)
결과
['one', 'two', 'three']
작동 과정을 살펴보자.
- 맨 앞에서 바로 ‘one’이 일치된다.
- 공백 한 개는
o
,t
어느 것에도 일치되지 않으므로 건너뛴다. ‘f’도 마찬가지이다. - ‘four’ 의 ‘o’에 도달했다.
o
는 일치되기 때문에, ‘u’에n
을 일치시켜본다. 물론 아니다. - 계속 넘어가서 ‘two’의 ‘t’에 도달했다. ‘t’는
t
에 일치된다. - ‘w’에서는
w
와h
중 일치되는 것을 찾는다. 현재tw
까지 일치되었다. - ‘o’까지 일치되어 ‘two`를 찾았다.
- 이와 비슷한 과정을 반복하여 ‘three’까지 찾고 종료한다.
일반적으로 |
로 나열한 단어들의 순서가 중요하지는 않다. 하지만 중요한 순간이 있다.
다음 예시를 보자.
matchObj = re.findall('one|oneself|onerous', 'oneself is the one thing.')
print(matchObj)
결과
['one', 'one']
‘oneself’가 있음에도 oneself
에 일치되지 않았다. 그 이유는 이미 ‘one’을 찾아버렸고, 정규식은 overlapping된 부분을 또 찾지 않기 때문에, ‘one’을 찾고 나서 남은 문자열은 ‘self is the one thing.’이다. 따라서 남은 문자열에서는 더 이상 oneself
를 찾을 수 없는 것이다.
이 문제의 해결 방법은 두 가지다. 물론 더 있을 수도 있다.
- 당연하게도 더 긴
oneself
를one
앞에다 두면 해결된다. - 아니면 단어 경계를 활용한다.
\bone\b|\boneself\b
로 쓰면 된다.
* : 0회 이상 반복
어떤 문자나 기호 뒤에 *(asterisk)를 붙이면 그 문자가 일치되는 만큼 일치된다. 예를 들어 a*
의 경우 ‘a’나 ‘aaa’ 혹은 ‘‘(빈 문자열)과도 일치된다.
예시를 보자.
print(re.match('a*', ''))
print(re.match('a*', 'a'))
print(re.search('a*', 'aaaa'))
print(re.fullmatch('a*', 'aaaaaa'))
print(re.findall('a*', 'aaabaaa aa '))
matchObj = re.search('<p>.*</p>', '<p> Lorem ipsum... is boring. </p>')
print(matchObj)
결과
<_sre.SRE_Match object; span=(0, 0), match=''>
<_sre.SRE_Match object; span=(0, 1), match='a'>
<_sre.SRE_Match object; span=(0, 4), match='aaaa'>
<_sre.SRE_Match object; span=(0, 6), match='aaaaaa'>
['aaa', '', 'aaa', '', 'aa', '', '', '']
<_sre.SRE_Match object; span=(0, 34), match='<p> Lorem ipsum... is boring. </p>'>
여섯 번째 결과의 경우, 파이썬 버전에 따라 None이 반환될 수도 있다.
그런데 한 가지 이상한 결과가 보인다. 다섯 번째 실행문이다.
print(re.findall('a*', 'aaabaaa aa '))
# ['aaa', '', 'aaa', '', 'aa', '', '', '']
빈 문자열이 이상하리만큼 많이 매칭되었다. 굉장히 비직관적인 결과이지만, 빈 문자열에도 일치된다는 것을 생각했을 때 아예 틀린 것은 분명히 아니다.
매칭되는 빈 문자열들은 a가 아닌 다른 문자들과의 경계에서 발생한다고 생각하면 될 듯하다. 하지만, 아마 대부분 이것은 원하는 결과가 아닐 것이기 때문에, ‘a’ 덩어리를 찾고 싶다면 다음 메타문자를 보자.
+ : 1회 이상 반복
*
과 비슷하지만 무조건 한 번이라도 등장해야 한다. 위와 거의 같은 예시를 보자.
print(re.match('a+', ''))
print(re.match('a+', 'a'))
print(re.search('a+', 'aaaa'))
print(re.fullmatch('a+', 'aaaaaa'))
print(re.findall('a+', 'aaabaaa aa '))
matchObj = re.search('<p>.+</p>', '<p> Lorem ipsum... is boring. </p>')
print(matchObj)
결과
None
<_sre.SRE_Match object; span=(0, 1), match='a'>
<_sre.SRE_Match object; span=(0, 4), match='aaaa'>
<_sre.SRE_Match object; span=(0, 6), match='aaaaaa'>
['aaa', 'aaa', 'aa']
<_sre.SRE_Match object; span=(0, 34), match='<p> Lorem ipsum... is boring. </p>'>
아마 이것이 여러분이 원하는 ‘a’ 덩어리를 찾은 결과일 것이다.
빈 문자열이 일치되지 않은 것을 기억하자.
{n, m} : 지정 횟수만큼 반복
중괄호는 지정한 횟수만큼 정규식을 반복시키는 것이다. 이 쓰임으로 중괄호를 쓸 때 쓰는 방법은 세 가지가 있다.
- {n} : 정확히 n회만큼 반복
- {n, m} : n회 이상 m회 이하 반복
- {n, } : n회 이상 반복. 무한히 일치될 수 있다.
물론 n은 자연수, m은 n보다 큰 정수이다. 그리 어렵지 않으므로 바로 예시를 보자.
print(re.search('a{3}', 'aaaaa'))
print(re.findall('a{3}', 'aaaaaaaa'))
print(re.findall('a{2,4}', 'a aa aaa aaaa aaaaa'))
print(re.findall('a{2,}', 'a aa aaa aaaa aaaaa'))
결과
<_sre.SRE_Match object; span=(0, 3), match='aaa'>
['aaa', 'aaa']
['aa', 'aaa', 'aaaa', 'aaaa']
['aa', 'aaa', 'aaaa', 'aaaaa']
예상과는 조금 다른 결과일지도 모르겠다. 오직 ‘aaa’만을 찾고 싶을 때 a{3}
처럼 쓰면 ‘aaaaa’의 일부분인 ‘aaa’에도 일치될 수 있다. 따라서 정확히 ‘aaa’만을 찾으려면 \baaa\b
처럼 단어 경계를 활용하는 쪽이 좋다.
참고로 {0, }
은 *
과 같고, {1,}
은 +
와 같다.
? : 0회 또는 1회 반복
이 메타문자도 어렵지는 않을 것이라 생각된다. ?
는 {0,1}
과 같다.
print(re.findall('ab?a', 'aa aba aaaa'))
결과
['aa', 'aba', 'aa', 'aa']
정규표현식은 항상 최대한 많은 부분을 일치시키려 한다는 것을 기억하자.
참고로, 앞에서 말한 반복 메타문자들(*
, +
, {n, m}
, ?
등)을 정량자 또는 수량자라고 부른다.
Advanced: 탐욕 정량자 vs 나태 정량자
그리고 이런 정량자(수량자)들은 한 가지 중요한 특성이 있다.
일단 전체 문자열이 매치가 되도록 노력하고, 그 선을 지키는 선에서 일치되는 부분에는 최대한 많이 일치시키려고 한다. 즉 기본적으로 모든 정량자들은 탐욕적이며, 가능한 많은 문자열에 매치되려고 한다.
말이 복잡한데, 예시를 보면서 천천히 설명하도록 하겠다.
# 1번 예시
matchObj = re.search('<p>.*</p>', '<p> Lorem ipsum... is boring. </p>')
print(matchObj.group())
print('# ---------------------------------------------------------------- #')
# 2번 예시
matchObj = re.search('<p>.*</p>', '''
<p> part 1 </p> part 2 </p>
<p> part 3 </p> part 4 </p>
''', re.DOTALL)
print(matchObj.group())
print('# ---------------------------------------------------------------- #')
# 3번 예시
matchObj = re.search('<p>.*?</p>', '''
<p> part 1 </p> part 2 </p>
<p> part 3 </p> part 4 </p>
''', re.DOTALL)
print(matchObj.group())
결과
<p> Lorem ipsum... is boring. </p>
# ---------------------------------------------------------------- #
<p> part 1 </p> part 2 </p>
<p> part 3 </p> part 4 </p>
# ---------------------------------------------------------------- #
<p> part 1 </p>
전체 문자열이 매치가 되도록 노력한다.
- 여러분은 조금 위에서 Lorem ipsum 예시를 보았을 것이다. 바로 위의 1번 예시는 이를 변형한 것이다.
사실 마침표.
는 모든 문자에 일치되기 때문에, ‘</p>‘에 해당하는 부분도 마침표에 일치될 수 있다. 만약에 이 부분까지.
에 일치시켜 버린다면,.*
부분이 ‘<p>’ 뒤쪽의 모든 문자를 집어삼켜 버리고, 따라서 정규식의 남은 패턴인</p>
부분은 대조해볼 문자열이 남아있지 않으므로 실패해야 한다고 생각할 수 있다. - 그러나, 정규식의 정량자들은 역행(backtracking)을 할 줄 안다. 이 말은,
*
나+
등은 탐욕적이기는 하지만, 전체 문자열에 일치되는 가능성마저 없애버리지는 않는다는 말과 갈다.- 우선
.*
가 모든 문자열을 집어삼켜 ‘</p>‘까지 해치웠다. 그러나, 정규식 패턴에는</p>
가 남아있기 때문에,.*
는 자신이 집어삼킨 문자열을 하나 뱉어내고, 남은 정규식 패턴</p>
에 대조해보라고 한다. - 마지막 문자 하나인 ‘>‘는 매치되지 않기 때문에,
.*
는 문자를 하나 더 뱉어낸다. 이제 ‘p>‘와 남은 정규식 패턴</p>
를 비교해보라고 시킨다. - 역시 일치되지 않으므로, 이와 같은 과정을 정규식 패턴과 뱉어낸 문자열이 일치될 때까지 혹은 모든 문자를 뱉어낼 때까지 반복하게 된다.
- Lorem ipsum 예시의 경우 4개의 문자를 뱉어내면 일치된다. 따라서 모든 문자열이 정규식 패턴과 일치되고, 전체 문자열이 결과로 반환된다.
.*
가 먹어치웠던 문자열을 살펴보면 그 경계가 끝까지 갔다가 반대 방향으로 후퇴하는 것처럼 보인다. 그래서 이름이 역행이다.
- 우선
- 이는 2번 예시를 보아도 알 수 있다.
.*
가 최대로 일치시키려고 하기 때문에, ‘part 1’이나 ‘part 2’까지 일치되는 것이 아닌 최대로 일치되는 부분인 ‘part 4’까지 일치시키는 것을 볼 수 있다.
3번 예시는 나태 정량자를 보여준다. 나태 정량자는 별다른 것은 없고, 단지 정량자 바로 뒤에 ?
를 붙여주기만 하면 된다. 그러면 탐욕적 정량자였던 *
는 최대로 일치시키는 대신 문자열은 가장 적게 먹어치우면서 일치되도록 하는 방법을 찾는다. 그래서 딱 ‘part 1’까지만 일치되고, 나머지 문자열은 버려진다.
+?
,{3, 5}?
,??
등도 가능하다.- 사실 나태 정량자도 역행을 한다. 그러나 역행이 꼭 뒤로 가는 것을 의미하는 것이 아닌, 각 정량자가 선호하는 방향과 반대 방향으로 갈 때 역행이라고 한다. 따라서 나태 정량자는 우선 최소로 일치하는 부분을 찾은 뒤(빈 문자열), 문자열이 일치될 때까지 역행(문자열 방향으로는 뒤쪽)한다.
그래서 탐욕 정량자와 나태 정량자의 차이는, 유력 대조부를 제일 긴 것을 우선적으로 찾느냐, 제일 짧은 것을 우선적으로 찾느냐의 차이이다.
그리고 결과적으로 탐욕 정량자와 나태 정량자의 일치부가 같아지는 때도 있다. 다만 이때는 검색 순서만이 다를 뿐이다.
역행에 관해서는 나중에 조금 더 자세히 다루도록 하겠다.
응용 문제
문제 1: 1~8자리 10진수에 일치하는 정규표현식을 작성하라.
문제 1 정답보기
r'\b\d{1,8}\b'
문제 2: 4자리 또는 8자리 16진수에 일치하는 정규표현식을 작성하라. 16진수는 0~9, a~f를 사용한다. 예시는 abcd1992, 7fffffff, 2dfa9a00이다. 윈도우 오류에서 ‘0xC1900101’ 비슷한 에러를 많이 봤을 것이다.
문제 2 정답보기
r'\b[0-9a-f]{4}\b|\b[0-9a-f]{8}\b'
문제 3: 1.2이나 3.72e3, 1.002e-12 같은 수를 부동소수점 수 또는 과학적 표기법으로 표기한 수라고 한다. 이와 같은 수에 일치하는 정규표현식을 작성하라.
문제 3 정답보기
r'\b\d*\.\d+(e-?\d+)?'
파이썬 버전 3.6 기준으로, \b
를 쓰려면 r prefix를 붙여 주어야 한다고 했었다.
문제 3의 정답에 아직 설명하지 않은 소괄호 ( )
가 있다. 이는 다음 글에서 설명한다.