9 분 소요

파이썬에서는 정규 표현식을 위해 re라는 표준 라이브러리 모듈이 존재한다. 사용법은 다음과 같다.

import re
pattern = re.compile('패턴')
mat = re.match('소스')
import re
mat = re.match('패턴', '소스')

사용법 1에서는 re.compile을 이용하여 정규 표현식이 쓰인 ‘패턴’ 문자열 인자를 컴파일한다. 그 후 컴파일된 패턴 객체를 반환하여 변수 pattern에 할당하고 있다. 그 후 match() 메서드를 통해 패턴을 만족하는 문자열이 소스 안에 있는지 검색한다. 이 방법은 compile 메서드를 통해 파이썬 프로그램이 해당 정규식의 의미를 먼저 파악하게 한 후에 나중에 작업을 수행하고자 할 때 주로 쓰일 수 있을 것이다.

사용법 2에서는 정규식을 이용하여 문자열을 검색해주는 메서드 중 하나인 match() 안에 첫 째 인자로 패턴 문자열을, 두 번째 인자로 소스 문자열을 넘겨서 사용하는 모습이다.

파이썬에서는 패턴이란 말을 주로 re.compile() 함수를 통해 컴파일한 결과를 의미하는 데에 쓰이는 것 같다. 또는 사용법2에서처럼 문자열 검색 메서드(match())의 첫 째 인자로 들어가는 비교할 정규식을 지칭하는 말로 쓰이기도 한다. 사실 둘 다 소스 중 검색할 문자열이란 뜻이란 건 같다.

정규식을 이용한 문자열 검색 메서드들

문자열 검색을 수행시켜주는 re 모듈에서 제공하는 메서드에는 다음이 존재한다.

method 기능
re.match(’패턴’, ‘소스’) 소스의 첫 문자열과 패턴이 서로 일치되는지 조사. 소스의 첫 문자열하고만 비교한다.
re.search(’패턴’, ‘소스’) 소스 전체에서 해당 패턴을 만족시키는 문자열을 조사. 이 때 해당 패턴을 만족시키는 문자열이 여러 개 존재해도 처음 문자열만 반환한다.
re.findall(’패턴’, ‘소스’) 소스 전체에서 해당 패턴을 만족시키는 모든 문자열을 list로 반환
re.finditer(’패턴’, ‘소스’) findall과 같은 역할을 하나 결과값은 iterator 객체로 반환. 이 때 iterator 내의 각각의 요소들은 match 객체이다.

위 메서드들은 re.compile() 메서드를 사용하지 않고 바로 위 메서드들을 사용하고자 할 때를 기준으로 작성하였다. 만약 예를 들어서 pattern = re.compile(’패턴’) 처럼 미리 컴파일을 하여 패턴 객체가 이미 존재한다면 이 패턴 객체를 이용하여 mat = pattern.match(’소스’) 와 같은 방식으로 사용하면 된다.

한 편 match, search는 정규식과 매치될 경우에는 match 객체(정규식의 문자열 검색 결과로 반환된 객체)를 반환하고, 매치되지 않을 경우 None을 반환시킨다.

하나하나 예제를 보자. 그 전에 다음과 같이 미리 패턴을 컴파일했다고 가정하고 시작하겠다.

import re

pattern_obj = re.compile('[a-z]+')  # 영어 소문자 a부터 z까지의 문자가 1번 이상 반복되는가?
  • match()

앞서 표로 봤듯, match() 함수는 패턴이 소스의 첫 문자열과 일치하는지를 조사한다.

import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.match('good')

if match_obj:
    print(match_obj)  # 매치 결과 있는 경우 해당 match 객체 출력
else:
    # match_obj 값이 None인 경우 (즉 매치 결과가 없을 경우)
    print('매치 결과 없음')
<re.Match object; span=(0, 4), match='good'>

위 예제에서는 소스인 ‘good’가 패턴 ‘[a-z]+’를 만족하기에 match 객체가 생성되어 match_obj 변수에 할당되었음을 알 수 있다. 실행결과에서, match=’good’이란 부분을 통해 해당 패턴을 만족시키는 문자열이 무엇인지 출력하고 있다. 또한 실행결과의 span=(0, 4)라는 정보를 통해서 매치되는 해당 문자열 ‘good’이 전체 소스에서 몇 번 인덱스부터 몇 번 인덱스까지를 차지하는지 그 위치 정보도 알 수 있다. (지금이야 ‘good’ 자체가 전체 문자열이지만, 아주 긴 문자열 소스 내에서는 이 위치 정보가 유용하게 쓰일 수도 있을 것 같다)

