[정규 표현식] 메타 문자
지난 포스트에 이어서 정규 표현식의 기본이라고 할 수 있는 메타 문자에 대해 알아보겠습니다. 메타 문자를 잘 다루게 되면 특정한 문자열들을 처리할 때 상당한 도움이 됩니다!
메타 문자
메타 문자(Regular Expression)란, 특별한 용도로서 사용하는 문자를 의미합니다. 아래는 가장 기본적인 메타 문자들입니다. 지난 포스트에서 배운 re 패키지의 함수들을 이용하여 각 문자들이 어떠한 역할을 하는지 알아보도록 하겠습니다.
. ^ $ * + ? { } [ ] \ | ( )
- 문자 클래스 [ ] : [ ] 사이의 문자들과 매치
예를 들어, “[x]”의 의미는 문자 x와 매치를 한다는 뜻이고, “[xyz]”은 문자 x, y, z 중 하나와 매치를 한다는 뜻이 됩니다. “python”은 “[xyz]” 중 “y”와 일치하므로 1번째에서 매치됩니다. “proxy”는 “x”와 “y” 둘 다 매치되지만, “[xyz]”의 의미에 따라 가장 먼저 매치되는 “x”에 대해서만 3번째에서 매치됩니다. 마지막으로 “algorithm”은 “[xyz]” 모두 포함하고 있지 않으므로 매치되지 않아 None 값이 도출됩니다.
import re
c = re.compile('[xyz]')
print(c.search("python"))
print(c.search("proxy"))
print(c.search("algorithm"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(1, 2), match='y'>
<re.Match object; span=(3, 4), match='x'>
None
모든 소문자 알파벳, 모든 숫자와 같이 특정 문자열을 한꺼번에 매치되게 하는 것도 가능합니다. 하이픈(-)은 두 문자 사이의 범위를 의미합니다. 즉, “[a-z]”는 소문자 알파벳, [A-Z]는 대문자 알파벳, [0-9]는 숫자, [가-힣]은 한글을 의미합니다. “123 안녕”은 “[가-힣]”에서 숫자를 제외한 “안녕”과 매치되므로 4번째에서 매치됩니다. 반면에 “123 hello”는 한글이 존재하지 않으므로 None 값이 도출됩니다.
import re
c = re.compile('[가-힣]')
print(c.search("123 안녕"))
print(c.search("123 hello"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(4, 5), match='안'>
None
또한, [ ] 안에 ^ 문자가 있으면 [ ]의 문자를 제외한 나머지 문자를 의미합니다. 즉, “[^가-힣]”은 한글을 제외한 문자를 의미하므로 “안녕”은 매치되지 않아 None, “hello”는 알파벳이므로 0번째에서 바로 매치됩니다.
import re
c = re.compile('[^가-힣]')
print(c.search("안녕"))
print(c.search("hello"))
--------------------------------------------------------------------------------------------------------------------------------
None
<re.Match object; span=(0, 1), match='h'>
이 외에도 문자 클래스와 관련된 메타 문자 중 \ 기호를 이용한 문자들이 있습니다. 소문자로 된 메타 문자는 포함을 의미하고 대문자로 된 메타 문자는 소문자와 정반대의 의미를 가지게 됩니다. 주로 제외를 의미함을 알 수 있습니다. 아래에 정의와 예시를 설명드립니다.
- “\d” : 숫자와 매치, [0-9]와 동일
- “\D” : 숫자를 제외한 문자와 매치, [^0-9]와 동일
- whitespace 문자 : 공백을 의미하는 문자
- " " : 공백
- “\t” : 탭
- “\n” : 줄바꿈, 개행 문자
- “\r” : 캐리지리턴(복귀), 커서의 위치를 현재 줄의 맨 앞으로 이동시킴
- “\f” : 페이지 나누기(폼 피드), 커서를 다음 줄의 현 column으로 이동
- “\v” : 수직 탭(Vertical tab)
-
“\s” : whitespace 문자와 매치, [ \t\n\r\f\v]와 동일
-
“\S” : whitespace 문자를 제외한 문자와 매치, [^ \t\n\r\f\v]와 동일
-
“\w” : 문자, 숫자, 언더바(_)와 매치
- “\W” : 문자, 숫자, 언더바(_)가 아닌 문자와 매치 (공백 또는 특수문자)
import re
a = re.compile('[\d]') # 1. "\d"
print("a [\d] :", a.search("369game"))
b = re.compile('[\D]') # 2. "\D"
print("b [\D] :", b.search("369game"))
c = re.compile('[\s]') # 4. "\s"
print("c [\s] :", c.search("I love tab "))
d = re.compile('[\S]') # 5. "\S"
print("d [\S] :", d.search("I love tab "))
e = re.compile('[\w]') # 6. "\w"
print("e [\w] :", e.search("_언더바."))
f = re.compile('[\W]') # 7. "\w"
print("f [\W] :", f.search("_언더바."))
--------------------------------------------------------------------------------------------------------------------------------
a [\d] : <re.Match object; span=(0, 1), match='3'>
b [\D] : <re.Match object; span=(3, 4), match='g'>
c [\s] : <re.Match object; span=(1, 2), match=' '>
d [\S] : <re.Match object; span=(0, 1), match='I'>
e [\w] : <re.Match object; span=(0, 1), match='_'>
f [\W] : <re.Match object; span=(4, 5), match='.'>
- 온점 (.) : \n을 제외한 모든 문자와 매치
“역.역” 문자열을 보겠습니다. 온점은 줄바꿈 문자를 제외한 모든 문자와 매치되므로 “역곡역”, “역삼역” 그리고 띄어쓰기인 ‘역 역’도 매치됩니다. 그러나 “역”과 “역” 사이에 문자가 존재하지 않는 “역역”은 매치되지 않습니다.
import re
c = re.compile('역.역')
print(c.search("역곡역"))
print(c.search("역삼역"))
print(c.search("역 역"))
print(c.search("역역"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 3), match='역곡역'>
<re.Match object; span=(0, 3), match='역삼역'>
<re.Match object; span=(0, 3), match='역 역'>
None
그러나 “역[.]역”은 “역.역”만 매치되고 “역삼역”은 매치되지 않은 것을 볼 수 있습니다. 이는 문자 클래스인 [ ] 안에 온점이 있으면 모든 문자라는 의미가 아닌 문자 그대로 온점(.)을 의미하기 때문입니다.
import re
c = re.compile('역[.]역')
print(c.search("역.역"))
print(c.search("역삼역"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 3), match='역.역'>
None
참고로 뒤의 포스트에서 다룰 예정이지만, re.DOTALL 옵션을 주면 줄바꿈 문자인 \n 문자와도 매치됩니다.
- 반복 (*) : * 바로 앞에 있는 문자가 0 번부터 무한 번까지 반복되는 문자열을 매치
정규표현식 “bo*k”을 보았을 때, “o”가 없어도 되고, 무수히 반복되어도 매치가 됩니다. 따라서 “bk”, “bok”, “book” 모두 매치가 되는 것을 볼 수 있습니다.
import re
c = re.compile('bo*k')
print(c.search("bk"))
print(c.search("bok"))
print(c.search("book"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 2), match='bk'>
<re.Match object; span=(0, 3), match='bok'>
<re.Match object; span=(0, 4), match='book'>
- 반복 (+) : * 바로 앞에 있는 문자가 1 번부터 무한 번까지 반복되는 문자열을 매치
정규표현식 “bo+k”는 “o”가 1개 이상 꼭 있어야 하므로 “bk”는 매치되지 않는 것을 볼 수 있습니다.
import re
c = re.compile('bo+k')
print(c.search("bk"))
print(c.search("bok"))
print(c.search("book"))
--------------------------------------------------------------------------------------------------------------------------------
None
<re.Match object; span=(0, 3), match='bok'>
<re.Match object; span=(0, 4), match='book'>
- 반복 ({m,n}) : {m,n} 바로 앞에 있는 문자가 m 번부터 n 번까지 반복되는 문자열을 매치
정규표현식 ‘bo{2,4}k’을 보겠습니다. “o”가 2번부터 4번까지 반복되는 문자열과 매치되므로, “book”과 “booook”만 매치되는 것을 볼 수 있습니다. 참고로 {m, n}과 같이 띄어쓰기가 있으면 메타 문자로 인식되지 않습니다. 따라서 띄어쓰기 없이 {m,n}로 사용해야 합니다.
import re
c = re.compile('bo{2,4}k')
print(c.search("bok"))
print(c.search("book"))
print(c.search("booook"))
print(c.search("booooook"))
--------------------------------------------------------------------------------------------------------------------------------
None
<re.Match object; span=(0, 4), match='book'>
<re.Match object; span=(0, 6), match='booook'>
None
만약 “{2,}”나 “{,4}”와 같이 한 쪽의 숫자가 없을 때는 각각 2번 이상, 4번 이하 반복의 의미를 가지게 됩니다. 이로 보았을 때, “{0, }”은 “*”, {1,}”은 “+”와 동일한 것을 알 수 있습니다.
import re
a = re.compile('bo{2,}k')
print(a.search("booooook"))
b = re.compile('bo{,4}k')
print(b.search("bk"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 8), match='booooook'>
<re.Match object; span=(0, 2), match='bk'>
- 반복(?) : ? 바로 앞에 있는 문자가 0 번부터 1 번까지 반복되는 문자열을 매치
즉, “?”는 “{0,1}”과 동일한 의미를 가지게 됩니다. 아래 예시에서 “book”은 ‘bo?k’에 매치되지 않은 것을 볼 수 있습니다.
import re
c = re.compile('bo?k')
print(c.search("bk"))
print(c.search("bok"))
print(c.search("book"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 2), match='bk'>
<re.Match object; span=(0, 3), match='bok'>
None
- OR(|) : 기호 앞 뒤로 있는 문자열 중 하나의 문자열과 매치
‘cat | dog’은 “cat” 혹은 “dog”가 있는 문자열과 매치하므로 “catdog”에서는 앞 부분에 있는 “cat”과 먼저 매치된다. |
import re
c = re.compile('cat|dog')
print(c.search("category"))
print(c.search("Snoop dogg"))
print(c.search("catdog"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 3), match='cat'>
<re.Match object; span=(6, 9), match='dog'>
<re.Match object; span=(0, 3), match='cat'>
- 꺽쇠(^) : 기호 이후의 문자열이 처음으로 나오는 문자열과 매치
”^”가 [ ] 안에 있을 경우 해당 문자열을 제외한 문자와 매치한다는 뜻이 되지만, 일반적인 의미는 위와 같습니다. “^인천”으로 했을 때, “인천광역시”는 “인천”이 처음부터 나오므로 매치되지만, “동인천”은 처음으로 나오는 문자가 “동”이므로 매치되지 않습니다.
import re
c = re.compile('^인천')
print(c.search("인천광역시"))
print(c.search("동인천"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 2), match='인천'>
None
- 달러($) : 기호 이전의 문자열이 끝으로 나오는 문자열과 매치
”^”와 정반대의 의미를 가집니다.
import re
c = re.compile('리$')
print(c.search("소리"))
print(c.search("리본"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(1, 2), match='리'>
None
- 단어 구분자(\b) : 앞 뒤로 모두 whitespace가 있는 문자와 매치
\b를 사용할 때는 "\"가 백슬래시가 아닌 단어 구분자임을 알려 줘야 합니다. 따라서 r’\b눈\b’와 같이 앞에 r을 꼭 붙여서 사용해야 합니다. 지금과 같은 경우 외에도 "\"가 whitespace가 아닌 하나의 문자로 해석되게 하려면 r을 붙이면 됩니다. 노래 가사 세 개 중 첫 번째는 “한눈에”에서 “눈” 앞 뒤로 whitespace가 아니기 때문에 매치되지 않지만, 두 번째 가사는 “눈” 앞 뒤로 공백인 whitespace이므로 매치됩니다. 마지막 가사는 “눈물이”에서 앞에는 whitespace이지만 뒤가 문자이므로 매치되지 않습니다.
import re
c = re.compile(r'\b눈\b')
print(c.search("이윽고 내가 한눈에 너를 알아봤을 땐"))
print(c.search("너의 눈 코 입 날 만지던 네 손길"))
print(c.search("너무 슬퍼서 눈물이 안 났어"))
--------------------------------------------------------------------------------------------------------------------------------
None
<re.Match object; span=(3, 4), match='눈'>
None
- 단어 구분자(\B) : 앞 뒤로 whitespace가 모두 없는 문자와 매치
이전에 말씀드렸듯이 대문자는 소문자와는 반대의 의미를 가집니다. 첫 번째 가사는 “한눈에”에서 “눈”의 앞뒤로 모두 문자로 둘러싸여 있으므로 매치됩니다. 두 번째와 세 번째 가사는 “눈”의 앞뒤로 모두 둘러싸여 있지 않으므로 None이 출력됩니다.
import re
c = re.compile(r'\B눈\B')
print(c.search("이윽고 내가 한눈에 너를 알아봤을 땐"))
print(c.search("너의 눈 코 입 날 만지던 네 손길"))
print(c.search("너무 슬퍼서 눈물이 안 났어"))
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(8, 9), match='눈'>
None
None
- .*? : 줄 바꿈 문자를 제외한 모든 문자를 0개 이상으로 찾는데, 이를 최소 크기로 찾는 것
이전 프로그래머스 포스트에서 잠깐 말씀드렸고, 상당히 많이 사용되는 기호입니다. 예시로 먼저 설명드리겠습니다. 정규표현식 ‘<.*>’에 대하여 “<사과><귤><배><딸기><수박><레몬><포도>" 문자열을 매치시킵니다. '<.\*>'은 \n을 제외한 모든 문자가 0개 이상이면서 "<>"로 둘러싸인 문자를 매치시킵니다. 그런데 "*" 메타 문자는 조건을 만족시키는 문자열을 최대한 가져오는 특성이 있습니다. 따라서, "<사과>"만 가져오는 것이 아니라 "<사과><귤><배><딸기><수박><레몬><포도>" 문자열 전체를 결과로 가져오게 됩니다. 여기서 0 번부터 1 번까지 반복되는 문자를 매치시키는 물음표를 뒤에 붙이게 되면 이러한 조건의 문자를 최소 크기로 찾는다는 의미가 됩니다. 결론적으로 '<.\*?>'에 대해서는 "<사과>"만 결과로 나오게 됩니다.사과>레몬>수박>딸기>배>귤>포도>레몬>수박>딸기>배>귤>사과>
import re
c = re.compile('<.*>')
print(c.match("<사과><귤><배><딸기><수박><레몬><포도>").group())
--------------------------------------------------------------------------------------------------------------------------------
<사과><귤><배><딸기><수박><레몬><포도>
import re
c = re.compile('<.*?>')
print(c.match("<사과><귤><배><딸기><수박><레몬><포도>").group())
--------------------------------------------------------------------------------------------------------------------------------
<사과>
다음 포스팅으로 컴파일 옵션에 대해 설명드리겠습니다!
댓글남기기