[Python][정규 표현식] 정규 표현식
정규 표현식 (regular expression, 간단히 regexp) 또는 정규식은 특정 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 언어이다. 정규 표현식은 파이썬에서만 존재하는 고유 언어가 아닌 여러 텍스트 편집기(word, 한글 등등) 또는 수많은 프로그래밍 언어에서도 사용할 수 있는 언어이다. 정규식을 이용하면 문자열 데이터 처리를 더 수월하게 할 수 있다. 정규식을 모르는 상태로 문자열 처리를 위해 코드를 짤 때에 비해 정규식을 알고 이를 코드에 이용할 때가 더 코드의 길이를 줄일 수 있고 따라서 가독성을 높일 수 있다는 장점을 가지고 있다. 그리고 검색할 문자열의 규칙이 복잡해진다면 정규식을 사용하는 것이 더욱 유리해진다. 정규식은 문자열의 검색 (텍스트 편집기나 웹페이지에서 ctrl+F로 특정 단어 찾을 때)과 치환 등에 사용된다고 한다.
정규식에서는 정규 문자 (또는 리터럴 문자)와 메타 문자로 나뉜다. 정규 문자는 문자 그대로를 의미한다. 예를 들어 n이란 문자는 말 그대로 n을 의미하지, 다른 것을 의미하지 않는다. 하지만 메타 문자는 해당 문자의 본래 뜻이 아닌 다른 뜻을 가진 문자를 의미한다. 파이썬에서 \n이란 문자는 \n 문자 그대로를 의미하는 게 아니라 “개행”을 의미하는 것과 같은 원리이다. 정규식에서는 전체 문자열 중에서 특정 조건을 만족하는 문자열을 추출하기 위해서 정규 문자와 메타 문자를 적절히 활용한다.
정규식에서의 메타 문자에는 다음의 문자들이 있다.
-
🐕
_________________________________________ | #**#$)!@!)!($)$#{}[]##@$@[abad] | ∠________________________________________|
( ㆆ Дㆆ )
사실 만화 캐릭터들은 욕을 하고 있던 게 아니라 정규 표현식을 이용한 고풍스러운 대화를 하고 있던 게 아닐까?
이 때, 전체 문자열 속에서 특정 조건을 만족시키는 문자열을 찾기 위해 쓰이는 정규 표현식을 패턴이라 한다. 예를 들어
“i wanna wash the dish in the washington DC”
라는 문자열 내에서 앞 뒤로 모두 공백으로 구분된 “wash”라는 문자열을 찾고자 한다. 이를 위해 파이썬에서 정규식을 쓰면 다음과 같다.
위 식을 이용하면 wanna와 the 사이에 있는 wash는 출력되나, washington안에 포함된 wash는 출력되지 않는다. 위 정규식이 패턴이 되는 것이다.
어떤 파이썬 서적[1]에서는 특정 문자열을 찾기 위해 가져온 전체 문자열을 소스(source)라고 부르기도 한다. 위의 “i wanna wash the dish in the washington DC”가 소스에 해당된다. 그 외 서적에서는 ‘문자열’이라는 말로 보통 쓰는데, 개인적으로는 조금은 포괄적이고 구별되지 않는 용어라고 느껴져서 (비교할 문자열(패턴)이라는 것인지 비교할 대상이 포함된 전체 문자열이라는 건지…) 여기서는 소스라는 표현을 그대로 빌려 쓰고자 한다.
지금부터는 이러한 정규 표현식의 자세한 문법과, 파이썬에서 정규 표현식을 지원하는 re 모듈에 대해서 알아보자.
메타 문자
앞에서 메타 문자는 그 문자 그대로의 뜻이 아닌 다른 특별한 뜻으로 쓰이는 문자를 지칭하는 개념이라 하였고, 메타 문자들을 나열하였다. 이제 각 메타 문자들이 가지는 각각의 뜻과 사용법에 대해서 알아보겠다.
문자클래스 []
문자 클래스(character class)를 뜻하는 []는 “[] 안에 들어간 문자들과 매치(match, 일치)”라는 의미를 갖는다. 다음의 예를 보자.
'[abc]'
'a', 'responsible', 'dirty'
위 패턴의 [abc]는 a, b, c 중 하나의 문자와 매치를 뜻한다. 소스 ‘a’는 해당 패턴을 만족시키므로 해당 패턴과 매치가 된다. ‘responsible’에는 b가 포함되어 있으므로 패턴을 만족시킨다. 반면 ‘dirty’에는 a, b, c 문자 모두 존재하지 않기에 매치되지 않는다.
문자 클래스를 의미하는 메타 문자인 [ ] 대괄호 안에는 어떤 문자도, 어떤 메타 문자도 들어갈 수 있다. 한 가지 주의할 점은, 문자 클래스 안에 ^라는 메타 문자 사용 시 해당 문자는 not을 의미한다. 예를 들어 [^abc]는 abc가 아닌 문자만 매치된다.
[ ] 대괄호 안에 하이픈 (-)을 쓰면 두 문자 사이의 범위 (from - to)를 의미한다. 예를 들어 [abc]는 [a-c]와 같고, [0-9]는 [0123456789]와 같은 의미이다. [a-zA-Z]는 영어 대문자 소문자 모두를 포함한다.
문자 클래스 중에서도 [a-zA-Z]와 같이 실제로 자주 쓰이는 정규식들이 있다. 이를 특수 문자로 더 간편하게 표현할 수 있다.
- 정규 표현식에서의 특수 문자 목록
특수 문자 | 의미 | 동일한 표현 |
---|---|---|
\d | 하나의 숫자를 의미 | [0-9] |
\D | 숫자가 아닌 문자를 지칭 | [^0-9] |
\w | 문자+숫자를 포함 (alphanumeric) | [a-zA-Z0-9_] |
\W | 문자, 숫자가 아닌 문자를 지칭 | [^a-zA-Z0-9_] |
\s | whitespace 문자를 지칭 (공백문자, 개행, 탭 등) | [ \t\n\r\f\v] (맨 앞칸은 공백문자 space이다) |
\S | whitespace가 아닌 문자를 지칭 | [^ \t\n\r\f\v] |
\b | 단어 구분자 (word boundary), whitespace로 구분된 문자를 지칭 | 파이썬에서 문자열 안에 그대로 쓰면 BackSpace로 인식하므로 r’\bspace\b’처럼 raw string과 같이 써야 한다. |
\B | whitespace로 구분되지 않은 문자를 지칭 | 파이썬에서 문자열 안에 그대로 쓰면 BackSpace로 인식하므로 r’\bspace\b’처럼 raw string과 같이 써야 한다. |
\A | 소스의 첫 문자열과 매치. ^ 메타문자와 동일하나 re.MULTILINE 옵션 사용 시 여러 줄에 상관없이 전체 문자열의 처음하고만 매치 | |
\Z | 문자열의 끝과 매치. re.MULTILINE 옵션 사용 시 전체 문자열의 끝과 매치 |
위 표를 유심히 보면 알겠지만, 보통 대문자는 소문자의 반대임을 알 수 있다. 한 편 위 표에 나온 re.MULTILINE은 앞으로 다룰 re 모듈에 대한 내용에서 자세히 나올 것이다.
dot .
dot 메타 문자는 개행(줄바꿈) 문자인 \n을 제외한 모든 문자와 매치됨을 의미한다. 다음의 예제를 보자.
'a.b' => a + \n을 제외한 모든 문자 + b
'aab', 'a4b', 'abc'
위 패턴은 즉 a와 b라는 문자 사이에 \n을 제외한 모든 문자가 들어가도 매치된다는 뜻이다.
위 소스와 비교해서 살펴보자. ‘aab’의 경우 a와 b 사이에 a라는 문자가 들어왔는데, 이는 닷에서 말하는 모든 문자라는 범위 내에 포함되는 문자이므로 패턴과 일치한다. ‘a4b’의 경우 a, b 사이에 4라는 문자가 들어왔으며 이도 모든 문자에 포함되므로 패턴과 매치된다. 그러나 ‘abc’의 경우 a와 b 사이에 아무런 문자도 존재하지 않으므로 해당 소스는 패턴과 매치되지 않는다.
한 편 정규식 a.b는 a[.]b와는 또 의미가 다르다. 닷은 문자 클래스 안에 들어가면 메타 문자가 아닌 정규 문자가 된다. 즉, 개행을 제외한 모든 문자라는 뜻의 메타 문자가 아닌 점이라는 문자 그 자체를 의미하게 된다. 따라서 만약 패턴으로 a[.]b이라 쓰면 ‘a.b’이라 쓴 문자열하고는 매치가 되겠지만 ‘a4b’와는 매치되지 않게 된다.
반복 *
- 메타 문자는 반복을 의미하며, 해당 메타 문자의 바로 앞에 쓰인 문자가 0회부터 거의 무한대까지 반복될 수 있음을 의미한다. 0회 반복이라는 것은 반복되지 않는다는 것도 포함한다는 뜻이다.
'bo*m'
'bm', 'bom', 'boom' 'bam'
패턴에 쓰인 정규식은 * 앞에 쓰인 o라는 문자가 반복되지 않거나 여러 번 반복되는지를 확인한다. 제시된 소스를 보면 ‘bm’의 경우 o가 0번 반복되므로 매치된다. ‘bom’, ‘boom’의 경우 o라는 문자가 0번 이상 (각각 1번, 2번) 반복되므로 역시 매치된다. 반면 소스 ‘bam’의 경우에는 패턴과는 달리 o가 있을 자리에 전혀 다른 문자인 a가 들어가 있으므로 매치되지 않는다.
반복 +
- 메타 문자는 * 메타 문자와 똑같이 반복을 의미한다. 다른 점은 * 은 최소 0번 이상 반복될 때 쓰이며, + 는 최소 1번 이상 반복을 의미한다.
'bo+m'
'bm', 'bom', 'boom', 'bam'
‘bm’의 경우 o라는 문자가 1번 이상 반복되지 않기에 (즉 해당 문자가 존재하지 않기에) 매치되지 않는다. ‘bom’, ‘boom’은 해당 문자가 적어도 1번 이상 반복되므로 매치된다. ‘bam’은 아까와 마찬가지로 o가 1번 이상 반복되지 않고 대신 다른 문자인 a가 들어가 있으므로 매치되지 않는다.
반복 {m,n}, ?
*와 + 라는 메타 문자들은 모두 반복 횟수 제한 없이 쓰였는데, 유한한 반복 횟수로 제한시키고 싶을 때도 있을 것이다. 이럴 때 메타 문자인 중괄호 {}와 그 안에 반복 횟수를 넣으면 된다.
- {m,n}
위 정규식은 반복 횟수 m회부터 n회까지 매치됨을 의미한다. m 또는 n은 생략될 수 있다. 따라서 {1,}은 + 와, {0,}은 * 메타 문자와 같은 의미이다.
- {n}
한 편 위 정규식 처럼 하나의 숫자만 써도 되는데 이 때는 반드시 n번 반복을 의미한다.
위 정규식들을 이용한 예제들을 보겠다.
'bo{2}m'
'bom', 'boom', 'booom'
위 패턴의 경우 {2} 바로 앞에 있는 문자 o가 반드시 (더도 말고 덜도 말고) 2번 반복되야 함을 의미한다. 따라서 위 소스들 중 ‘boom’만이 패턴과 매치된다.
'bo{1,4}m'
'bom', 'boom', 'boooom'
위 패턴에서는 o라는 문자가 1회~4회 반복됨을 확인한다. 따라서 위 소스 전부 해당 패턴과 매치된다.
{m,n} 사용 시 {m, n}처럼 쉼표 쓰고 한 칸 뛰고 n을 쓰지 않도록 주의한다. 파이썬에서 직접 해보니 한 칸 뛰면 문법에 틀려서인지 실행이 되지 않는다. 반드시 쉼표 뒤에 바로 숫자를 쓰자.
- ?
한 편 메타 문자 ?도 존재한다. 이는 {0,1}과 동일한 의미이다. 즉, 해당 문자가 없어도 되고 있어도 된다는 뜻이다.
'bo?m'
'bm', 'bom'
위 패턴은 o라는 문자가 있어도, 없어도 된다는 뜻이므로 위의 소스들 모두 패턴과 매치된다.
or |
‘ | ’ 메타 문자는 or을 의미한다. 그래서 [abc] = a | b | c 이다. |
'photo|picture'
'look at the picture marked number 1' => 패턴과 매치
문자열의 맨 처음과 일치 여부 ^
^ 메타 문자는 문자열의 맨 처음과 매치됨을 의미한다.
import re
source = 'it is good to see you'
print(re.search('^it', source))
print(re.match('it', source))
<re.Match object; span=(0, 2), match='it'>
<re.Match object; span=(0, 2), match='it'>
여기서 나온 re 모듈은 re 모듈 에서 자세히 언급될 것이다. 여기서 먼저 말하자면, match() 메서드는 패턴이 source의 맨 첫 문자열과 일치하는지 여부만 확인하고 그 뒤 문자열은 검색하지 않는다. 반면, search() 메서드는 패턴을 만족시키는 source 내 모든 문자열을 전부 검사하여 매치되는 문자열 한 개만 찾는다. 그런데 메타 문자 ^를 search() 메서드와 함께 쓰면 마치 match() 메서드를 쓰는 것과 같은 효과를 준다.
import re
source = 'is it good to see you?'
print(re.search('^it', source))
print(re.search('it', source))
None
<re.Match object; span=(3, 5), match='it'>
위 예제에서 ^ 메타 문자의 의미를 재확인할 수 있다.
컴파일 옵션 re.MULTILINE을 같이 사용할 경우 소스가 여러 줄의 문자열일 때 각 줄의 첫 문자열과 패턴을 대조한다. 컴파일 옵션도 re 모듈의 컴파일 옵션 설명 때 나올 것이다.
문자 클래스에서도 언급했듯, ^ 메타 문자는 문자 클래스 메타 문자 [] 안에서 쓰일 시 Not의 의미를 지닌다. [^a]는 a가 아닌 문자를 의미한다.
문자열의 끝과 일치 여부, $
해당 메타 문자는 ^ 메타 문자와 반대의 의미를 지닌다. 즉, 문자열의 끝과의 일치 여부를 확인한다.
^ 메타 문자는 그 의미에 맞게 적용시킬 문자 바로 앞에 썼다. $도 마찬가지로 $를 적용시킬 정규 문자 뒤에 적는다.
import re
source = 'is it good to see you'
print(re.search('you$', source))
print(re.search('see$', source))
<re.Match object; span=(18, 21), match='you'>
None
해당 메타 문자도 컴파일 옵션 re.MULTILINE의 영향을 받는다. 이를 다음 예제에서 확인할 수 있다.
import re
source = '''it is good to see you
it is fine thank you
and you
?'''
pattern_dollor = re.compile('you$', re.MULTILINE)
pattern_up = re.compile('^it', re.MULTILINE)
print(pattern_dollor.findall(source))
print(pattern_up.findall(source))
['you', 'you', 'you']
['it', 'it']
지금까지 소개된 메타 문자 정리
메타 문자 | 기능 | 예) | ||
---|---|---|---|---|
[] (문자 클래스) | [] 안에 들어간 모든 문자와 매치 | [abc] = 문자 a 또는 b 또는 c | ||
. | 개행(줄바꿈) 문자인 \n을 제외한 모든 문자와 매치 | approve란 문자열은 a.e에 해당 | ||
* | 0번 이상 반복되는 문자와 매치 | ‘bo’, ‘bro’, ‘brro’ 모두 br*o에 해당 | ||
+ | 1번 이상 반복되는 문자와 매치 | ‘bro’, ‘brro’는 br+o에 해당하나 ‘bo’는 매치되지 않음 | ||
{m, n} | 바로 앞 문자가 m번 부터 n번 까지의 반복 횟수를 가지는 지 여부 | bo{1,3}m = ‘bom’, ‘boom’, ‘booom’ | ||
? | 바로 앞 문자가 반복되지 않거나 한 번 반복되면 매치 | bo?m = ‘bm’, ‘bom’ | ||
or를 의미 | A | B ⇒ 문자열 ‘A’일 경우 ‘A’와 매치됨 | ||
^ | 소스의 첫 문자열과의 일치 여부. (문자 클래스 [] 안에 쓰이면 not, 즉 반대라는 의미) | ‘money in my pocket’은 정규식 ‘^money’와 매치 | ||
$ | 소스의 마지막 문자열과의 일치 여부 | ‘money in my pocket’은 정규식 ‘pocket$’와 매치 |
zero-length match, zero-width assertion
사실 메타 문자는 [], .(dot), *, +, {}, ?와 |, ^, $, \b, \A, \Z으로 두 부류로 나눌 수 있다. 이 둘의 차이점은 전자의 경우는 문자 자체와의 일치 여부를 검색하지만, 후자의 메타 문자들은 문자 자체와의 매치가 아닌 위치가 매치되는지를 검색한다. 다시 말해 전자의 메타 문자들은 검색 대상이 ‘문자’이고, 후자의 메타 문자들은 검색 대상이 ‘위치’인 것이다. (사실 앞서 ^와 $에 대해 다룰 때 각각 “첫 문자열과의 일치”, “마지막 문자열과의 일치”란 표현에서 이미 이 사실이 암시되고 있었는지도 모른다)
예를 들어 정규식이 ‘.’이고 소스 문자열이 ‘abc’라고 한다면 이 정규식 조건에 맞는 문자는 a, b, c 전부이다. 즉 .이라는 정규식이 소스 내의 각각의 문자들과의 매치 여부를 검색한 것이다. 그래서 만약 소스 문자열이 아무 문자도 없는 ‘’ 이라면 매치되는 결과는 없다.
반면, 정규식이 ‘^a’ 이고 소스가 ‘abc’일 때 해당 정규식의 의미는 ‘^라는 메타문자로 표시한 곳이 a라는 문자 바로 앞의 위치인가? 즉, a라는 문자 앞에 아무런 문자도 없는가?”이다. 즉 ^는 어떤 문자와의 매치가 아닌 위치와의 매치 여부를 묻는 메타 문자라 할 수 있을 것이다. 그래서 정규식이 ‘^’일 때 소스가 아무 문자도 없는 ‘’이라면 ‘’와 매치된다는 결과를 받을 것이다. 이렇게 정규식에 ^, $, |만 쓰면 빈 문자열과 매치되는 zero-length match가 이뤄진다. 여기서 빈 문자열은 말 그대로 문자 길이가 0이기에 해당 이름이 붙여진 것으로 추정된다. (반대로 말하자면 전자의 메타문자들은 문자가 매칭 대상이니 길이가 존재하는 nonzero-length겠지. 근데 이 용어는 존재하지 않는 것 같더라…)
이 zero-length match로 인해 생기는 특징이 하나 있다. zero-length match의 특징을 가지는 메타 문자들은 소스와 매칭될 때 검색 위치가 앞으로 나아가지 않고 그대로 멈춘다는 것이다.
사실 소스 속에서 정규식으로 쓰인 패턴이 일치하는 부분이 있는지 검색하는 과정은 소스 문자열의 맨 처음부터 시작하여 한 글자씩 패턴과 대조하는 식이다. 예를 들어 정규식이 ‘\d\s\d’이고, 소스가 ‘3 45’일 때를 보자.
▼
'\d\s\d'
▼
'3 45'
우선 소스의 맨 첫 문자열인 3과 패턴의 맨 처음 부분인 \d와의 매치 여부를 확인한다. 매치되므로 다음으로 넘어간다.
▼
'\d\s\d'
▼
'3 45'
그 후 정규식에서는 다음 정규식인 \s로 넘어가고, 소스에서는 다음 문자인 ‘ ’ (공백문자) 으로 넘어가서 \s와 ‘ ‘가 서로 매치되는지 검색한다. 그 다음으로 정규식도 \d로 넘어가고 소스 내 검색 대상 문자열도 다음으로 넘어가는 식으로 반복된다.
다음은 패턴이 ‘^g$’ 이고 소스가 ‘abc\ndef\ng’ 인 예제를 생각해보자. (\n은 개행 문자) (이 때 ^와 $는 각 줄마다 적용되도록 했다고 가정한다)
▼
'^g$'
▼
abc
def
g
맨 처음 정규식은 ^이다. 해당 메타 문자는 문자가 아닌 문자 앞 위치를 지칭한다. 소스에서는 a 바로 앞의 아무 것도 없는 곳을 가리킨다. 일단 a 앞에 아무 문자도 없으니 ^는 매치되는 셈이다.
▼
'^g$'
▼
abc
def
g
그런데, 앞서 말했듯 ^은 zero-length이기 때문에 소스에서는 다음 문자인 b로 검색 위치가 넘어가지 않고 a에 그대로 남게 되는 것이다. 패턴에서는 다음 정규식인 g로 넘어갔고, 여기서 g는 리터럴 문자이기에 a와 매치되는지 확인한다. g는 a가 아니므로 매치되지 않는다. 벌써 패턴을 만족시키지 않기에 다음의 예제처럼 $는 생략하고 다음 문자인 b로, 패턴에서는 다시 처음 문자인 ^로 돌아가 계속 검색을 진행하며 이러한 과정을 소스 끝까지 반복한다.
▼
'^g$'
▼
abc
def
g
(위 상태에서는 b라는 문자 앞에 a라는 문자가 존재하므로 ^를 만족시키지 못한다. 따라서 이 경우 c라는 다음 문자로 넘어가며, 패턴 커서는 다시 ^로 돌아간다)
이러한 특징을 두고 ^, $ 등의 메타 문자들은 “문자열을 소비시키지 않는다”라고 말하기도 하고, zero-width assertions이라고도 한다. zero-width assertion에 해당하는, 즉 문자열을 소비시키지 않는 것에는 다음에 다룰 전방탐색과 후방탐색도 포함된다.
References
[1] Bill Lubannovic, “Introducing Python” (O’REILLY, 2015)
[2] 정규 표현식, 점프 투 파이썬
[3] 정규 표현식, 위키백과
https://ko.wikipedia.org/wiki/정규_표현식#:~:text=정규 표현식
[4] RegExp.info
Start of String and End of String Anchors
[5] 코드쓰는사람
This content is licensed under
CC BY-NC 4.0
댓글남기기