정규 표현식 HOWTO
정규 표현식 HOWTO (Regular Expression HOWTO)
이 문서는 re 모듈로 Python에서 정규 표현식을 사용하는 방법을 입문 튜토리얼로 설명해요. 라이브러리 레퍼런스의 해당 절보다 더 부드럽게 접근합니다. 저자는 A.M. Kuchling이고, 저자의 이메일은 [email protected]예요.
출처: Python 공식 문서
들어가며 (Introduction)
정규 표현식(RE, regex, regex pattern이라고도 불러요)은 본질적으로 Python 안에 내장되어 re 모듈로 사용할 수 있게 된, 아주 작고 고도로 전문화된 일종의 프로그래밍 언어예요. 이 작은 언어로 여러분은 매치하고 싶은 문자열 집합의 규칙을 지정합니다. 그 집합에는 영어 문장, 이메일 주소, TeX 명령, 여러분이 원하는 무엇이든 들어갈 수 있어요. 그러고 나서 "이 문자열이 패턴과 매치되나요?", "이 문자열 어딘가에 패턴 매치가 있나요?" 같은 질문을 던질 수 있어요. 또 RE를 써서 문자열을 수정하거나 여러 방식으로 쪼갤 수도 있습니다.
정규 표현식 패턴은 일련의 바이트코드로 컴파일된 뒤, C로 작성된 매칭 엔진이 그걸 실행해요. 고급 사용을 위해서는 주어진 RE를 엔진이 어떻게 실행할지 세심히 주의를 기울여, 더 빨리 도는 바이트코드가 나오도록 RE를 특정 방식으로 작성해야 할 수도 있어요. 최적화는 이 문서에서 다루지 않습니다. 그건 매칭 엔진 내부에 대한 깊은 이해가 필요하기 때문이에요.
정규 표현식 언어는 비교적 작고 제약이 있어서, 모든 문자열 처리 작업을 정규 표현식으로 할 수는 없어요. 또 정규 표현식으로 할 수는 있지만 표현식이 아주 복잡해지는 작업도 있어요. 그런 경우에는 Python 코드를 직접 작성해 처리가 나을 수 있어요. Python 코드는 정교한 정규 표현식보다 느리겠지만, 대신 더 이해하기 쉬울 가능성이 높습니다.
단순한 패턴 (Simple patterns)
가장 단순한 정규 표현식부터 배워 볼게요. 정규 표현식은 문자열을 다루는 데 쓰이므로, 가장 흔한 작업인 문자 매치부터 시작해요. 정규 표현식의 바탕이 되는 컴퓨터 과학(결정적·비결정적 유한 오토마타)에 대한 자세한 설명은 컴파일러 작성에 관한 거의 어떤 교과서를 봐도 됩니다.
문자 매치하기
대부분의 글자와 문자는 단순히 자기 자신과 매치됩니다. 예를 들어 정규 표현식 test는 문자열 test와 정확히 매치돼요. (대소문자 무시 모드를 켜면 이 RE가 Test나 TEST도 매치하게 할 수 있는데, 이건 나중에 다룰게요.)
이 규칙에는 예외가 있는데, 어떤 문자들은 특별한 **메타문자(metacharacter)**라서 자기 자신과 매치되지 않습니다. 대신 보통이 아닌 무언가를 매치하라고 신호를 보내거나, RE의 다른 부분을 반복시키거나 의미를 바꾸는 식으로 영향을 줘요. 이 문서의 상당 부분이 여러 메타문자가 무엇을 하는지 설명하는 데 쓰입니다.
메타문자의 완전한 목록은 다음과 같아요. 그 의미는 이 HOWTO의 나머지에서 다룰게요.
. ^ $ * + ? { } [ ] \ | ( )
가장 먼저 볼 메타문자는 [와 ]예요. 이건 문자 클래스(character class), 즉 매치하고 싶은 문자들의 집합을 지정하는 데 쓰입니다. 문자를 하나씩 나열할 수도 있고, 두 문자를 주고 그 사이를 '-'로 구분해 범위를 나타낼 수도 있어요. 예를 들어 [abc]는 a, b, c 중 아무 문자와나 매치되는데, 이건 범위로 같은 집합을 표현한 [a-c]와 같아요. 소문자만 매치하고 싶다면 RE는 [a-z]가 됩니다.
메타문자(\ 제외)는 클래스 안에서는 활성화되지 않아요. 예를 들어 [akm$]는 'a', 'k', 'm', '$' 중 아무 문자와나 매치됩니다. '$'는 보통 메타문자지만 문자 클래스 안에서는 그 특별한 성질이 벗겨져요.
클래스에 나열되지 않은 문자를 매치하려면 집합을 **보완(complement)**하면 됩니다. 클래스의 첫 문자로 '^'를 넣는 것으로 표시해요. 예를 들어 [^5]는 '5'를 제외한 아무 문자와나 매치됩니다. 캐럿이 문자 클래스의 다른 위치에 나타나면 특별한 의미를 갖지 않아요. 예: [5^]는 '5' 또는 '^'를 매치합니다.
아마 가장 중요한 메타문자는 백슬래시 \일 거예요. Python 문자열 리터럴에서처럼, 백슬래시 뒤에 여러 문자를 붙여 다양한 특별 시퀀스를 나타낼 수 있어요. 또 백슬래시는 모든 메타문자를 이스케이프해서 패턴 안에서도 그들을 문자 그대로 매치할 수 있게 해 줘요. 예를 들어 [나 \를 매치해야 한다면 그 앞에 백슬래시를 붙여 특별한 의미를 제거하면 됩니다: \[ 또는 \\.
'\'로 시작하는 특별 시퀀스 중에는 자주 유용하게 쓰이는 미리 정의된 문자 집합을 나타내는 것들이 있어요. 예를 들면 숫자 집합, 글자 집합, 공백이 아닌 것 집합 같은 것이죠.
예를 들어 볼게요: \w는 영숫자(alphanumeric) 문자와 매치해요. 정규 표현식 패턴이 바이트로 표현되면 이는 클래스 [a-zA-Z0-9_]와 동일합니다. 패턴이 문자열이라면 \w는 unicodedata 모듈이 제공하는 Unicode 데이터베이스에서 글자(letter)로 표시된 모든 문자와 매치돼요. 문자열 패턴에서 더 제한된 \w 정의를 쓰려면 정규 표현식을 컴파일할 때 re.ASCII 플래그를 주면 됩니다.
다음 특별 시퀀스 목록은 완전하지 않아요. 완전한 목록과 Unicode 문자열 패턴의 확장 클래스 정의는 표준 라이브러리 레퍼런스의 Regular Expression Syntax 마지막 부분을 보세요. 일반적으로 Unicode 버전은 Unicode 데이터베이스에서 해당 범주에 있는 어떤 문자든 매치합니다.
\d— 십진 숫자와 매치; 클래스[0-9]와 동일.\D— 숫자가 아닌 문자와 매치; 클래스[^0-9]와 동일.\s— 공백 문자와 매치; 클래스[ \t\n\r\f\v]와 동일.\S— 공백이 아닌 문자와 매치; 클래스[^ \t\n\r\f\v]와 동일.\w— 영숫자 문자와 매치; 클래스[a-zA-Z0-9_]와 동일.\W— 영숫자가 아닌 문자와 매치; 클래스[^a-zA-Z0-9_]와 동일.
이런 시퀀스는 문자 클래스 안에 포함시킬 수도 있어요. 예를 들어 [\s,.]는 공백 문자, ',', '.' 중 아무거나 매치하는 문자 클래스입니다.
이 절의 마지막 메타문자는 .예요. 이건 줄바꿈 문자를 제외한 아무 것이나 매치하고, 줄바꿈까지도 매치하는 대체 모드(re.DOTALL)도 있어요. .는 "아무 문자나" 매치하고 싶을 때 자주 쓰입니다.
반복하기 (Repeating things)
가변적인 문자 집합을 매치할 수 있다는 건, 문자열에 이미 있는 메서드로는 할 수 없었던 정규 표현식의 첫 번째 능력이에요. 하지만 그것만이 추가된 능력이라면 별로 진보가 아니었겠죠. 또 다른 능력은, RE의 일부가 특정 횟수만큼 반복되어야 한다고 지정할 수 있다는 겁니다.
반복을 위한 첫 메타문자는 *이에요. *는 글자 그대로의 문자 '*'와 매치하지 않고, 앞의 문자가 정확히 한 번이 아니라 0번 이상 매치될 수 있다고 지정합니다. 예를 들어 ca*t는 'ct'('a' 0개), 'cat'('a' 1개), 'caaat'('a' 3개) 등을 매치해요.
* 같은 반복은 **탐욕적(greedy)**입니다. RE를 반복할 때 매칭 엔진은 그걸 가능한 한 많이 반복하려고 해요. 패턴의 나중 부분이 매치되지 않으면, 그때 매칭 엔진은 되돌아가며 더 적은 반복으로 다시 시도합니다.
단계별 예시를 보면 더 명확해져요. 표현식 a[bcd]*b를 생각해 봅시다. 이건 글자 'a', 클래스 [bcd]에서 0개 이상의 글자, 마지막에 'b'로 끝나는 걸 매치해요. 이제 이 RE를 문자열 'abcbd'에 매치한다고 상상해 보세요.
| 단계 (Step) | 매치된 것 (Matched) | 설명 (Explanation) |
|---|---|---|
| 1 | a |
RE의 a가 매치됩니다. |
| 2 | abcbd |
엔진이 [bcd]*를 매치하는데, 갈 수 있는 데까지, 즉 문자열 끝까지 갑니다. |
| 3 | 실패 (Failure) | 엔진이 b를 매치하려 하는데 현재 위치가 문자열 끝이라 실패합니다. |
| 4 | abcb |
한 글자 덜 매치하도록 [bcd]*를 되돌립니다. |
| 5 | 실패 (Failure) | b를 다시 시도하지만 현재 위치가 마지막 문자인 'd'입니다. |
| 6 | abc |
[bcd]*가 bc만 매치하도록 다시 되돌립니다. |
| 7 | abcb |
b를 다시 시도합니다. 이번엔 현재 위치의 문자가 'b'라서 성공합니다. |
이제 RE의 끝에 도달했고, 'abcb'를 매치했어요. 이건 매칭 엔진이 처음엔 갈 수 있는 데까지 가고, 매치를 찾지 못하면 점진적으로 되돌아가며 RE의 나머지를 반복해서 다시 시도하는 방식을 보여줍니다. 엔진은 [bcd]*가 0번 매치할 때까지 되돌아가고, 그래도 실패하면 그 문자열은 RE와 전혀 매치되지 않는다고 결론 내릴 거예요.
또 다른 반복 메타문자는 +인데, 1번 이상 매치해요. *와 +의 차이를 주의 깊게 보세요. *는 0번 이상 매치하므로 반복되는 대상이 아예 없어도 되지만, +는 최소한 1번은 나와야 해요. 비슷한 예로 ca+t는 'cat'('a' 1개), 'caaat'('a' 3개)를 매치하지만 'ct'는 매치하지 않아요.
반복 연산자(수량자)가 두 개 더 있어요. 물음표 문자 ?는 한 번 또는 0번 매치합니다. 뭔가를 선택 사항(optional)으로 표시한다고 생각하면 돼요. 예를 들어 home-?brew는 'homebrew' 또는 'home-brew'를 매치합니다.
가장 복잡한 수량자는 {m,n}이에요. 여기서 m과 n은 십진 정수입니다. 이 수량자는 최소 m번, 최대 n번 반복해야 한다는 뜻이에요. 예를 들어 a/{1,3}b는 'a/b', 'a//b', 'a///b'를 매치해요. 슬래시가 없는 'ab'나 슬래시가 네 개인 'a////b'는 매치하지 않습니다.
m이나 n 중 하나는 생략할 수 있는데, 그 경우 빠진 값에 대해 합리적인 값이 가정돼요. m을 생략하면 하한이 0으로 해석되고, n을 생략하면 상한이 무한대가 됩니다.
가장 단순한 경우인 {m}은 앞의 항목을 정확히 m번 매치해요. 예를 들어 a/{2}b는 'a//b'만 매치합니다.
환원주의 성향의 독자라면 다른 수량자 세 개가 모두 이 표기법으로 표현될 수 있음을 눈치챘을 거예요. {0,}는 *와 같고, {1,}는 +와 같고, {0,1}은 ?와 같습니다. 가능하면 *, +, ?를 쓰는 게 낫습니다. 그냥 더 짧고 읽기 쉬우니까요.
정규 표현식 사용하기 (Using regular expressions)
이제 몇 가지 단순한 정규 표현식을 봤으니, 실제로 Python에서 어떻게 쓰는 걸까요? re 모듈이 정규 표현식 엔진에 대한 인터페이스를 제공해서, RE를 객체로 컴파일한 뒤 그걸로 매치를 수행할 수 있게 해 줍니다.
정규 표현식 컴파일하기
정규 표현식은 **패턴 객체(pattern object)**로 컴파일되는데, 패턴 객체에는 패턴 매치 검색이나 문자열 치환 같은 다양한 작업을 위한 메서드가 있습니다.
>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')
re.compile()은 선택적 flags 인자도 받아들이는데, 다양한 특수 기능과 문법 변형을 활성화하는 데 쓰여요. 사용 가능한 설정은 나중에 다룰게요. 지금은 예시 하나만 볼게요:
>>> p = re.compile('ab*', re.IGNORECASE)
RE는 문자열로 re.compile()에 넘겨집니다. RE가 문자열로 처리되는 이유는, 정규 표현식이 Python 핵심 언어의 일부가 아니라서 그것을 표현하기 위한 특별한 문법이 만들어지지 않았기 때문이에요. (RE가 전혀 필요 없는 애플리케이션도 있으니, 언어 명세를 부풀려 포함시킬 필요가 없죠.) 대신 re 모듈은 socket이나 zlib 모듈처럼 Python에 포함된 그저 하나의 C 확장 모듈입니다.
RE를 문자열에 넣으면 Python 언어는 더 단순해지지만, 다음 절의 주제가 되는 단점이 하나 있어요.
백슬래시의 저주 (The backslash plague)
앞서 말했듯, 정규 표현식은 백슬래시 문자('\')로 특별한 형태를 나타내거나 특별 문자를 특별한 의미를 발휘하지 않고 쓰게 합니다. 이건 Python이 문자열 리터럴에서 같은 목적으로 같은 문자를 쓰는 것과 충돌해요.
LaTeX 파일에서 찾을 수 있는 문자열 \section을 매치하는 RE를 쓰고 싶다고 해 봅시다. 프로그램 코드에 무엇을 쓸지 알아내려면, 매치하고 싶은 대상 문자열로 시작합니다. 다음으로 백슬래시와 다른 메타문자들 앞에 백슬래시를 붙여 이스케이프해야 합니다. 결과적으로 문자열 \\section이 돼요. re.compile()에 넘겨야 하는 결과 문자열은 \\section이어야 해요. 그런데 이걸 Python 문자열 리터럴로 표현하려면 두 백슬래시를 또 이스케이프해야 합니다.
| 문자 (Characters) | 단계 (Stage) |
|---|---|
\section |
매치할 텍스트 문자열 |
\\section |
re.compile()용으로 이스케이프된 백슬래시 |
"\\\\section" |
문자열 리터럴용으로 이스케이프된 백슬래시 |
간단히 말해, 글자 그대로의 백슬래시 하나를 매치하려면 RE로 '\\\\'를 써야 해요. 정규 표현식이 \\여야 하고, 각 백슬래시는 일반 Python 문자열 리터럴 안에서 \\로 표현돼야 하기 때문이죠. 백슬래시가 반복적으로 등장하는 RE에서는 이게 백슬래시가 잔뜩 반복되는 결과를 낳고 결과 문자열을 이해하기 어렵게 만듭니다.
해결책은 정규 표현식에 Python의 원시 문자열(raw string) 표기를 쓰는 거예요. 'r'이 붙은 문자열 리터럴에서는 백슬래시가 특별히 처리되지 않으므로, r"\n"은 '\'와 'n'을 담은 두 문자 문자열이고 "\n"은 줄바꿈을 담은 한 문자 문자열이에요. 정규 표현식은 Python 코드에서 보통 이 원시 문자열 표기로 작성됩니다.
추가로, 정규 표현식에는 유효하지만 Python 문자열 리터럴로는 유효하지 않은 특별 이스케이프 시퀀스는 이제 SyntaxWarning을 일으키고, 결국에는 SyntaxError가 될 거예요. 즉 원시 문자열 표기나 백슬래시 이스케이프를 쓰지 않으면 그 시퀀스는 유효하지 않게 된다는 뜻입니다.
| 일반 문자열 (Regular String) | 원시 문자열 (Raw string) |
|---|---|
"ab*" |
r"ab*" |
"\\\\section" |
r"\\section" |
"\\w+\\s+\\1" |
r"\w+\s+\1" |
매치 수행하기 (Performing matches)
컴파일된 정규 표현식을 나타내는 객체를 얻었으면, 이걸로 뭘 할까요? 패턴 객체에는 여러 메서드와 속성이 있어요. 여기서는 가장 중요한 것만 다룰게요. 완전한 목록은 re 문서를 참고하세요.
| 메서드/속성 (Method/Attribute) | 용도 (Purpose) |
|---|---|
match() |
RE가 문자열의 시작에서 매치되는지 판별 |
search() |
문자열을 훑으며 이 RE가 매치되는 아무 위치나 찾기 |
findall() |
RE가 매치되는 모든 부분 문자열을 찾아 리스트로 반환 |
finditer() |
RE가 매치되는 모든 부분 문자열을 찾아 **이터레이터(iterator)**로 반환 |
match()와 search()는 매치를 찾지 못하면 None을 돌려줍니다. 성공하면 매치 객체(match object) 인스턴스가 반환되는데, 여기에는 매치에 관한 정보(어디서 시작하고 끝나는지, 매치된 부분 문자열, 그 외)가 담겨 있어요. 이건 re 모듈로 인터랙티브하게 실험해 보면 알 수 있습니다.
이 HOWTO는 예시에 표준 Python 인터프리터를 사용해요. 먼저 Python 인터프리터를 실행하고 re 모듈을 임포트한 뒤 RE 하나를 컴파일합니다:
>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')
이제 RE [a-z]+에 여러 문자열을 매치해 볼 수 있어요. 빈 문자열은 +가 '1번 이상의 반복'을 뜻하므로 전혀 매치되지 않아야 해요. 이 경우 match()는 None을 돌려줘야 하고, 그러면 인터프리터는 아무 출력도 하지 않아요. match()의 결과를 명시적으로 출력하면 이게 분명해집니다.
>>> p.match("")
>>> print(p.match(""))
None
이제 매치되어야 하는 문자열, 예컨대 tempo에 시도해 볼게요. 이 경우 match()는 매치 객체를 반환하므로, 결과를 변수에 저장해 나중에 쓰는 게 좋아요.
>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>
이제 매치 객체에 매치된 문자열에 관한 정보를 물어볼 수 있어요. 매치 객체 인스턴스에도 여러 메서드와 속성이 있는데, 가장 중요한 것들은 이래요:
| 메서드/속성 (Method/Attribute) | 용도 (Purpose) |
|---|---|
group() |
RE가 매치한 문자열 반환 |
start() |
매치의 시작 위치 반환 |
end() |
매치의 끝 위치 반환 |
span() |
매치의 (start, end) 위치를 담은 튜플 반환 |
이 메서드들을 시도해 보면 그 의미가 곧 명확해져요:
>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)
group()은 RE가 매치한 부분 문자열을 돌려줍니다. start()와 end()는 매치의 시작·끝 인덱스를 돌려주고, span()은 시작·끝 인덱스를 하나의 튜플로 돌려줘요. match() 메서드는 RE가 문자열의 시작에서 매치되는지만 검사하므로, start()는 항상 0이 됩니다. 하지만 패턴의 search() 메서드는 문자열을 훑어가므로, 그 경우 매치가 0에서 시작하지 않을 수 있어요.
>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)
실제 프로그램에서 가장 흔한 스타일은 매치 객체를 변수에 저장한 뒤, None인지 확인하는 거예요. 보통 이렇게 생겼습니다:
p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
print('Match found: ', m.group())
else:
print('No match')
패턴에 대한 모든 매치를 돌려주는 패턴 메서드가 두 개 있어요. findall()은 매치된 문자열들의 리스트를 돌려줍니다:
>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']
이 예시에서 r 접두사(리터럴을 원시 문자열 리터럴로 만드는 것)가 필요한 이유는, 일반적인 "요리된(cooked)" 문자열 리터럴에서 Python이(정규 표현식과 달리) 인식하지 못하는 이스케이프 시퀀스가 이제 SyntaxWarning을 일으키고 결국 SyntaxError가 되기 때문이에요. "백슬래시의 저주"를 보세요.
findall()은 결과로 반환하기 전에 전체 리스트를 만들어야 해요. finditer() 메서드는 매치 객체 인스턴스들의 시퀀스를 이터레이터로 반환합니다:
>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator
<callable_iterator object at 0x...>
>>> for match in iterator:
... print(match.span())
...
(0, 2)
(22, 24)
(29, 31)
모듈 수준 함수 (Module-level functions)
패턴 객체를 만들어 그 메서드를 호출하지 않아도 돼요. re 모듈은 match(), search(), findall(), sub() 같은 최상위 함수도 제공합니다. 이 함수들은 대응하는 패턴 메서드와 같은 인자를 받되 RE 문자열을 첫 인자로 추가하며, 여전히 None이나 매치 객체 인스턴스를 돌려줘요.
>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')
<re.Match object; span=(0, 5), match='From '>
속을 들여다보면, 이 함수들은 여러분을 위해 패턴 객체를 만들어 그 위에서 적절한 메서드를 호출할 뿐이에요. 또 컴파일된 객체를 캐시에 저장하므로, 같은 RE를 쓰는 이후 호출은 패턴을 계속 다시 파싱하지 않아도 됩니다.
이 모듈 수준 함수를 쓸까요, 아니면 패턴을 직접 얻어 그 메서드를 호출할까요? 루프 안에서 regex를 접근한다면 미리 컴파일해 두면 몇 번의 함수 호출을 절약할 수 있어요. 루프 밖에서는 내부 캐시 덕분에 별 차이가 없습니다.
컴파일 플래그 (Compilation flags)
컴파일 플래그는 정규 표현식이 동작하는 방식의 일부 측면을 수정하게 해 줍니다. 플래그는 re 모듈에서 IGNORECASE 같은 긴 이름과 I 같은 한 글자 짧은 형태, 두 이름으로 제공돼요. (Perl의 패턴 수정자에 익숙하다면, 한 글자 형태가 같은 글자를 쓴다는 걸 알 거예요. 예를 들어 re.VERBOSE의 짧은 형태는 re.X입니다.) 여러 플래그는 비트 OR로 지정할 수 있어요. 예를 들어 re.I | re.M은 I와 M 플래그를 둘 다 설정합니다.
다음은 사용 가능한 플래그 표이고, 그다음에 각각을 더 자세히 설명할게요.
| 플래그 (Flag) | 의미 (Meaning) |
|---|---|
ASCII, A |
\w, \b, \s, \d 같은 몇몇 이스케이프가 각각의 속성을 가진 ASCII 문자에만 매치하게 함 |
DOTALL, S |
.가 줄바꿈을 포함한 아무 문자에나 매치하게 함 |
IGNORECASE, I |
대소문자 무시 매치를 함 |
LOCALE, L |
로캘 인식 매치를 함 |
MULTILINE, M |
^와 $에 영향을 주는 다중 줄 매치 |
VERBOSE, X ('extended'의 뜻) |
더 깔끔하고 이해하기 쉽게 정리할 수 있는 장황한(verbose) RE를 활성화 |
re.I / re.IGNORECASE — 대소문자 무시 매칭을 수행합니다. 문자 클래스와 리터럴 문자열이 대소문자를 무시하고 글자에 매치돼요. 예를 들어 [A-Z]는 소문자에도 매치됩니다. ASCII 플래그로 비-ASCII 매치를 끄지 않았다면 전체 Unicode 매칭도 동작해요. Unicode 패턴 [a-z] 또는 [A-Z]를 IGNORECASE 플래그와 함께 쓰면, 52개의 ASCII 글자와 4개의 추가 비-ASCII 글자, 즉 'İ'(U+0130), 'ı'(U+0131), 'ſ'(U+017F), 'K'(U+212A)에 매치됩니다. Spam은 'Spam', 'spam', 'spAM', 'ſpam'와 매치해요(마지막은 Unicode 모드에서만 매치). 이 소문자화는 현재 로캘을 고려하지 않아요. LOCALE 플래그도 설정하면 그때는 고려합니다.
re.L / re.LOCALE — \w, \W, \b, \B와 대소문자 무시 매칭을 Unicode 데이터베이스 대신 현재 로캘에 의존하게 합니다. 로캘은 언어 차이를 고려한 프로그램 작성에 도움을 주려고 만들어진 C 라이브러리의 기능이에요. 예를 들어 인코딩된 프랑스어 텍스트를 처리한다면 \w+로 단어를 매치하고 싶을 텐데, 바이트 패턴에서 \w는 클래스 [A-Za-z]에만 매치하므로 é나 ç에 해당하는 바이트는 매치하지 못해요. 시스템이 적절히 설정되고 프랑스어 로캘이 선택되면, 특정 C 함수들이 프로그램에 é에 해당하는 바이트도 글자로 간주해야 한다고 알려줍니다. 정규 표현식을 컴파일할 때 LOCALE 플래그를 설정하면 그 결과 컴파일된 객체가 \w에 대해 그 C 함수들을 쓰게 해요. 이건 더 느리지만, \w+가 기대대로 프랑스어 단어를 매치하게도 해 줍니다. Python 3에서는 이 플래그 사용이 권장되지 않아요. 로캘 메커니즘이 매우 신뢰할 수 없고, 한 번에 하나의 "문화"만 처리하며, 8비트 로캘에서만 동작하기 때문입니다. Python 3는 Unicode(str) 패턴에 기본적으로 이미 Unicode 매칭을 켜고 있고, 여러 로캘/언어를 처리할 수 있어요.
re.M / re.MULTILINE — (^와 $는 아직 설명하지 않았어요. "More metacharacters" 절에서 소개할게요.) 보통 ^는 문자열의 시작에서만, $는 문자열의 끝과 (있으면) 문자열 끝의 줄바꿈 바로 앞에서만 매치됩니다. 이 플래그를 지정하면 ^가 문자열의 시작과 각 줄바꿈 직후의 각 줄 시작에서 매치됩니다. 마찬가지로 $ 메타문자는 문자열 끝에서, 그리고 각 줄의 끝(각 줄바꿈 직전)에서도 매치됩니다.
re.S / re.DOTALL — '.' 특수 문자가 줄바꿈을 포함한 아무 문자에나 매치하게 합니다. 이 플래그가 없으면 '.'는 줄바꿈을 제외한 아무거나 매치해요.
re.A / re.ASCII — \w, \W, \b, \B, \s, \S가 전체 Unicode 매칭 대신 ASCII 전용 매칭을 수행하게 합니다. 이건 Unicode 패턴에만 의미가 있고, 바이트 패턴에서는 무시됩니다.
re.X / re.VERBOSE — 이 플래그는 정규 표현식을 더 형식화할 수 있는 유연성을 주어 더 읽기 쉽게 작성할 수 있게 해 줍니다. 이 플래그를 지정하면 RE 문자열 안의 공백은, 문자 클래스 안에 있거나 이스케이프되지 않은 백슬래시 앞에 있는 경우를 제외하고 무시됩니다. 덕분에 RE를 더 명확하게 정리하고 들여쓰기할 수 있어요. 또 이 플래그는 RE 안에 주석을 넣을 수 있게 해 주는데, 엔진이 무시하는 주석은 문자 클래스 안에 없고 이스케이프되지 않은 백슬래시 앞에 없는 '#'으로 표시됩니다.
예를 들어 re.VERBOSE를 쓰는 RE를 볼게요. 얼마나 읽기 쉬운지 보이죠?
charref = re.compile(r"""
&[#] # Start of a numeric entity reference
(
0[0-7]+ # Octal form
| [0-9]+ # Decimal form
| x[0-9a-fA-F]+ # Hexadecimal form
)
; # Trailing semicolon
""", re.VERBOSE)
장황함(verbose) 설정이 없다면 RE는 이렇게 생겼을 거예요:
charref = re.compile("&#(0[0-7]+"
"|[0-9]+"
"|x[0-9a-fA-F]+);")
위 예시에서는 Python의 문자열 리터럴 자동 연결이 RE를 더 작은 조각으로 쪼개는 데 쓰였지만, 여전히 re.VERBOSE 버전보다 이해하기 어려워요.
더 강력한 패턴 (More pattern power)
지금까지 정규 표현식 기능의 일부만 다뤘어요. 이 절에서는 몇 가지 새 메타문자와, 그룹으로 매치된 텍스트의 일부를 꺼내는 방법을 다룰게요.
더 많은 메타문자들
아직 다루지 않은 메타문자가 있어요. 대부분 이 절에서 다룰게요.
남은 메타문자 중 일부는 **제로 폭 단언(zero-width assertion)**입니다. 이들은 엔진이 문자열을 전진하게 하지 않아요. 대신 문자를 전혀 소비하지 않고 단지 성공하거나 실패할 뿐입니다. 예를 들어 \b는 현재 위치가 단어 경계에 있다는 단언이라서, \b 때문에 위치가 바뀌지 않아요. 즉 제로 폭 단언은 절대 반복하면 안 됩니다. 주어진 위치에서 한 번 매치되면 분명히 무한 번 매치될 수 있으니까요.
|— 대안(alternation), 즉 "또는" 연산자. A와 B가 정규 표현식이면,A|B는 A 또는 B 중 하나와 매치되는 아무 문자열이든 매치해요.|는 다중 문자 문자열을 대안으로 쓸 때 합리적으로 동작하도록 우선순위가 매우 낮습니다.Crow|Servo는'Crow'또는'Servo'를 매치하지,'Cro','w'나'S','ervo'를 매치하는 게 아니에요. 글자 그대로의'|'를 매치하려면\|를 쓰거나[|]처럼 문자 클래스로 감싸세요.^— 줄의 시작에서 매치.MULTILINE플래그를 설정하지 않으면 문자열의 시작에서만 매치돼요.MULTILINE모드에서는 문자열 안의 각 줄바꿈 직후에서도 매치합니다. 예를 들어From이라는 단어를 줄의 시작에서만 매치하고 싶다면 RE는^From입니다.
>>> print(re.search('^From', 'From Here to Eternity'))
<re.Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None
글자 그대로의 '^'를 매치하려면 \^를 쓰세요.
$— 줄의 끝에서 매치. 줄의 끝은 문자열의 끝이거나, 줄바꿈 문자 뒤에 오는 아무 위치로 정의됩니다.
>>> print(re.search('}$', '{block}'))
<re.Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))
<re.Match object; span=(6, 7), match='}'>
글자 그대로의 '$'를 매치하려면 \$를 쓰거나 [$]처럼 문자 클래스로 감싸세요.
\A— 문자열의 시작에서만 매치.MULTILINE모드가 아니면\A와^는 사실상 같아요.MULTILINE모드에서는 둘이 다릅니다:\A는 여전히 문자열의 시작에서만 매치하지만,^는 문자열 안에서 줄바꿈 문자 뒤에 오는 어떤 위치에서도 매치될 수 있어요.\z— 문자열의 끝에서만 매치.\Z—\z와 같음. 옛 Python 버전과의 호환을 위한 것.\b— 단어 경계(word boundary). 단어의 시작이나 끝에서만 매치하는 제로 폭 단언이에요. 단어는 영숫자 문자의 시퀀스로 정의되므로, 단어의 끝은 공백이나 비-영숫자 문자로 표시됩니다.
다음 예시는 class가 완전한 단어일 때만 매치하고, 다른 단어 안에 포함돼 있을 때는 매치하지 않아요.
>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None
이 특별 시퀀스를 쓸 때 기억해야 할 미묘한 점이 둘 있어요. 첫째로, 이건 Python의 문자열 리터럴과 정규 표현식 시퀀스 사이의 최악의 충돌입니다. Python 문자열 리터럴에서 \b는 백스페이스 문자, ASCII 값 8이에요. 원시 문자열을 쓰지 않으면 Python이 \b를 백스페이스로 변환하고, 여러분의 RE는 기대대로 매치되지 않아요. 다음 예제는 이전 RE와 똑같아 보이지만 RE 문자열 앞의 'r'을 뺐어요.
>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))
<re.Match object; span=(0, 7), match='\x08class\x08'>
둘째로, 이 단언이 쓸모없는 문자 클래스 안에서는, Python 문자열 리터럴과의 호환을 위해 \b가 백스페이스 문자를 나타냅니다.
\B— 또 다른 제로 폭 단언으로,\b의 반대예요. 현재 위치가 단어 경계가 아닐 때만 매치합니다.
그룹화 (Grouping)
RE가 매치됐는지 아닌지만 아는 것보다 더 많은 정보를 얻어야 할 때가 많아요. 정규 표현식은 관심 있는 서로 다른 구성 요소와 매치되는 여러 하위 그룹으로 나뉜 RE를 작성해 문자열을 해부하는 데 자주 쓰입니다. 예를 들어 RFC-822 헤더 줄은 헤더 이름과 값으로 나뉘는데, ':'로 구분되지요:
From: [email protected]
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: [email protected]
이건 전체 헤더 줄에 매치되는 정규 표현식을 쓰고, 하나의 그룹은 헤더 이름에, 다른 그룹은 헤더 값에 매치되게 하면 됩니다.
그룹은 '(', ')' 메타문자로 표시돼요. '('와 ')'는 수학 표현식에서의 의미와 거의 같습니다. 안에 담긴 표현식을 함께 묶고, *, +, ?, {m,n} 같은 수량자로 그룹의 내용을 반복할 수 있어요. 예를 들어 (ab)*는 ab를 0번 이상 반복한 것과 매치합니다.
>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)
'(', ')'로 표시된 그룹은 매치한 텍스트의 시작·끝 인덱스도 캡처하는데, 이건 group(), start(), end(), span()에 인자를 넘겨 꺼낼 수 있어요. 그룹은 0부터 번호가 매겨집니다. 그룹 0은 항상 존재하고, 그건 RE 전체니까 매치 객체 메서드들은 모두 기본 인자로 그룹 0을 갖습니다. 나중에 매치한 텍스트의 범위를 캡처하지 않는 그룹을 표현하는 법도 보게 될 거예요.
>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'
하위 그룹은 왼쪽에서 오른쪽으로, 1부터 번호가 매겨집니다. 그룹은 중첩될 수 있어요. 번호를 정하려면 왼쪽에서 오른쪽으로 여는 괄호 문자를 세면 됩니다.
>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'
group()에는 여러 그룹 번호를 한 번에 넘길 수 있는데, 그 경우 해당 그룹들의 값이 담긴 튜플을 반환합니다.
>>> m.group(2,1,2)
('b', 'abc', 'b')
groups() 메서드는 1번부터 그 수만큼의 모든 하위 그룹의 문자열을 담은 튜플을 돌려줍니다.
>>> m.groups()
('abc', 'b')
패턴 안의 **역참조(backreference)**는, 앞선 캡처 그룹의 내용이 문자열의 현재 위치에서도 반드시 발견되어야 한다고 지정할 수 있게 해 줘요. 예를 들어 \1은 그룹 1의 정확한 내용이 현재 위치에서 발견되면 성공하고, 그렇지 않으면 실패합니다. Python의 문자열 리터럴도 숫자 뒤에 백슬래시를 붙여 문자열에 임의의 문자를 포함시키는 것을 지원하므로, RE에 역참조를 넣을 때는 원시 문자열을 쓰는 걸 잊지 마세요.
예를 들어 다음 RE는 문자열에서 두 번 반복된 단어를 감지합니다.
>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'
이런 역참조는 단순히 문자열을 검색할 때는 자주 유용하지 않아요. 이렇게 데이터를 반복하는 텍스트 형식은 드물기 때문이죠. 하지만 문자열 치환을 수행할 때는 아주 유용하다는 걸 곧 알게 될 거예요.
비캡처 그룹과 이름 붙은 그룹
정교한 RE는 관심 있는 부분 문자열을 캡처하거나 RE 자체를 묶고 구조화하기 위해 많은 그룹을 쓸 수 있어요. 복잡한 RE에서는 그룹 번호를 추적하는 게 어려워집니다. 이 문제를 돕는 기능이 두 개 있어요. 둘 다 정규 표현식 확장을 위한 공통 문법을 쓰니, 그걸 먼저 볼게요.
Perl 5는 표준 정규 표현식에 대한 강력한 추가 기능으로 유명해요. 이 새 기능들을 위해 Perl 개발자들은 표준 RE와 혼란스럽게 달라지지 않도록 새 한 글자 메타문자나 \로 시작하는 새 특별 시퀀스를 고르지 못했어요. 예를 들어 &를 새 메타문자로 고른다면, 옛 표현식들은 &가 일반 문자라고 가정하고 \&나 [&]로 이스케이프하지 않았을 거예요.
Perl 개발자들이 고른 해법은 확장 문법으로 (?...)를 쓰는 거였어요. 괄호 바로 뒤의 ?는, 반복할 대상이 없으므로 문법 오류였기 때문에 이건 호환성 문제를 만들지 않았습니다. ? 바로 뒤의 문자들이 어떤 확장인지 나타내요. 그래서 (?=foo)는 어떤 것(긍정 전방탐색 단언)이고 (?:foo)는 다른 것(부분식 foo를 담은 비캡처 그룹)이에요.
Python은 Perl의 확장 몇 가지를 지원하고, Perl의 확장 문법에 자체 확장 문법을 더했어요. 물음표 뒤의 첫 문자가 P라면 그건 Python 고유의 확장임을 알 수 있어요.
일반 확장 문법을 봤으니, 복잡한 RE에서 그룹 작업을 단순화해 주는 기능들로 돌아가 볼게요.
때로는 그룹을 정규 표현식의 일부를 나타내는 데 쓰고 싶지만, 그룹의 내용을 꺼내는 데는 관심이 없을 수 있어요. 비캡처 그룹(non-capturing group) (?:...)을 쓰면 이 사실을 명시적으로 만들 수 있어요. 여기서 ...는 다른 어떤 정규 표현식으로도 바꿀 수 있습니다.
>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()
그룹이 매치한 내용을 꺼낼 수 없다는 점을 빼면, 비캡처 그룹은 캡처 그룹과 정확히 똑같이 동작합니다. 안에 아무거나 넣을 수 있고, * 같은 반복 메타문자로 반복할 수 있고, 다른 그룹(캡처든 비캡처든) 안에 중첩할 수 있어요. (?:...)는 특히 기존 패턴을 수정할 때 유용한데, 다른 모든 그룹의 번호를 바꾸지 않고도 새 그룹을 추가할 수 있기 때문이에요. 캡처 그룹과 비캡처 그룹 사이에 검색 성능 차이는 없다는 점을 언급해 둘게요. 어느 형태도 다른 것보다 빠르지 않아요.
더 중요한 기능은 **이름 붙은 그룹(named group)**입니다. 그룹을 번호로 참조하는 대신 이름으로 참조할 수 있어요. 이름 붙은 그룹의 문법은 Python 고유 확장 중 하나입니다: (?P<name>...). name은 말할 것도 없이 그룹의 이름이에요. 이름 붙은 그룹은 캡처 그룹처럼 정확히 동작하면서, 추가로 그룹에 이름을 연결합니다. 캡처 그룹을 다루는 매치 객체 메서드는 모두, 그룹을 번호로 참조하는 정수든 원하는 그룹 이름을 담은 문자열이든 받아들입니다. 이름 붙은 그룹에도 여전히 번호가 주어지므로, 그룹에 대한 정보를 두 가지 방식으로 꺼낼 수 있어요:
>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'
또한 groupdict()로 이름 붙은 그룹을 사전으로 꺼낼 수 있어요:
>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe')
>>> m.groupdict()
{'first': 'Jane', 'last': 'Doe'}
이름 붙은 그룹은 번호를 기억하는 대신 쉽게 기억되는 이름을 쓰게 해 주니 참 편리해요. imaplib 모듈에서 가져온 예시 RE를 볼게요:
InternalDate = re.compile(r'INTERNALDATE "'
r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
r'(?P<year>[0-9][0-9][0-9][0-9])'
r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
r'"')
그룹 9를 꺼내야 한다고 기억하는 것보다 m.group('zonem')을 꺼내는 게 분명 훨씬 쉬워요.
(...)\1 같은 표현식에서 역참조 문법은 그룹의 번호를 가리켜요. 당연히 번호 대신 그룹 이름을 쓰는 변형도 있죠. 이것도 Python 확장입니다: (?P=name)은 name이라는 그룹의 내용이 현재 지점에서 다시 매치되어야 함을 나타냅니다. 두 번 반복되는 단어를 찾는 정규 표현식 \b(\w+)\s+\1\b는 \b(?P<word>\w+)\s+(?P=word)\b로도 쓸 수 있어요:
>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'
전방탐색 단언 (Lookahead assertions)
또 다른 제로 폭 단언은 **전방탐색 단언(lookahead assertion)**이에요. 전방탐색 단언은 긍정/부정 형태가 모두 있고, 이렇게 생겼습니다:
(?=...)— 긍정 전방탐색 단언(positive lookahead assertion). 여기서...로 나타낸 포함된 정규 표현식이 현재 위치에서 성공적으로 매치되면 성공하고, 그렇지 않으면 실패해요. 하지만 포함된 표현식이 시도된 다음에는 매칭 엔진이 전혀 전진하지 않아요. 패턴의 나머지는 단언이 시작된 바로 그 위치에서 시도됩니다.(?!...)— 부정 전방탐색 단언(negative lookahead assertion). 긍정 단언의 반대예요. 포함된 표현식이 문자열의 현재 위치에서 매치되지 않으면 성공합니다.
구체적으로 만들기 위해, 전방탐색이 유용한 경우를 봅시다. 파일명을 매치하고 .으로 구분된 base 이름과 확장자로 쪼개는 간단한 패턴을 생각해 보세요. 예를 들어 news.rc에서 news는 base 이름이고 rc는 파일명의 확장자예요.
이걸 매치하는 패턴은 아주 간단합니다: .*[.].*$. .는 메타문자라 특별 취급해야 하므로 문자 클래스 안에 넣어 그 특정 문자만 매치하게 한 점에 주목하세요. 또 끝에 붙은 $도 주목하세요. 이건 문자열의 나머지가 모두 확장자에 포함되게 하기 위한 것이에요. 이 정규 표현식은 foo.bar, autoexec.bat, sendmail.cf, printers.conf와 매치됩니다.
이제 문제를 조금 복잡하게 해 봅시다. 확장자가 bat가 아닌 파일명을 매치하고 싶다면 어떨까요? 몇 가지 잘못된 시도를 볼게요:
.*[.][^b].*$ — 첫 번째 시도는 확장자의 첫 문자가 b가 아니어야 한다고 요구해 bat를 배제하려 해요. 그런데 이 패턴이 foo.bar도 매치하지 않기 때문에 틀렸어요.
.*[.]([^b]..|.[^a].|..[^t])$ — 첫 해법을 고치려고 다음 중 하나가 매치되길 요구하다 보니 표현식이 더 지저분해집니다: 확장자의 첫 문자가 b가 아니다, 둘째 문자가 a가 아니다, 또는 셋째 문자가 t가 아니다. 이건 foo.bar를 받아들이고 autoexec.bat은 거부하지만, 세 글자 확장자를 요구하므로 sendmail.cf처럼 두 글자 확장자를 가진 파일명은 받아들이지 못해요. 이걸 고치려고 패턴을 또 복잡하게 만들어 볼게요.
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$ — 세 번째 시도에서는 sendmail.cf 같은 세 글자보다 짧은 확장자 매치를 허용하려고 둘째·셋째 글자를 모두 선택 사항으로 만들었어요.
이제 패턴이 정말 복잡해져서 읽고 이해하기 어려워졌어요. 게다가 문제가 바뀌어 bat와 exe 둘 다 확장자로 배제하고 싶다면 패턴은 훨씬 더 복잡하고 혼란스러워질 거예요.
부정 전방탐색이 이 모든 혼란을 가로지릅니다: .*[.](?!bat$)[^.]*$. 부정 전방탐색이 뜻하는 바는, 이 지점에서 표현식 bat가 매치되지 않으면 패턴의 나머지를 시도하고, bat$가 매치되면 전체 패턴이 실패한다는 거예요. 끝의 $는 확장자가 bat로 시작할 뿐인 sample.batch 같은 것을 허용하기 위해 필요합니다. [^.]*는 파일명에 점이 여러 개일 때 패턴이 동작하게 합니다. 다른 파일 확장자를 배제하는 것은 이제 쉬워요. 단언 안에 대안으로 그냥 추가하면 됩니다. 다음 패턴은 bat 또는 exe로 끝나는 파일명을 배제합니다: .*[.](?!bat$|exe$)[^.]*$.
문자열 수정하기 (Modifying strings)
지금까지는 정적 문자열에 대해 검색만 수행했어요. 정규 표현식은 다음 패턴 메서드를 써서 문자열을 다양한 방식으로 수정하는 데도 흔히 쓰입니다.
| 메서드/속성 (Method/Attribute) | 용도 (Purpose) |
|---|---|
split() |
RE가 매치되는 곳마다 문자열을 쪼개 리스트로 만듦 |
sub() |
RE가 매치되는 모든 부분 문자열을 찾아 다른 문자열로 교체 |
subn() |
sub()와 같은 일을 하지만 새 문자열과 교체 횟수를 반환 |
문자열 쪼개기 (Splitting strings)
패턴의 split() 메서드는 RE가 매치되는 곳마다 문자열을 쪼개, 조각들의 리스트를 돌려줍니다. 문자열의 split() 메서드와 비슷하지만, 쪼갤 구분자에서 훨씬 더 큰 일반성을 제공해요. 문자열 split()은 공백이나 고정 문자열로만 쪼갤 수 있습니다. 예상대로 모듈 수준 함수 re.split()도 있어요.
.split(string[, maxsplit=0]) — 정규 표현식의 매치로 string을 쪼갭니다. RE에 캡처 괄호를 쓰면 그 내용도 결과 리스트의 일부로 반환돼요. maxsplit이 0이 아니면 최대 maxsplit 번의 쪼갬이 수행됩니다.
maxsplit에 값을 넘겨 쪼갬 횟수를 제한할 수 있어요. maxsplit이 0이 아니면 최대 maxsplit 번 쪼개고, 문자열의 나머지는 리스트의 마지막 요소로 반환됩니다. 다음 예시에서 구분자는 비-영숫자 문자의 어떤 시퀀스든 됩니다.
>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']
때로는 구분자 사이의 텍스트뿐 아니라 구분자가 무엇이었는지도 알아야 할 때가 있어요. RE에 캡처 괄호를 쓰면 그 값들도 리스트의 일부로 반환됩니다. 다음 호출들을 비교해 보세요:
>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']
모듈 수준 함수 re.split()은 사용할 RE를 첫 인자로 추가한다는 점만 빼면 같아요.
>>> re.split(r'[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split(r'([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split(r'[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']
검색하고 교체하기 (Search and replace)
또 다른 흔한 작업은 패턴의 모든 매치를 찾아 다른 문자열로 교체하는 거예요. sub() 메서드는 교체 값(문자열 또는 함수일 수 있음)과 처리할 문자열을 받습니다.
.sub(replacement, string[, count=0]) — string에서 RE의 가장 왼쪽, 겹치지 않는 발생들을 replacement로 교체해 얻은 문자열을 반환합니다. 패턴을 찾지 못하면 string이 변경 없이 반환돼요. 선택 인자 count는 교체할 패턴 발생의 최대 횟수이고, 음이 아닌 정수여야 합니다. 기본값 0은 모든 발생을 교체한다는 뜻이에요.
sub() 메서드의 간단한 예시를 볼게요. 색 이름을 colour라는 단어로 교체합니다:
>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'
subn() 메서드는 같은 일을 하지만, 새 문자열 값과 수행된 교체 횟수를 담은 2-튜플을 반환합니다:
>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)
빈 매치(empty match)는 이전 빈 매치에 인접하지 않을 때만 교체됩니다.
>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'
replacement가 문자열이면 그 안의 백슬래시 이스케이프가 처리됩니다. 즉 \n은 하나의 줄바꿈 문자로, \r은 캐리지 리턴으로 변환되는 식이에요. \& 같은 알 수 없는 이스케이프는 그대로 둡니다. \6 같은 역참조는 RE에서 대응하는 그룹이 매치한 부분 문자열로 교체됩니다. 이렇게 하면 원본 텍스트의 일부를 결과 교체 문자열에 포함시킬 수 있어요.
이 예시는 section 뒤에 {, }로 감싸인 문자열이 오는 패턴을 매치하고, section을 subsection으로 바꿉니다:
>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'
(?P<name>...) 문법으로 정의한 이름 붙은 그룹을 참조하는 문법도 있어요. \g<name>은 name이라는 그룹이 매치한 부분 문자열을 쓰고, \g<number>는 해당 그룹 번호를 씁니다. 따라서 \g<2>는 \2와 같지만, \g<2>0 같은 교체 문자열에서는 모호하지 않아요. (\20은 그룹 2 다음에 글자 그대로의 '0'이 오는 것으로가 아니라, 그룹 20에 대한 참조로 해석됩니다.) 다음 치환들은 모두 동등하고, 교체 문자열의 세 변형을 모두 사용합니다.
>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'
replacement는 함수일 수도 있는데, 그러면 훨씬 더 많은 제어가 가능해요. 함수라면 pattern의 겹치지 않는 모든 발생마다 호출됩니다. 각 호출에서 함수는 그 매치에 대한 매치 객체 인자를 받고, 이 정보를 사용해 원하는 교체 문자열을 계산해 반환하면 됩니다.
다음 예시에서 교체 함수는 십진수를 16진수로 변환합니다:
>>> def hexrepl(match):
... "Return the hex string for a decimal number"
... value = int(match.group())
... return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'
모듈 수준 함수 re.sub()를 쓸 때는 패턴이 첫 인자로 넘겨집니다. 패턴은 객체나 문자열로 제공될 수 있어요. 정규 표현식 플래그를 지정해야 한다면 첫 매개변수로 패턴 객체를 쓰거나, 패턴 문자열에 임베디드 수정자를 써야 해요. 예를 들어 sub("(?i)b+", "x", "bbbb BBBB")는 'x x'를 반환합니다.
흔한 문제들 (Common problems)
정규 표현식은 어떤 애플리케이션에는 강력한 도구지만, 어떤 면에서는 그 동작이 직관적이지 않아 때로 기대한 대로 동작하지 않아요. 이 절에서는 가장 흔한 함정 몇 가지를 짚어 줄게요.
문자열 메서드를 쓰세요
때로는 re 모듈을 쓰는 게 실수일 수 있어요. 고정 문자열이나 단일 문자 클래스를 매치 중이고, IGNORECASE 플래그 같은 re 기능을 전혀 쓰지 않는다면 정규 표현식의 완전한 힘은 필요하지 않을 수 있어요. 문자열에는 고정 문자열로 작업을 수행하는 메서드가 여럿 있고, 보통 훨씬 더 빠릅니다. 그 구현이 크고 더 일반적인 정규 표현식 엔진 대신, 그 목적에 맞게 최적화된 작은 C 루프 하나이기 때문이에요.
한 예로 고정 문자열 하나를 다른 것으로 교체하는 걸 들 수 있어요. 예를 들어 word를 deed로 교체한다고 해요. re.sub()가 쓸 함수처럼 보이지만, replace() 메서드를 고려해 보세요. replace()는 word를 다른 단어 안에서도 교체해 swordfish를 sdeedfish로 만들지만, 순진한 RE word도 그렇게 했을 거란 점에 주목하세요. (단어의 일부에 치환을 수행하는 걸 피하려면 word의 양쪽에 단어 경계가 필요하도록 패턴이 \bword\b여야 해요. 이건 replace()의 능력을 넘어서는 작업이죠.)
또 다른 흔한 작업은 문자열에서 한 문자의 모든 발생을 지우거나 다른 한 문자로 교체하는 거예요. re.sub('\n', ' ', S) 같은 걸로 할 수도 있지만, translate()가 두 작업을 모두 할 수 있고 어떤 정규 표현식 연산보다 빠를 거예요. 간단히 말해, re 모듈을 쓰기 전에 더 빠르고 단순한 문자열 메서드로 문제를 풀 수 없는지 먼저 고려하세요.
match() vs search()
match() 함수는 RE가 문자열의 시작에서 매치되는지만 검사하는 반면, search()는 문자열을 앞으로 훑으며 매치를 찾아요. 이 구분을 염두에 두는 게 중요합니다. 기억하세요, match()는 0에서 시작하는 성공적인 매치만 보고하고, 매치가 0에서 시작하지 않으면 보고하지 않아요.
>>> print(re.match('super', 'superstition').span())
(0, 5)
>>> print(re.match('super', 'insuperable'))
None
반면 search()는 문자열을 전진하며 처음 찾은 매치를 보고합니다.
>>> print(re.search('super', 'superstition').span())
(0, 5)
>>> print(re.search('super', 'insuperable').span())
(2, 7)
때로는 re.match()를 계속 쓰고 RE 앞에 .*를 그냥 붙이고 싶은 유혹이 들 거예요. 그 유혹을 참고 re.search()를 쓰세요. 정규 표현식 컴파일러는 매치를 찾는 과정을 빠르게 하기 위해 RE를 어느 정도 분석합니다. 그런 분석 중 하나는 매치의 첫 문자가 무엇이어야 하는지 알아내는 거예요. 예를 들어 Crow로 시작하는 패턴은 'C'로 시작해야 매치됩니다. 이 분석 덕에 엔진은 첫 문자를 찾으러 문자열을 빠르게 훑고, 'C'를 찾았을 때만 전체 매치를 시도할 수 있어요. .*를 추가하면 이 최적화를 무너뜨려, 문자열 끝까지 훑은 뒤 RE의 나머지 매치를 위해 백트래킹해야 합니다. re.search()를 쓰세요.
탐욕적 vs 비탐욕적
a*처럼 정규 표현식을 반복할 때의 결과 동작은 가능한 한 많은 패턴을 소비하는 거예요. 이 사실은 HTML 태그를 둘러싼 꺾쇠괄호 같은 쌍을 이루는 구분자를 매치하려 할 때 자주 여러분을 물어요. 단일 HTML 태그를 매치하는 순진한 패턴은 .*의 탐욕적 성질 때문에 동작하지 않습니다.
>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span())
(0, 32)
>>> print(re.match('<.*>', s).group())
<html><head><title>Title</title>
RE는 '<html>'의 '<'를 매치하고, .*가 문자열의 나머지를 소비해요. 그런데 RE에는 아직 남은 게 있고, >는 문자열 끝에서 매치될 수 없으므로 정규 표현식 엔진은 >의 매치를 찾을 때까지 문자 하나씩 백트래킹해야 해요. 최종 매치는 '<html>'의 '<'에서 '</title>'의 '>'까지 뻗어서, 여러분이 원하는 게 아니에요.
이 경우 해결책은 가능한 한 적은 텍스트를 매치하는 비탐욕적 수량자 *?, +?, ??, {m,n}?를 쓰는 거예요. 위 예시에서 '>'는 첫 '<'가 매치된 직후에 시도되고, 실패하면 엔진이 한 번에 한 문자씩 전진하며 매 단계마다 '>'를 다시 시도합니다. 그러면 딱 맞는 결과가 나와요:
>>> print(re.match('<.*?>', s).group())
<html>
(참고: 정규 표현식으로 HTML이나 XML을 파싱하는 건 고통스러워요. 빠르고 대충 만든 패턴이 흔한 경우는 처리하겠지만, HTML과 XML에는 명백한 정규 표현식을 깨뜨리는 특수한 경우가 있어요. 가능한 모든 경우를 처리하는 정규 표현식을 다 쓸 때쯤이면 패턴이 아주 복잡해집니다. 그런 작업에는 HTML이나 XML 파서 모듈을 쓰세요.)
re.VERBOSE 사용하기
이쯤이면 정규 표현식이 아주 간결한 표기법이지만 그리 읽기 좋지는 않다는 걸 알아챘을 거예요. 중간 정도 복잡도의 RE는 백슬래시, 괄호, 메타문자들의 길다란 모음이 되어 읽고 이해하기 어려워집니다.
그런 RE에는 정규 표현식을 컴파일할 때 re.VERBOSE 플래그를 지정하는 게 도움이 될 수 있어요. 정규 표현식을 더 명확하게 형식화할 수 있게 해 주기 때문이에요.
re.VERBOSE 플래그는 몇 가지 효과가 있습니다. 문자 클래스 안에 있지 않은 정규 표현식 안의 공백은 무시됩니다. 즉 dog | cat 같은 표현식이 덜 읽기 쉬운 dog|cat과 동등해지지만, [a b]는 여전히 'a', 'b', 또는 공백 문자와 매치됩니다. 게다가 RE 안에 주석을 넣을 수도 있는데, 주석은 # 문자부터 다음 줄바꿈까지 뻗어요. 삼중 따옴표 문자열과 함께 쓰면 RE를 더 깔끔하게 형식화할 수 있습니다:
pat = re.compile(r"""
\s* # Skip leading whitespace
(?P<header>[^:]+) # Header name
\s* : # Whitespace, and a colon
(?P<value>.*?) # The header's value -- *? used to
# lose the following trailing whitespace
\s*$ # Trailing whitespace to end-of-line
""", re.VERBOSE)
이건 다음보다 훨씬 읽기 쉬워요:
pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")
피드백 (Feedback)
정규 표현식은 복잡한 주제예요. 이 문서가 이해에 도움이 됐나요? 명확하지 않은 부분이나 여기 다루지 않은 문제를 겪은 게 있었나요? 그렇다면 개선 제안을 이슈 트래커로 보내 주세요.
정규 표현식에 관한 가장 완전한 책은 거의 확실히 Jeffrey Friedl의《Mastering Regular Expressions》(O'Reilly 출판)예요. 아쉽게도 이 책은 독점적으로 Perl과 Java 계열의 정규 표현식에 집중하고 Python 자료는 전혀 없어서, Python 프로그래밍의 레퍼런스로는 쓸모가 없을 거예요. (초판은 Python의 지금은 제거된 regex 모듈을 다뤘는데, 별 도움이 안 됩니다.) 도서관에서 빌려 보는 걸 고려해 보세요.
더 알아보기 (Learn more)
re모듈 라이브러리 문서 — 함수·패턴·매치 객체 전체 레퍼런스- Python 공식 문서: Regular expression HOWTO