728x90
반응형

쿠클링(A.M. Kuchling)

한글판 johnsonj 2003.11.25

요약:

이 문서는 파이썬에서 re 모듈을 가지고 정규 표현식을 사용하는 법에 대한 개론 튜토리얼이다. 라이브러리 참조서보다 좀 더 부드럽게 소개한다.

이 문서는 http://www.amk.ca/python/howto에서 얻을 수 있다.

 



내용

1 들어가는 말

re 모듈이 파이썬 1.5에 추가되어, 펄-스타일의 정규 표현식 패턴을 제공한다. 이전 버전의 파이썬에서는 regex 모듈이 따라왔는데, 이는 이맥스-스타일의 패턴을 제공한다. 이맥스-스타일의 패턴은 약간 읽기가 더 어렵고 그렇게 많은 특징을 제공하지 않으므로, 새로운 코드를 작성할 때 regex 모듈을 사용해야 할 이유가 별로 없다. 비록 예전 코드에서는 사용하는 것을 마주할 수도 있겠지만 말이다.

정규 표현식(RE)은 본질적으로, 작고 고도로 전문화된 프로그래밍 언어로서 파이썬에 임베드 되어서 re 모듈을 통하여 사용할 수 있다. 이 작은 언어를 사용하면, 일치시키고 싶은 문자열 집합에 대하여 규칙을 지정할 수 있다; 이 집합에는 영어 문장이나, 전자우편 주소, 또는 TeX 명령어, 원하는 무엇이든 포함된다. 그러면 이렇게 질문할 수 있다 ``이 문자열이 패턴에 일치하는가?'', 또는 ``이 문자열에서 패턴에 일치하는 것이 있는가?''. RE를 사용하면 또한 문자열을 변경하거나 여러가지 방식으로 가를 수 있다.

정규 표현식 패턴은 일련의 바이트코드로 컴파일되고 C로 작성된 일치 엔진에 의해서 실행된다. 수준높게 사용하려면, 이 엔진이 어떻게 주어진 RE를 실행하는지 주의를 기울여서, 더 빨리 실행되는 바이트코드를 생산하도록 RE를 작성할 필요가 있다. 최적화는 이 일치 엔진의 내부에 대해서 잘 알고 있어야 할 필요가 있기 때문에 이 문서에서 다루지 않는다.

정규 표현식 언어는 상대적으로 작고 제한적이다. 그래서 정규 표현식을 사용한다고 하더라도 모든 문자열 작업이 가능한 것은 아니다. 또 정규 표현식으로 처리가 가능하지만, 그 표현식이 너무나도 복잡한 작업도 있다. 이런 경우, 파이썬 코드를 작성해서 처리를 하는 것이 더 좋다; 정교한 정규 표현식보다 파이썬 코드가 느리겠지만, 아마도 더 이해가 쉬울 것이다.

2 간단한 패턴

먼저 가장 간단한 정규 표현식을 배워보자. 정규 표현식은 문자열 처리에 사용되기 때문에, 가장 일반적인 작업부터 시작하자: 문자열을 일치시켜 보자.

정규 표현식 아래에 숨겨진 더 자세한 컴퓨터 공학적 설명이 (결정 그리고 비-결정 유한 오토마타) 필요하면, 컴파일러의 작성에 관한 교과서를 아무거나 하나 참조하자.

2.1 문자 일치시키기

대부분의 문자와 기호는 그냥 자신을 일치시킨다. 예를 들어, 정규 표현식 test는 문자열 "test"에 정확하게 일치한다. (대소문자-구분 모드를 활성화하면 이 RE를 "Test"나 "TEST"에도 일치시킬 수 있다; 이에 관해서는 나중에 더 자세히 다룬다.)

이 규칙에는 예외가 몇가지 있다; 어떤 문자들은 특별해서, 자신과 일치하지 않는다. 대신에, 그런 문자들은 무언가 정상을 벗어난 어떤 것들이 일치해야 한다는 것을 나타내거나, 또는 반복함으로써 RE의 다른 부분에 영향을 미친다. 이 문서의 대부분은 다양한 메타문자들과 그 문자들이 하는 역할을 논의한다.

다음은 메타문자의 완벽한 목록이다; 그 의미를 지금부터 다루어 보겠다.