import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.match('4 goods')

if match_obj:
    print(match_obj)  # 매치 결과 있는 경우 해당 match 객체 출력
else:
    # match_obj 값이 None인 경우 (즉 매치 결과가 없을 경우)
    print('매치 결과 없음')
매치 결과 없음

예제 1-2의 경우, 소스에서 4라는 숫자로 시작하기 때문에 패턴과 일치하지 않아 else문이 실행되었음을 알 수 있다. 앞서 말했듯이 match() 함수는 소스의 첫 문자열하고만 패턴과 비교하므로 뒤에 goods라는 패턴과 매치되는 문자열이 존재해도 match() 메서드는 매치 결과가 없다고 반응한다.

import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.match('사 goods')

if match_obj:
    print(match_obj)  # 매치 결과 있는 경우 해당 객체 출력
else:
    # match_obj 값이 None인 경우 (즉 매치 결과가 없을 경우)
    print('매치 결과 없음')
매치 결과 없음

어찌 보면 당연한 거겠지만은 한글은 영어가 아니므로 역시나 해당 패턴을 만족시키지 못한다.

  • search()

이번에는 위와 똑같은 예제를 search() 메서드로 수행해보겠다.

import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.search('good')

if match_obj:
    print(match_obj)  # 매치 결과 있는 경우 해당 객체 출력
else:
    # match_obj 값이 None인 경우 (즉 매치 결과가 없을 경우)
    print('매치 결과 없음')
<re.Match object; span=(0, 4), match='good'>

여기까지는 match()와 다른 게 없어보인다.

import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.search('4 goods')

if match_obj:
    print(match_obj)  # 매치 결과 있는 경우 해당 객체 출력
else:
    # match_obj 값이 None인 경우 (즉 매치 결과가 없을 경우)
    print('매치 결과 없음')
<re.Match object; span=(2, 7), match='goods'>

그러나 예제 3-5는 match()와는 전혀 다른 결과를 낳았다. 소스의 첫 문자열하고만 비교했던 match() 메서드와는 달리, search() 메서드는 소스의 전체 문자열들을 비교하기 때문에 패턴을 만족시키는 문자열 ‘good’이 뒤에 있었다고 하더라도 search() 메서드에서는 해당 문자열이 패턴과 매치된다고 판단하기 때문에 가능한 일이다.

  • findall()
import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.findall('i wanna buy their goods in 7 days')

if match_obj:
    print(match_obj)  # 매치 결과 있는 경우 해당 객체 출력
else:
    # match_obj 값이 None인 경우 (즉 매치 결과가 없을 경우)
    print('매치 결과 없음')
['i', 'wanna', 'buy', 'their', 'goods', 'in', 'days']

해당 패턴을 만족시키는 모든 문자열들을 리스트로 반환하는 것을 알 수 있다. 패턴과 일치하는 문자열이 없다면…? 다음과 같을 것이다.

import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.findall('000-1111-2222')
print(match_obj)
[]

빈 리스트를 반환한다.

  • finditer
import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.finditer('i wanna buy their goods in 7 days')
print(match_obj)
for mat_obj in match_obj:
    print(mat_obj)
<callable_iterator object at 0x000001FD04F96020>
<re.Match object; span=(0, 1), match='i'>
<re.Match object; span=(2, 7), match='wanna'>
<re.Match object; span=(8, 11), match='buy'>
<re.Match object; span=(12, 17), match='their'>
<re.Match object; span=(18, 23), match='goods'>
<re.Match object; span=(24, 26), match='in'>
<re.Match object; span=(29, 33), match='days'>

위 예제의 실행결과를 보면 알겠지만, finditer() 메서드 사용시 반환되는 값 자체는 iterator 객체이다. 따라서 iterator 안의 match 객체들을 알고 싶다면 for문을 사용해야함을 알 수 있다.

import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.finditer('000-1111-2222')
print(match_obj)
for mat_obj in match_obj:
    print(mat_obj)
<callable_iterator object at 0x000002191288A020>

빈 iterator 객체가 반환되었음을 알 수 있다.

match 객체의 메서드들

앞서 match 메서드와 search 메서드를 통해 얻은 match 객체에는 특정 기능을 수행할 수 있는 메서드들이 있다. 그 메서드들과 기능은 다음과 같다.

method 목적
group() 매치된 문자열 반환
start() 매치된 문자열의 시작 위치 반환
end() 매치된 문자열의 끝 위치 반환
span() 매치된 문자열의 (시작, 끝)에 해당하는 튜플 반환
import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.match('goods')

try:
    print(match_obj.group())
    print(match_obj.start())
    print(match_obj.end())
    print(match_obj.span())
except AttributeError as att_err:
    print(att_err)
goods
0
5
(0, 5)

위 예제에서는 앞서 언급한 메서드들을 모두 사용한 결과이다. match() 메서드는 앞서 보았듯이 소스의 첫 문자열의 매치만 검색하므로 매치된 문자열의 시작 위치는 항상 0일 수밖에 없다.

import re

pattern_obj = re.compile('[a-z]+')
match_obj = pattern_obj.search('4 goods')  # match -> search

try:
    print(match_obj.group())
    print(match_obj.start())
    print(match_obj.end())
    print(match_obj.span())
except AttributeError as att_err:
    print(att_err)
goods
2
7
(2, 7)

하지만 search 메서드로 검색하는 경우, 소스 내에서 패턴과 일치하는 문자열을 모두 찾으므로 시작값이 다를 수 있음을 위 예제를 통해 확인할 수 있다.

그 외 re 모듈에서 제공하는 메서드들

메서드 기능
split(’패턴’, ’소스’) 소스 내 패턴을 기준으로 소스들을 나누고 이를 리스트로 반환. ⇒ 문자열 자료형의 split() 메서드의 역할과 일치
sub(’패턴’, ‘새로 바꿀 문자열’, ‘소스’ 소스 내의 패턴과 일치하는 문자열들을 ‘새로 바꿀 문자열’로 전부 바꾸고, 이를 문자열로 반환. ⇒ 문자열 자료형의 replace()메서드의 역할과 일치

다음은 각 메서드들의 예제이다.

  • split()
import re

source = 'bottle of water and butter'
pattern_obj = re.compile('t')
split_obj = re.split(pattern_obj, source)
print(split_obj)
print(type(split_obj))

splited_list = source.split('t')  # 문자열 자료형에 내장된 split() 메서드 이용
print(splited_list)
['bo', '', 'le of wa', 'er and bu', '', 'er']
<class 'list'>
['bo', '', 'le of wa', 'er and bu', '', 'er']  # 문자열 자료형의 split() 결과
  • sub()
import re

source = 'bottle of water and butter'
pattern_obj = re.compile('t')
split_obj = re.sub(pattern_obj, 'r', source)
print(split_obj)
print(type(split_obj))

print(source.replace('t', 'r'))
borrle of warer and burrer
<class 'str'>
borrle of warer and burrer

컴파일 옵션

re.compile() 메서드에 다음의 옵션들을 사용할 수 있다.

옵션 약어 기능
DOTALL S 점 (.)이 개행 문자를 포함하여 모든 문자와 매치할 수 있도록 한다.
IGNORECASE I 대소문자에 관계없이 매치하도록 함
MULTILINE M 여러 줄과 매치할 수 있도록 함
VERBOSE X 정규식을 보기 편하게 만들거나 주석을 이용할 수 있게 한다.

위에서 약어는 re.DOTALL이라 써도 되고 re.S라 써도 같은 기능을 한다는 뜻이다.

  • DOTALL, S

. 메타 문자는 개행 문자 \n을 제외한 모든 문자와 매치된다고 하였다. 만약 \n 개행 문자도 포함하여 매치하고프다면 해당 옵션을 사용하면 된다.

다음 예제는 DOTALL 옵션을 사용하지 않았을 때의 예제이다.

import re

source = 'a\nb'
pattern_obj = re.compile('a.b')
match_obj = re.match(pattern_obj, source)
print(match_obj)
None

. 메타 문자는 앞서 말했듯 개행 문자 \n은 포함하지 않으므로 매치되는 결과가 없다고 실행결과가 뜬다. 이제 compile() 메서드에 re.DOTALL 옵션을 추가하여 다시 실행해보자.

import re

source = 'a\nb'
pattern_obj = re.compile('a.b', re.DOTALL)  # 옵션 추가
match_obj = re.match(pattern_obj, source)
print(match_obj)
<re.Match object; span=(0, 3), match='a\nb'>

개행 문자도 포함함을 알 수 있다. re.DOTALL 옵션은 보통 여러 줄로 이루어진 문자열에서 개행 문자에 상관없이 검색하고자 할 때 쓰인다.

  • IGNORECASE, I
import re

source = 'g'
pattern_obj = re.compile('[a-z]+', re.I)
match_obj = re.match(pattern_obj, 'goods')
match_obj2 = re.match(pattern_obj, 'Goods')
match_obj3 = re.match(pattern_obj, 'goOdS')
print(match_obj)
print(match_obj2)
print(match_obj3)
<re.Match object; span=(0, 5), match='goods'>
<re.Match object; span=(0, 5), match='Goods'>
<re.Match object; span=(0, 5), match='goOdS'>
  • MULTILINE, M

해당 옵션은 메타 문자 ^, $와 연관된 옵션으로, 해당 메타 문자들은 각각 전체 소스의 첫 문자열, 마지막 문자열만을 의미한다. 그래서 소스가 여러 줄로 개행되어 써진 문자열인 경우에도 각각 첫 줄의 첫 문자열, 마지막 줄의 마지막 문자열에만 적용된다.

이 때 MULTILINE 옵션을 쓰면 해당 메타 문자들 사용 시 각 줄의 첫 또는 마지막 문자열에 대해서 일일이 매치하는지 검색하게 해준다.

import re

source = """
Oh, Titan was her gorgeous armament
And Titan was her sail and crew;
A thing of pride to sweep the surging tide
And laugh to scorn the perilous blue.
Yet let us weep not for her treasured hulk
That sank leagues deep into the sea,
But for the toll of ill-starred voyagers
Who rode her to eternity.
"""

pattern_obj = re.compile('^And')
match_obj = pattern_obj.search(source)
print(match_obj)
None

옵션 적용하지 않았을 때 source의 첫 문자열은 And가 아닌 Oh, 이므로 매치되는 검색 결과가 나오지 않았다.

import re

source = """
Oh, Titan was her gorgeous armament
And Titan was her sail and crew;
A thing of pride to sweep the surging tide
And laugh to scorn the perilous blue.
Yet let us weep not for her treasured hulk
That sank leagues deep into the sea,
But for the toll of ill-starred voyagers
Who rode her to eternity.
"""

pattern_obj = re.compile('^And', re.MULTILINE)
match_obj = pattern_obj.search(source)
print(match_obj)
<re.Match object; span=(37, 40), match='And'>

해당 옵션 적용 시, 긴 문자열의 소스 내에서 각각 여러 줄의 시작 문자열을 검색하므로 검색 결과가 나온다. 위 예제에서 findall 메서드 사용 시 And가 두 번 나온다.

  • VERBOSE, X

정규식이 복잡해지면 나중에 다시 해당 정규식을 볼 때나, 다른 사람들이 보려고 하면 대부분 한 번에 이해하지 못할 것이다. 이럴 때 정규식을 한 줄씩 개행시켜 구분시키고 거기에 주석까지 넣어 의미를 설명해줄 수 있다면 아주 좋을 것이다. 이를 가능케하는 옵션이 바로 VERBOSE 옵션이다.

import re

complex_compile = re.compile(r'&[#](0[0-7]+|[0-9]+|x[0-9a-fA-F]+);')

위 예제의 정규식을 좀 더 알아보기 쉽게 여러 줄로 나누고 주석도 넣어보자.

import re
complex_compile = 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)