. ^ $ * + ? { [ ] \ | ( )

살펴 볼 첫 번째 메타문자는 "[" 그리고 "]"이다. 이 메타문자는 문자 부류를 지정하는데 사용되는데, 문자 부류란 일치시키고 싶은 문자 집합이다. 문자들은 따로따로 나열될 수 있다. 또는 일정 범위의 문자들을 나타내려면 문자 두 개를 주고 그 사이를 "-"로 가르면 된다. 예를 들면, [abc]는 다음 "a", "b", 또는 "c" 어느 문자와도 일치한다; 이는 [a-c]과 동일하며, 범위를 사용해서 같은 문자 집합을 표현한다. 오직 소문자와만 일치시키고 싶으면, RE는 [a-z]와 같이 된다.

메타문자는 부류안에서는 작용하지 않는다. 예를 들어, [akm$]는 "a", "k", "m", 또는 "$"의 어느 문자와도 일치한다; 일반적으로 "$"는 메타문자이지만, 문자 부류 안에서는 그 특별한 본성을 잃는다.

여집합을 지정해서 범위 안에 없는 문자에도 일치시킬 수 있다. 이는 부류에서 첫 문자로 "^"를 포함하여 나타낸다; 다른 위치에서 "^"가 나타나면 그냥 "^" 문자에 일치된다. 예를 들어, [^5]는 "5"를 제외하고 어떤 문자에도 일치한다.

아마도 가장 중요한 메타문자는 역사선 "\"일 것이다. 파이썬 기호문자에서 역사선 뒤에 다양한 문자들이 따르면서 다양한 특수 연속열을 나타낼 수 있다. 또는 모든 메타문자들을 피시시키는데에도 사용되므로 여전히 패턴으로 일치시킬 수 있다; 예를 들어, "[" 또는 "\"를 일치시키고 싶으면, 앞에 역사선을 먼저 두어서 그의 특별한 의미를 없앨 수 있다: \[ 또는 \\.

특수 연속열중에 "\"로 시작하는 연속열은 자주 쓸모가 있는 문자들이 미리정의된 집합을 나타내는데, 숫자문자, 기호 집합, 또는 공백이 아닌 어떤 것이든지 된다. 다음과 같이 미리 정의된 특수 연속열을 사용할 수 있다:

\d
10진 숫자와 일치한다; 다음 부류와 동일 [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)가 있어서 새줄문자조차도 일치시킬 수 있다. "."는 ``어떤 문자''라도 일치시키고 싶을 때 자주 사용된다.

2.2 반복적인 작업

다양한 문자 집합에 일치시키는 능력은 정규 표현식이 할 수 있는 첫 번째 능력으로 이는 문자열에 메쏘드에서는 아직 가능하지 않은 능력이다. 그렇지만, 그 정도가 regexes의 능력에 추가된 전부라면, 별로 크게 진보된 것이 아닐 것이다. 또다른 능력은 RE의 일부를 몇 번이라도 반복하도록 지정할 수 있다는 것이다.

일을 반복하는데 사용되는 첫 번째 매개변수는 *이다. *는 기호 문자"*"에 일치되지 않는다; 대신에, 이전의 문자가 정확하게 한번 말고 0회 이상 일치될 수 있도록 지정할 수 있다.

예를 들어, ca*t는 "ct" (0개의 "a" 문자), "cat" (1개의 "a" 문자), "caaat" (3개의 "a" 문자), 등등에 일치한다. RE 엔진은 C의 int 유형에서 기원하는 다양한 내부 제한이 있다. 이 제한 때문에 20억개가 넘는 "a" 문자에는 일치하지 않는다; 그 정도로 큰 문자열을 구성하려면 메모리가 충분하지 않을 것이므로, 그 한계에 봉착해서는 안된다.

*와 같은 반복은 탐욕적(greedy)이라고 한다; RE를 반복할 때, 일치 엔진은 가능하면 많이 일치시키려고 시도할 것이다. 패턴의 나중 부분이 일치하지 않으면, 일치 엔진은 다시 돌아와 몇 번의 반복을 다시 시도한다.

단계별 예제로 이 점을 보다 명확하게 밝혀 보겠다. a[bcd]*b라는 표현식을 생각해 보자. 이는 기호 "a"와, 부류 [bcd]에서의 0회 이상의 기호와 일치하고, 그리고 마지막으로 "b"로 끝난다. 이제 이 RE를 문자열 "abcbd"에 일치시켜 본다고 생각해 보자.

단계  일치됨  해설 
1 a RE에서 a가 일치한다.
2 abcbd 엔진은 [bcd]*에 일치하고, 최대한 멀리 가서, 문자열의 끝에 이른다.
3 Failure 엔진은 b에 일치하려고 시도하지만, 현재 위치가 문자열의 끝이므로, 실패한다.
4 abcb 다시 돌아온다, [bcd]*가 하나 이하의 문자에 일치한다.
5 Failure 다시 b를 시도하지만, 현재 위치가 마지막 문자이다. 즉 "d"이다.
6 abc 다시 돌아온다, 그래서 [bcd]*는 오직 "bc"에만 일치한다.
6 abcb 다시 b를 시도한다. 그러나 이 번에는 현재 위치의 문자가 "b"이고, 그래서 성공한다.

이제 RE의 마지막에 다다랐다. 그리고 "abcb"에 일치했다. 이는 어떻게 일치 엔진이 먼저 최대한 멀리 가는지 보여준다. 만약 아무 일치도 발견하지 못하면 순차적으로 다시 돌아와 RE의 남은 부분을 계속해서 반복적으로 시도한다. [bcd]*에 대하여 어떤 일치도 발견하지 못할 때까지 돌아 올 것이다. 그리고 잇달아 실패하면, 일치 엔진은 그 문자열이 RE에 전혀 일치하지 않는다고 결론을 내릴 것이다.

또다른 반복 메타문자는 +인데, 이는 1회 이상 일치한다. *+의 차이에 주의를 기울이자; *0회 이상 일치하므로, 그래서 반복되는 것이 전혀 존재하지 않을 수도 있지만, 반면에 +는 적어도 한 번의 출현을 요구한다. 비슷한 예를 들어 보면, ca+t는 "cat" (1 "a"), "caaat" (3 "a"'s)에 일치하지만, "ct"에는 일치하지 않는다.

반복 수식자가 두 가지 더 있다. 물음표 문자 ?는 0회 또는 1회 일치한다; 즉 무언가가 선택적인 것으로 표식설정되어 있는 것이라고 간주해도 좋다. 예를 들어, home-?brew는 "homebrew" 또는 "home-brew"에 일치한다.

가장 복잡한 반복 수식자는 {m,n}인데, 여기에서 mn은 십진 정수이다. 이 수식자는 최소 m회의 반복과, 최대 n회가 반복됨을 의미한다. 예를 들어, a/{1,3}b는 "a/b", "a//b", 그리고 "a///b"에 일치된다. "ab"에는 일치하지 않는데, 이는 사선이 없기 때문이며, 또는 "a////b"에도 일치하지 않는데, 이는 사선이 네개이기 때문이다.

m 또는 n을 생략해도 좋다; 이 경우에, 생략된 값에 대하여 합리적인 값이 추정된다. m을 생략하면 0으로 번역되는데, 반면에 n을 생략하면 최대 무한 값이 된다 -- 실제로, 앞에서 20억이 한계라고 언급했지만, 그 정도면 무한이라고 해도 좋을 것 같다.

간결하게 처리하기를 좋아하는 독자라면 나머지 다른 수식자가 모두 다음 표기법으로 표현될 수 있다는 것을 눈치챘을 것이다. {0,}*이고, {1,}+와 동등하며, 그리고 {0,1}?와 똑같다. 더 짧고 읽기 편하기 때문에 가능하면 *, +, 또는 ?를 사용하는 편이 좋다.

3 정규 표현식 사용하기

간단한 몇가지 정규 표현식을 둘러 보았다. 그렇하면 어떻게 실제로 정규 표현식을 파이썬에서 사용하는가? re 모듈이 정규 표현식에 대한 인터페이스를 제공하여 주므로, RE를 객체로 컴파일해서 그들에 대해 일치를 수행할 수 있다.

3.1 정규 표현식 컴파일하기

정규 표현식은 RegexObject 실체로 컴파일된다. 이는 다양한 연산을 위한 메쏘드를 가지고 있어서 패턴 일치를 찾거나 문자열 대치를 수행한다.

>>> import re
>>> p = re.compile('ab*')
>>> print p
<re.RegexObject instance at 80b4150>

re.compile()은 또한 선택적인 표식(flags) 인수를 받는다. 이 인수를 사용하면 다양한 특수 특징들과 구문을 사용할 수 있다. 나중에 사용가능한 설정값들을 검토해 보겠지만, 현재로서는 한가지 예제만으로도 충분할 것이다:

>>> p = re.compile('ab*', re.IGNORECASE)

RE는 re.compile()에 문자열로 건네진다. RE는 정규 표현식이 코어 파이썬 언어의 일부가 아니기 때문에 문자열로 처리된다. 그리고 정규 표현식을 표현하는데 어떤 특별한 구문도 만들어지지 않는다. (RE가 전혀 필요하지 않은 어플리케이션도 있어서, 정규 표현식을 포함했다고 자랑할 필요가 없다.) 대신, re 모듈은 그냥 파이썬에 포함된 C 확장 모듈이다. 마치 socket 모듈이나 zlib 모듈처럼 말이다.

문자열로 RE를 표현하면 파이썬 언어는 더 간단하지만, 단점이 하나 있는데 다음 섹션의 주제로 삼겠다.

3.2 고민거리 역사선

이전에 언급한 바와 같이, 정규 표현식은 역사선 문자 ("\")를 사용하여 그의 특별한 의미를 요청하지 않고 특별한 형태를 나타내거나 특별한 문자를 사용하도록 허용한다. 이는 파이썬이 같은 문자를 같은 목적으로 문자열 기호문자에 사용하는 것과 충돌한다.

RE를 문자열 "\section"에 일치시켜 보고 싶다고 하자, 이는 LATEX 파일에서 봄직하다. 프로그램 코드에 무엇을 쓸지 가늠하려면, 먼저 일치시키기를 원하는 문자열로 시작하자. 다음으로, 역사선을 앞에 두어서 역사선과 기타 메타문자들을 피신시키고 "\\section" 문자열을 만들어야 한다. re.compile()으로 건네야 하는 결과 문자열은 반드시 \\section이어야 한다. 그렇지만, 이를 파이썬 문자열 기호문자로 표현하려면, 역사선 두 개 모두를 또다시 피신시켜야 한다.

문자  단계 
\section 일치시킬 텍스트 문자열
\\section re.compile을 위해 피신시킨 역사선
"\\\\section" 문자열 기호문자를 위해 피신시킨 역사선

간단히 말해, 기호문자 역사선을 일치시키려면, RE 문자열로 '\\\\'라고 써야 하는데, 정규 표현식은 "\\"이어야 하고, 각 역사선은 정규 파이썬 문자열 기호문자 안에서 "\\"라고 표현되어야 하기 때문이다. RE에서 역사선을 반복적으로 사용해야 한다면, 이는 수 많은 역사선의 반복으로 이어져야 하고 결과 문자열을 이해하기 어렵게 된다.

해결책은 파이썬의 날 문자열 표기법을 정규 표현식에 사용하는 것이다; 역사선은 "r"이 접두사로 된 문자열 기호문자 안에서는 어떤 특별한 방법으로 다루어지지 않는다, 그래서 r"\n"은 두개의 문자 문자열로서 "\"와 "n"를 담고 있는데, 반면에 "\n"은 한개짜리 문자열로서 새줄문자를 담고 있다. 아주 자주 정규 표현식은 파이썬 코드에서 이런 날 문자열 표기법으로 표현된다.

정규 표현식  날 문자열 
"ab*" r"ab*"
"\\\\section" r"\\section"
"\\w+\\s+\\1" r"\w+\s+\1"

3.3 일치시켜 보기

컴파일된 정규 표현식을 나타내는 객체를 확보했다고 하면, 그것으로 무엇을 할 것인가? RegexObject 실체는 여러 메쏘드와 속성을 가지고 있다. 가장 중요한 것들만 여기에서 다루어 보겠다; 완벽한 목록은 라이브러리 참조서를 참고하자.

메쏘드/속성  목적 
match() 문자열의 처음에서 RE가 일치하는지 결정한다.
search() 문자열을 훓어서, RE가 일치하는 위치를 찾는다.
findall() RE가 일치하는 곳의 모든 하부문자열을 찾아서, 그 문자열들을 리스트로 반환한다.
finditer() RE가 일치하는 곳의 모든 하부문자열을 찾아서, 그 문자열들을 반복자로 반환한다.

match()search()는 아무 일치도 발견하지 못하면 None을 반환한다. 성공적으로 발견하면 MatchObject 실체가 반환되는데, 여기에는 그 일치에 관한 정보가 담기어 있다: 어디에서 시작하고 끝나는지, 일치된 하부문자열, 등등.

re 모듈을 가지고 상호대화적으로 실험해 보면 이에 대하여 알 수 있다. Tkinter를 사용할 수 있으면, 파이썬 배포본에 포함된 데모 프로그램인 Tools/scripts/redemo.py를 보고 싶을 것이다. 이 데모 프로그램에서 RE와 문자열을 입력하면, RE가 일치하는지 실패하는지 화면에 보여준다. redemo.py는 복잡한 RE를 디버그하고 싶을 때 아주 쓸모가 있다. 필 슈바르쯔(Phil Schwartz)의 Kodos도 또한 상호대화적인 도구로서 RE 패턴을 테스트하고 개발하는데 사용된다. 이 HOWTO는 표준 파이썬을 예제로 사용할 것이다.

먼저, 파이썬을 실행하고, re 모듈을 수입한 다음, RE를 컴파일 한다:

Python 2.2.2 (#1, Feb 10 2003, 12:57:01)
>>> import re
>>> p = re.compile('[a-z]+')
>>> p
<_sre.SRE_Pattern object at 80c3c28>

이제, 다양한 문자열을 RE [a-z]+에 일치시켜 볼 수 있다. 빈 문자열은 절대로 일치되지 않는데, 왜냐하면 +는 '1회 이상의 반복'을 의미하기 때문이다. match()는 이 경우에 None을 반환할 것이다. 그래서 파이썬에는 아무것도 인쇄되지 않는다. 이 점을 명확히 하기 위하여 명시적으로 match()의 결과를 인쇄해 볼 수 있다.

>>> p.match("")
>>> print p.match("")
None

이제, 일치 될 만한 문자열, 예를 들어 "tempo"와 같은 문자열에 시도해 보자. 이 경우에 match()MatchObject를 반환할 것이고, 그래서 그 결과를 나중에 사용하기 위해 저장해 두어야 한다.

>>> m = p.match( 'tempo')
>>> print m
<_sre.SRE_Match object at 80c4f68>

이제 일치된 문자열에 관하여 정보를 MatchObject에 질의해 볼 수 있다. MatchObject 실체는 또한 여러 메쏘드와 속성이 있다; 가장 중요한 것들은 다음과 같다:

메쏘드/속성  목적 
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일 것이다. 그렇지만, RegexObject 실체의 search 메쏘드는 문자열을 훓기 때문에, 일치 위치가 0이 아닐 수도 있다.

>>> print p.match('::: message')
None
>>> m = p.search('::: message') ; print m
<re.MatchObject instance at 80c9650>
>>> m.group()
'message'
>>> m.span()
(4, 11)

실제 프로그램에서, 가장 일반적인 스타일은 MatchObject를 변수에 저장하고나서, 그것이 None인지 점검하는 것이다. 이는 보통 다음과 같이 보인다:

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print 'Match found: ', m.group()
else:
    print 'No match'

두개의 RegexObject 메쏘드가 한 패턴에 관하여 일치된 모든 것들을 반환한다. findall()은 일치된 문자열들을 담은 리스트를 반환한다:

>>> p = re.compile('\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

findall()은 전체 리스트를 만들어야만 결과로 반환할 수 있다. 파이썬 2.2에서는 finditer() 메쏘드도 사용할 수 있는데, MatchObject 실체의 연속열을 반복자로 반환한다.

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator
<callable-iterator object at 0x401833ac>
>>> for match in iterator:
...     print match.span()
...
(0, 2)
(22, 24)
(29, 31)

3.4 모듈-수준의 함수

RegexObject을 만들고 그의 메쏘드를 호출할 필요가 없다; re 모듈은 match(), search(), sub(), 등등의 최상위 수준의 함수를 제공하기도 한다. 이 함수들은 RegexObject 메쏘드에 상응하는 인수들을 취해서, RE 문자열을 그의 첫 인수로 더하고, 그리고 여전히 None이나 MatchObject 실체를 반환한다.

>>> print re.match(r'From\s+', 'Fromage amk')
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')
<re.MatchObject instance at 80c5978>

안을 들여다 보면, 이 함수들은 그냥 여러분을 위하여 RegexObject를 만들고 거기에다 그에 적절한 메쏘드를 요청한다. 또한 컴파일된 객체를 캐쉬에 저장하기도 하여서, 앞으로 같은 RE를 사용하여 호출되면 더 빠르게 수행된다.

이런 모듈-수준의 함수를 사용해야 할까, 아니면 RegexObject를 확보해서 그의 메쏘드들을 직접 호출해야 할까? 선택은 얼마나 자주 그 RE가 사용될 것인가에 달려 있고, 개인적인 코딩 스타일에 달려 있다. RE가 코드에서 한 곳에서만 사용된다면, 모듈 함수가 아마도 더 편리할 것이다. 프로그램이 수 많은 정규 표현식을 담고 있거나, 같은 정규 표현식을 여러 곳에서 재사용한다면, 모든 정의들을 한 곳에 모아서, 코드의 한 부분에서 모든 RE를 미리 컴파일 하는 편이 더 가치가 있을 것이다. 표준 라이브러리에서 예제를 하나 취하자면, xmllib.py에서 하나를 뽑아 보겠다:

ref = re.compile( ... )
entityref = re.compile( ... )
charref = re.compile( ... )
starttagopen = re.compile( ... )

일반적으로 나는 단 한번만 사용할 때에도 컴파일된 객체로 작업하는 편을 선호한다. 그러나 나만한 순수주의자는 별로 없을 것이다.

3.5 컴파일 표식

컴파일 표식으로 정규 표현식이 어떻게 작동할 것인가에 관한 모습을 변경할 수 있다. 표식은 re 모듈에서 두 가지 이름을 사용할 수 있는데, 긴 이름으로 IGNORECASE, 그리고 짧은 이름으로 하나짜리-기호 형태인 I가 있다. (펄의 패턴 변경자를 잘 안다면, 하나짜리-기호 형태는 같은 기호를 사용한다; 예를 들어, 짧은 형태의 re.VERBOSEre.X이다.) 비트별로 논리합 연산을 하여 여러 표식이 지정될 수 있다; 예를 들어 re.I | re.MI 표식과 M 표식을 모두 설정한다.

다음은 사용가능한 표식 테이블이다. 각각에 대하여 보다 자세하게 설명을 달았다.

표식  의미 
DOTALL, S .을 새줄문자도 포함하여 어떤 문자에도 일치하도록 만든다.
IGNORECASE, I 대소문자-구별없이 일치시킨다
LOCALE, L 로케일을-인식하여 일치시킨다
MULTILINE, M 여러-줄 일치, ^$에 영향을 미친다
VERBOSE, X RE에 설명을 붙일 수 있다. RE를 더 깨끗하고 이해하기 쉽게 조직할 수 있다.

I
IGNORECASE
대소문자 비구별 일치를 수행한다; 문자 부류와 기호문자 문자열은 대소문자를 무시하고 기호에 일치한다. 예를 들어, [A-Z]는 소문자 기호에도 역시 일치할 것이고, Spam은 "Spam", "spam", 또는 "spAM"에 일치할 것이다. 이런 소문자변환은 현재 로케일을 고려하지 않은 것이다; LOCALE 표식을 설정해도 마찬가지다.

L
LOCALE
\w, \W, \b, 그리고 \B를 현재 로케일에 의존하게 만든다.

로케일은 언어의 차이를 고려하는 프로그램을 만드는데 도움을 줄 목적으로 만들어진 C 라이브러리의 특징이다. 예를 들어, 프랑스어 텍스트를 처리하고자 하면, \w+를 단어에 일치시킬 수 있었으면 하고 바라지만, \w는 오직 문자 부류 [A-Za-z]에만 일치할 뿐이다; 다시 말해 "é" 또는 "ç"에 일치하지 않는다. 시스템이 제대로 환경설정되어 있고 프랑스어 로케일이 선택되었다면, 특정 C 함수들이 프로그램에게 "é"이 기호로 간주되어야 한다고 말해 줄 것이다. 정규 표현식을 컴파일할 때 LOCALE 표식을 설정해 놓으면 그 결과로 나온 컴파일 객체가 \w에 대하여 이런 C 함수들을 사용하게 된다; 이것이 더 느리지만, \w+를 예상대로 프랑스어 단어에 일치시키도록 할 수 있다.

M
MULTILINE
(^ 그리고 $는 아직 설명하지 않았다; 이에 대해서는 섹션 4.1에서 소개할 생각이다.)

보통 ^는 문자열의 처음에서만 일치한다. 그리고 $는 문자열의 끝에서만 일치하고, (혹시 있다면) 문자열의 끝에 새줄문자 바로 앞에서 일치한다. 이 표식이 지정되면, ^는 문자열의 처음과 그 문자열에서 다음에 새줄문자가 나오는 즉시 각 줄의 시작에서 일치한다. 비슷하게, $ 메타문자는 각 줄의 끝과 문자열의 끝에서 일치한다 (각 새줄문자 바로 앞에서).

 

S
DOTALL
특수 문자 "."을 새줄문자를 포함하여 어떤 문자에도 일치하게 만든다; 이 표식이 없으면, "."는 새줄문자를 제외하고 어느 문자에도 일치한다.

X
VERBOSE
이 표식을 사용하면 더 유연하게 정규 표현식의 모양을 만들수 있어서 정규 표현식을 더 읽기 쉽게 만들 수 있다. 이 표식이 지정되면, RE 문자열 안의 공백은 무시된다. 단 공백이 문자 부류 안에 있거나 앞에 피신되지 않은 역사선이 있는 경우는 제외 된다; 이렇게 하면 RE를 보다 선명하게 들여쓰고 조직화할 수 있다. 또한 RE 안에 주석을 달 수 도 있는데 엔진이 무시한다; 주석은 "#"로 표시되며 이 문자는 문자 부류 안에 있어서도 안되고 앞에 피신되지 않는 역사선이 와도 안된다.

예를 들어, 다음은 re.VERBOSE를 사용하는 RE이다; 읽기가 얼마나 더 쉬운지 보자.

charref = re.compile(r"""
 &[#]		     # 수치 개체 참조의 시작
 (
   [0-9]+[^0-9]      # 십진 형태
   | 0[0-7]+[^0-7]   # 팔진 형태
   | x[0-9a-fA-F]+[^0-9a-fA-F] # 십육진 형태
 )
""", re.VERBOSE)

verbose가 설정되어 있지 않으면, RE는 다음처럼 보일 것이다:

charref = re.compile("&#([0-9]+[^0-9]"
                     "|0[0-7]+[^0-7]"
                     "|x[0-9a-fA-F]+[^0-9a-fA-F])")

위의 예제에서, 파이썬이 문자열 기호문자들을 자동으로 결합하여 RE를 더 작은 조작으로 부수었지만, re.VERBOSE를 사용한 버전보다 여전히 더 이해하기 어렵다.

 

4 패턴 파워에 대하여 더 자세히

지금까지는 정규 표현식의 일부를 다루어 보았을 뿐이다. 이 섹션에서는, 새로운 메타문자들 몇가지를 다루어 보고, 그룹을 사용하여 일치된 텍스트의 일부를 열람하는 법을 다루어 보겠다.

 
4.1 메타문자들을 더 자세히

아직 다루어 보지 않은 메타문자들이 몇가지 있다. 그들 대부분을 이 섹션에서 다루어 보겠다.

남아있는 메타문자중 다루어 볼 메타문자는 0-너비 선언이다. 0-너비 선언 문자들에서 엔진은 문자열에서 앞으로 나아가지 않는다; 대신에, 전혀 문자들을 소비하지 않으며, 그냥 성공하거나 실패할 뿐이다. 예를 들어, \b는 현재 위치가 언어의 경계에 있다는 선언이다; 위치는 \b에 의해서 전혀 변하지 않는다. 이는 곧 0-너비 선언이 절대로 반복되지 않는다는 뜻인데, 주어진 위치에서 한 번 일치하면, 수 없이 많은 횟수를 일치할 것이 뻔하기 때문이다.

|
대안, 또는 ``or'' 연산자이다. 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.MatchObject instance at 80c1520>
>>> print re.search('^From', 'Reciting From Memory')
None

 

$
줄의 마지막에 일치한다, 이는 문자열의 끝으로 정의되거나, 또는 새줄문자 다음의 위치로 정의된다.

>>> print re.search('}$', '{block}')
<re.MatchObject instance at 80adfa8>
>>> print re.search('}$', '{block} ')
None
>>> print re.search('}$', '{block}\n')
<re.MatchObject instance at 80adfa8>

기호문자 "$"를 일치시키려면, \$를 사용하거나 다음과 같이 [$] 문자 부류 안에 싸 넣자.

 

\A
문자열의 처음에만 일치한다. MULTILINE 모드가 아니라면, \A^는 그 효과가 같다. 그렇지만 MULTILINE 모드라면, 효과가 다르다; \A는 여전히 문자열의 처음에 일치하지만, ^은 문자열 안에서 새줄문자 뒤면 어느 위치에나 일치해도 좋다.

 

\Z
문자열의 끝에만 일치한다.

 

\b
단어 경계이다. 이는 0-너비 선언으로서 단어의 처음이나 끝에만 일치한다. 단어는 영문자숫자 문자의 연속열로 정의된다, 그래서 단어의 끝은 공백이나 비-영문자숫자 문자로 나타낸다.

다음 예제에서는 완전한 단어일 때만 "class"가 일치한다; 그 안에 또다른 단어가 포함되어 있으면 일치하지 않는다.

>>> p = re.compile(r'\bclass\b')
>>> print p.search('no class at all')
<re.MatchObject instance at 80c8f28>
>>> print p.search('the declassified algorithm')
None
>>> print p.search('one subclass is')
None

이런 특수 연속열을 사용할 때 명심해야할 미묘한 문제가 두가지 있다. 첫째, 파이썬의 문자열 기호문자와 정규 표현식의 연속열 사이의 심각한 충돌이다. 파이썬 문자열 기호문자에서, "\b"는 역사선 문자이며, ASCII 값이 8이다. 날 문자열을 사용하지 않으면, 파이썬은 "\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.MatchObject instance at 80c3ee0>

둘째, 이런 선언이 사용되지 않는 문자 부류 안에서, \b는 파이썬의 문자열 문자기호와의 호환성을 위해서 백스페이스 문자를 나타낸다.

 

\B
또다른 0-너비 선언이다. 이는 \b의 반대로서, 현재 위치가 단어 경계가 아닐 때만 일치한다.

 

4.2 그룹짓기

단순히 RE가 일치했는지의 여부보다 더 많은 정보를 얻고 싶을 때가 많다. 정규 표현식은 RE를 여러 하부그룹으로 잘라서 서로 다른 해당 구성요소들에 일치하도록 작성함으로써 문자열을 분할 하는데 자주 사용된다. 예를 들어, RFC-822 머리부 줄은 머리부 이름과 값으로 나뉘고, ":"로 갈린다. 이는 전체 머리부 줄에 일치하는 정규 표현식을 작성하면 처리될 수 있다. 정규 표현식에서 한 그룹은 머리부 이름에 일치하고 다른 그룹은 머리부의 값에 일치하도록 하면 된다.

그룹은 "(", ")" 메타문자로 표식된다. "(" 그리고 ")"는 수학 표현식에서의 의미와 거의 같다; 표현식들을 사이에 함께 그룹짓는다. 예를 들어, *, +, ?, 또는 {m,n}와 같은 반복 수식자로 그룹의 내용을 반복할 수 있다. (ab)*는 "ab"의 하나이상의 반복과 일치한다.

>>> p = re.compile('(ab)*')
>>> print p.match('ababababab').span()
(0, 10)

"(", ")"로 나타낸 그룹은 일치한 텍스트의 시작 지표와 끝 지표를 얻을 수 있다; 이 지표를 열람하려면 인수를 group(), start(), end(), 그리고 span()에 건네면 된다. 그룹은 0에서부터 번호가 매겨진다. 그룹 0번은 언제나 존재한다; 즉 온전한 전체 RE이며, 그래서 MatchObject 메쏘드는 모두 그룹 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')

패턴에 역참조를 사용하면 이전 나포 그룹의 내용이 문자열의 현재 위치에서도 역시 발견되어야 한다고 지정할 수 있다. 예를 들어, \1은 그룹 1의 정확한 내용이 현재 위치에서 발견되면 성공하고, 그렇지 않으면 실패한다. 꼭 기억해야 할 것은 파이썬의 문자열 기호문자들도 역시 역사선 다음에 숫자를 사용하여 임의의 문자들을 문자열에 포함하기 때문에, RE에 역참조를 구현할 때는 날 문자열을 사용하라는 것이다.

예를 들어, 다음의 RE는 문자열에서 중복되는 단어를 탐지한다.

>>> p = re.compile(r'(\b\w+)\s+\1')
>>> p.search('Paris in the the spring').group()
'the the'

이와 같은 역참조는 문자열을 그냥 탐색할 때는 별로 쓸모가 없다 -- 이런 식으로 데이터가 반복되는 텍스트 형태는 거의 없다 -- 하지만 문자열 대체를 수행할 때는 아주 쓸모가 있다는 것을 알게 될 것이다.

4.3 비-나포 그룹과 이름붙은 그룹

정교한 RE라면 많은 그룹을 가질 수 있고, 관심을 둔 하부문자열을 나포하고 또 RE 그 자체를 그룹짓고 구성할 수 있다. 복잡한 RE에서는, 그룹 번호를 추적 유지하기가 힘들어진다. 이 문제를 도와줄 특징이 두가지 있다. 두 가지 특징 모두 정규 표현식 확장에 대하여 공통인 구문을 사용하기 때문에, 먼저 이것을 살펴 보겠다.

펄 5에는 표준 정규 표현식에 여러 특징들이 추가되었다. 파이썬의 re 모듈은 그렇게 추가된 것들을 대부분 지원한다. 펄의 정규 표현식을 표준 RE와 아주 다르게 만들지 않고서, 새로운 한개짜리 메타문자나 또는 "\"로 시작하는 새로운 특수 연속열을 골라서 새로운 특징을 나타내기는 어려웠을 것이다. 예를 들어 "&"를 새로운 메타문자로 골랐다면, 구형 표현식은 "&"가 정규 문자라고 간주할 것이고 이것을 피신시키기 위하여 \& 또는 [&]를 작성하려고는 하지 않을 것이다.

펄 개발자들이 고른 해결책은 (?...)를 확장 구문으로 사용한 것이었다. 괄호 바로 다음에 나오는 "?"는 반복할 것이 없을 것이므로, 이렇게 해서 호환성 문제가 야기되지 않았다. "?" 바로 다음의 문자들은 어떤 확장자가 사용되고 있는지를 나타내고, 그래서 (긍정적인 내다보기 선언인) (?=foo)과 (하부 표현식 foo를 담고 있는 비-나포 그룹인) (?:foo)는 다르다.

파이썬은 펄의 확장 구문에 또 확장 구문을 추가한다. 물음 표 다음에 첫 문자가 "P"라면, 파이썬에 종속적인 확장이다. 현재로는 그런 확장이 두 가지가 있다: (?P<name>...)는 이름붙인 그룹을 정의한다. 그리고 (?P=name)는 이름붙은 그룹에 대한 역참조이다(backreference). 앞으로 Perl 5의 미래 버전에서 다른 구문을 사용하여 비슷한 특징이 추가된다면, re 모듈은 그 새로운 구문을 지원하기 위해 변경될 것이다. 한편으로 호환성을 위해 그 파이썬 종속적인 구문은 유지할 것이다.

이제 일반적인 확장 구문을 살펴 보았으므로, 복잡한 RE로 그룹을 처리하는 작업을 간단히 해주는 특징으로 되돌아가 보자. 그룹은 왼쪽에서 오른쪽으로 번호가 매겨지고 복잡한 표현식은 많은 그룹을 사용할 수 있기 때문에, 올바른 번호매기기를 추적 유지하기가 점점 힘들어진다. 그렇게 복잡한 RE를 변경하는 작업은 짜증나는 일이다. 앞부분에다 새로운 그룹을 삽입하면, 그 다음에 따르는 모든 것들의 번호가 변경된다.

먼저, 그룹을 사용하여 정규 표현식의 부분을 모으고 싶지만, 그룹의 내용을 열람하는데는 관심이 없을 때가 자주 있다. 이 의도를 명확하게 나타내고 싶으면 비-나포 그룹을 사용하면 된다: (?:...)가 바로 그것인데, 괄호 안에 다른 어떤 정규 표현식도 넣을 수 있다.

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

일치된 그룹들의 내용을 열람할 수 없다는 사실만 제외하면, 비-나포 그룹은 정확하게 나포 그룹과 똑 같이 행위한다; 그 안에 무엇이든지 넣을 수 있고, "*"와 같은 반복 문자로 반복할 수 있으며, 그리고 (나포 그룹이든 비-나포 그룹이든 상관없이) 다른 그룹 안에 내포시킬 수 있다. (?:...)가 특히 유용할 때는 기존의 그룹을 변경할 때 인데, 왜냐하면 다른 모든 그룹들이 어떻게 번호가 매겨져야 하는지 바꾸지 않고서도 새로운 그룹을 추가할 수 있기 때문이다. 나포 그룹과 비 나포 그룹 사이에 탐색에 있어서의 수행성능의 차이는 전혀 없다는 사실을 꼭 언급해야겠다; 한 형태가 다른 형태보다 더 빠르거나 하지는 않는다.

둘째로 더 중요한 특징은 이름붙은 그룹이다; 번호로 그룹을 참조하는 대신에, 그룹에 이름을 붙여 참조할 수 있다.

이름붙은 그룹을 위한 구문은 파이썬-종속적인 확장의 하나이다: (?P<name>...)이 바로 그것인데, name이 당연히 그 그룹의 이름이다. 이름을 그룹에 연관짓는다는 것만 제외하면, 이름붙은 그룹도 역시 나포 그룹과 동일하게 행위한다. 나포 그룹을 다루는 MatchObject 메쏘드들은 모두 번호로 그룹을 참조하기 위한 정수나 또는 그룹 이름을 담은 문자열을 받는다. 이름붙은 그룹은 여전히 번호가 주어진다. 그래서 두 가지 방식으로 그룹에 관한 정보를 열람할 수 있다:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

이름붙은 그룹은 번호를 기억하기 보다 쉽게 이름을 기억할 수 있기 때문에 간편하다. 다음은 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과 같은 표현식에서 역참조를 위한 구문은 그룹의 번호를 참조하기 때문에, 당연히 번호 대신에 그룹 이름을 사용하는 변종들이 있다. 이 또한 파이썬 확장이다: (?P=name)name이라고 부르는 그룹의 내용이 현재 위치에서 다시 발견되어야 함을 나타낸다. 중복된 단어를 찾는 정규 표현식인 (\b\w+)\s+\1는 다음과 같이 (?P<word>\b\w+)\s+(?P=word) 씌여질 수도 있다:

>>> p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
>>> p.search('Paris in the the spring').group()
'the the'

4.4 내다보기 선언(Lookahead Assertions)

또다른 0-너비 확인은 내다보기 선언이다. 내다보기 선언은 긍정적인 형태와 부정적인 형태로 사용할 수 있으며 다음과 같다:

(?=...)
긍정적인 내다보기 선언. 여기에서는 ...로 나타낸 담겨진 정규 표현식이 현재 위치에서 성공적으로 일치하면 성공하고, 그렇지 않으면 실패한다. 그러나, 일단 담겨진 정규 표현식이 시도되더라도, 일치 엔진은 더 이상 진행되지 않는다; 나머지 패턴이 그 확인이 시작된 바로 그 곳에서 시도된다.

 

(?!...)
부정적인 내다보기 선언. 이는 긍정적인 확인의 반대이다; 담겨진 문자열이 문자열의 현재 위치에서 일치하지 않으면 성공한다.

내다보기 선언이 쓸모가 있는 사례를 보여주는 예제를 보면 이것을 확실하게 이해하는데 도움이 될 것이다. 파일이름에 일치하고 그 이름을 "."을 가름자로 하여 기본 이름과 확장자로 가르는 간단한 패턴을 생각해 보자. 예를 들어 "news.rc"에서, "news"는 기본 이름이고, "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$가 일치하면, 전체 패턴은 실패한다. 뒤 따르는 $는 "sample.batch"와 같은 것이 허용된다는 것을 확인하는데 필요하다. 확장자는 "bat"로 시작해야만 한다.

다른 파일이름 확장자를 제외하는 일은 이제 쉽다; 그냥 내다보기 선언의 안에다 대안으로 추가해 넣기만 하면 된다. 다음 패턴은 "bat"나 "exe"로 끝나는 파일이름 확장자를 제외시킨다:

.*[.](?!bat$|exe$).*$

5 문자열 변경하기

이 시점까지는, 정적인 문자열에 대하여 탐색을 수행했을 뿐이다. 정규 표현식은 또한 다양한 방법으로, 문자열을 변경하는데에도 사용된다. RegexObject 메쏘드를 사용하면 된다:

메쏘드/속성  목적 
split() RE가 일치할 때마다 문자열을 갈라서 리스트에 넣는다
sub() RE가 일치하는 모든 하부문자열을 찾아서 다른 문자열로 바꾼다.
subn() sub()와 똑 같은 일을 하지만, 새로운 문자열과 대치 횟수를 반환한다

5.1 문자열 가르기

RegexObjectsplit() 메쏘드는 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('[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split('([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split('[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

5.2 찾기와 바꾸기

또다른 일반적인 작업은 패턴에 일치하는 모든 것들을 발견해서, 다른 문자열로 치환하는 것이다. sub() 메쏘드는 문자열이나 함수일 수 있는 교체 값과, 그리고 처리될 문자열을 취한다.

sub( replacement, string[, count = 0])
문자열(string)에서 RE가 가장 왼쪽에서 중첩되지 않게 출현할 때마다 대체물(replacement)로 대치하여 얻은 문자열을 반환한다. 패턴이 발견되지 않으면, 문자열(string)은 그대로 반환된다.

선택적인 인수 count는 패턴의 출현이 대치될 최대 횟수이다; 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)

빈 일치는 이전의 일치에 바로 연이어서 일어나지 않을 때만 교체된다.

>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b-d-'

만약 replacement가 문자열이면, 그 안에 있는 역사선은 모두 처리된다. 다시 말해, "\n"은 한개의 새줄문자로 변환되고, "\r"은 나르게복귀문자로 변환된다. 등등. "\j"와 같이 잘 모르는 피신 문자들은 그대로 둔다. "\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"는 다음에 기호문자 "0"이 따라오는 그룹 2번에 대한 참조가 아니라 그룹 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는 함수가 될 수도 있는데, 이것이 훨씬 더 통제가 가능하다. replacement가 함수라면, 그 함수는 pattern이 중첩되지 않고 출현할 때마다 호출된다. 각 호출마다, 그 함수에 그 일치에 대한 MatchObject 인수가 건네지고 이 정보를 사용하여 원하는 대체 문자열을 계산하고 그것을 반환한다.

다음 예제에서, 교체 함수는 십진수를 십육진수로 번역한다:

>>> 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() 함수를 사용할 때, 패턴이 첫 인수로 건네진다. 패턴은 문자열이거나 RegexObject일 수 있다; 정규 표현식 표식을 지정할 필요가 있으면, RegexObject를 첫 인수로 사용하거나, 또는 임베드된 수식자를 패턴에 사용해야 한다. 예를 들어 sub("(?i)b+", "x", "bbbb BBBB")'x x'를 반환한다.

6 일반적인 문제들

정규 표현식은 어떤 곳에는 강력한 도구이지만, 어떤 면에서 그 행위는 직관적이지 않고 때로는 예상대로 행위하지 않는다. 이 섹션에서는 가장 흔하게 빠지는 함정들을 지적해 보겠다.

6.1 문자열 메쏘드를 사용하자

때로는 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 모듈에 의존하기 전에, 먼저 문제가 더 빠르고 더 간단한 문자열 메쏘드로 해결될 수 있는지 생각해 보자.

6.2 match() 대 search()

match() 함수는 오직 RE가 문자열의 처음에서 일치하는지만 점검하는데, 반면에 search()는 문자열을 따라가며 일치를 찾는다. 이 차이를 염두에 두는 것이 중요하다. match()가 성공적인 일치를 보고하면 그 위치는 언제나 0이다; 일치가 0에서 시작되지 않으면, match()는 보고하지 않는다.

>>> 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 앞에 .*를 추가해서 re.match()를 계속 사용하고 싶은 유혹이 들때가 있다. 이런 유혹을 떨쳐 버리고 대신에 re.search()를 사용하자. 정규 표현식 컴파일러는 일치를 찾는 과정의 속도를 높이기 위해 RE에 몇가지 분석을 한다. 그런 분석중에 한가지는 일치하는 첫 문자가 무엇이어야 하는지 가늠하는 것이 있다; 예를 들어, Crow는 "C"로 시작하는 문자열과 일치해야 한다. 그 분석 덕분에 엔진은 재빨리 문자열을 훓으며 시작 문자를 찾고, "C"가 발견될 때만 완전한 일치를 시도한다.

.*를 덧붙이면 이런 최적화가 불가능해져서, 문자열의 끝까지 훓어야 하고 다시 돌아와 나머지 RE에 대하여 일치를 찾아야 한다. 대신에 re.search()를 사용하자.

6.3 탐욕적 탐색 대 비-탐욕적 탐색

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 해석기를 이용하자.)

6.4 re.VERBOSE를 사용하지 않으면

지금까지 어쩌면 정규 표현식이 아주 간결한 표기법일 수 있지만, 읽기가 아주 어렵다는 것을 눈치챘을 수도 있겠다. 적당하게 복잡한 RE는 역사선, 괄호, 그리고 메타문자의 기다란 집단이 될 수 있어서, 읽고 이해하기에 아주 어렵게 된다.

그런 RE에 대해서, 정규 표현식을 컴파일 할 때 re.VERBOSE 표식을 지정하면 정규 표현식을 보다 명료하게 형식화할 수 있기 때문 에 도움이 될 수 있다.

re.VERBOSE 표식은 여러가지 효과가 있다. 정규 표현식에서 문자 범주 안에 들어 있지 않은 공백은 무시된다. 이는 곧 dog | cat과 같은 표현식이 좀 읽기에 피곤한 dog|cat과 동등하지만, [a b]는 여전히 문자 "a", "b", 또는 스페이스에 일치할 것이라는 뜻이다. 게다가, RE 안에 주석도 달 수 있다; 주석은 "#" 문자로 시작하여 다음의 새줄문자까지 확장된다. 삼중-따옴표 문자열과 함께 사용하면, 보다 깔끔하게 RE의 모습을 다듬을 수 있다:

pat = re.compile(r"""
 \s*                 # 앞의 공백을 건너뛴다
 (?P<header>[^:]+)   # 머리부 이름
 \s* :               # 공백, 그리고 쌍점
 (?P<value>.*?)      # 머리부의 값 -- *? 
                     # 다음에 따르는 공백을 없애는데 사용된다
 \s*$                # 줄끝까지 이끌리는 공백
""", re.VERBOSE)

이것이 다음보다 훨씬 더 읽기 쉽다:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

7 되먹임

정규 표현식은 복잡한 주제이다. 이 문서가 이해하는데 도움이 되었는지 모르겠다. 이해가 안가는 부분이 있거나 여기에서 다루지 않은 문제가 있다면, 본인에게 개선을 위한 제안을 보내주기를 바란다.

정규 표현식에 관한 가장 완벽한 책은 오라일리(O'Reilly)에서 발간한 제프리 프리들(Jeffrey Friedl)의 정규 표현식 정복(Mastering Regular Expressions)일 것이다. 불행하게도, 거의 펄과 자바 취향의 정규 표현식에 집중할 뿐 파이썬에 관해서는 전혀 다루지 않는다. 그래서 파이썬으로 프로그래밍하는데 참조하기에는 별로 쓸모가 없을 것이다. (첫 판은 파이썬에서 이제 폐기된 regex 모듈을 다루었고, 이는 별로 도움이 되지 못할 것이다.)

이 문서에 대하여 ...

정규 표현식 HOWTO

이 문서는 LaTeX2HTML 번역기를 사용하여 만들어졌다.

LaTeX2HTML은 다음에 복사권이 있다: Copyright © 1993, 1994, 1995, 1996, 1997, Nikos Drakos, Computer Based Learning Unit, University of Leeds, and Copyright © 1997, 1998, Ross Moore, Mathematics Department, Macquarie University, Sydney.

LaTeX2HTML을 파이썬 문서화에 적용하면서 프레드 드레이크(Fred L. Drake, Jr)가 상당히 손을 보았다. 원래의 항해 아이콘은 크리스토퍼 페트릴리(Christopher Petrilli)가 공헌하였다.


Release 0.05.
728x90
반응형
블로그 이미지

nineDeveloper

안녕하세요 현직 개발자 입니다 ~ 빠르게 변화하는 세상에 뒤쳐지지 않도록 우리모두 열심히 공부합시다 ~! 개발공부는 넘나 재미있는 것~!

,