re.VERBOSE 옵션 사용 시 문자열에 사용된 whitespace는 컴파일할 때 제거된다. (그래서 위 예제처럼 개행이 가능해진다) 단, [] 안에 사용한 whitespace는 제외된다. 그리고 # 주석도 무시한다.

raw string

파이썬에서는 문자열 내에 백슬래시()를 쓸 경우 \n, \b처럼 특수 문자로 인식하는 경우가 있다. 이 때 백슬래시 자체를 문자 그대로 문자열에 넣고자 한다면 문자열 앞에 r을 쓰면 해당 문자열은 raw string이 되어 백슬래시 자체를 그대로 문자열로 인식한다.

'\write'   =>  '[a-zA-Z0-9]rite'

위 예에서는 백슬래시를 쓸 경우 \w 자체를 특수 문자로 여겨 다음과 같이 인식된다.

r'\write'   => '\write'

위 예에서는 맨 앞에 r을 붙여 raw string으로 만들었으므로 백슬래시도 자체로 출력된다. 이 raw string은 re모듈의 compile 내에서 정규식을 작성할 때 유용하게 쓰일 것이다.


References

[1] Bill Lubannovic, “Introducing Python” (O’REILLY, 2015)

[2] 정규 표현식, 점프 투 파이썬

점프 투 파이썬

This content is licensed under CC BY-NC 4.0

댓글남기기