정규 표현식

asda주asda·2022년 2월 9일
1

Python

목록 보기
24/31

출처 :
https://wikidocs.net/1642

정규표현식

정규 표현식(Regular Expression)은 복잡한 문자열을 처리할 때 사용하는 기법으로, 파이썬만의 고유 문법이 아니라 문자열을 처리하는 모든 곳에서 사용된다.

정규 표현식을 간단히 '정규식' 이라고도 한다.

정규표현식의 필요성

만일 다음과 같은 문제가 주어졌다고 하자.

주민등록번호를 포함하고 있는 텍스트가 있다. 이 텍스트에 포함된 모든 주민등록번호의 뒷자리를 * 문자로 변경해야한다.

만일 정규식을 모르고 있다면 아래와 같은 절차로 소스코드를 작성해야한다.
1. 전체 텍스트를 공백을 기준으로 나눈다.
2. 나눈 단어가 주민등록번호 형식인지 조사한다.
3. 단어가 주민등록번호 형식이라면 뒷자리를 *로 변경한다.
4. 나눈 단어를 다시 합친다.

이를 구현하는 코드는 아래와 같다.

data = """
park 800905-1049118
kim  700905-1059119
"""

result = []
# 공백을 기준으로 나눈다.
for line in data.split("\n"):
    word_result = []
    for word in line.split(" "):
        # 나눈 단어가 주민등록번호 형식인지를 조사한다.
        if len(word) == 14 and word[:6].isdigit() and word[7:].isdigit():
            # 뒷자리를 *로 변경한다.
            word = word[:6] + "-" + "*******"
        word_result.append(word)
    # 나눈 단어를 다시 합친다.
    result.append(" ".join(word_result))
print("\n".join(result))

출력 결과:
park 800905-*******
kim  700905-*******

반면에 정규식을 사용하면 더욱 간단하고 직관적으로 작성할 수 있다.

import re 

data = """
park 800905-1049118
kim  700905-1059119
"""

pat = re.compile("(\d{6})[-]\d{7}")
print(pat.sub("\g<1>-*******", data))

출력 결과:
park 800905-*******
kim  700905-*******

정규 표현식을 사용하면 코드가 간결해진다. 만약 찾으려는 문자열, 바꿔야하는 문자열의 규칙이 매우 복잡하다면 정규식의 효용은 더 커지게된다.

정규 표현식의 기초, 메타 문자

정규 표현식에서 사용하는 메타 문자(meta charactor)에는 다음과 같다.
이 때 메타 문자는 원래 그 문자가 가진 뜻이 아닌 특별한 용도로 사용되는 문자를 말한다.

. ^ & + ? { } [ ] \ | ( )

정규 표현식에서 위 메타 문자를 사용하면 특별한 의미를 가지게 된다.

문자클래스 [ ]

문자 클래스 (character class) [ ]는 '[ ] 사이의 문자들과 매치' 라는 의미다.
문자 클래스를 만드는 메타 문자인 [ ] 사이에는 어떤 문자도 들어갈 수 있다.

즉 정규 표현식이 [abc]라면 이 표현식의 의미는 'a, b, c 중 한 개의 문자와 매치'를 뜻한다.
예를 들어 'a' , 'before', 'dude' 가 정규식 [abc]와 어떻게 매치되는지 살펴보면

  • 'a'는 정규식과 일치하는 문자인 'a'가 있으므로 매치된다.
  • 'before'는 정규식과 일치하는 문자인 'b'가 있으므로 매치된다.
  • 'dude'는 정규식과 일치하는 문자인 'a, b, c' 중 하나라도 포함하지 않기에 매치되지 않는다.

[ ] 안의 두 문자 사이에 하이픈(-)을 사용하면 두 문자 사이의 범위(From - To)를 의미한다. 예를 들어 [a-c]는 [abc]와 동일하며 [0-5]는 [012345]와 동일하다.

  • [a-zA-Z]: 알파벳 모두
  • [0-9]: 숫자

문자 클래스([])안에는 어떤 문자나 메타 문자도 사용이 가능하지만 ^를 주의해야한다. 문자 클래스 안에 메타문자 ^는 반대(not)을 의미한다. 예를 들어 [^0-9] 라는 정규표현식은 숫자가 아닌 문자만 매치된다.

자주 사용하는 문자 클래스
[0-9] 또는 [a-zA-Z]는 굉장히 자주 사용하는 정규표현신이다. 이렇게 자주 사용하는 정규식은 별도의 표기법으로 표현할 수 있다.

  • \d : 숫자와 매치, [0-9]와 동일한 표현식이다.
  • \D : 숫자가 아닌 것과 매치, [^0-9]와 동일한 표현식이다.
  • \s : whitespace 문자와 매치, [ \t\n\r\f\v] 와 동일한 표현식이다. 맨 앞의 빈칸은 spacebar 이다.
  • \S : whitespace 문자가 아닌 것과 매치, [^ \t\n\r\f\v] 와 동일한 표현식이다.
  • \w : 문자, 숫자와 매치, [0-9a-zA-Z]와 동일한 표현식이다.
  • \W : 문자, 숫자가 아닌 것과 매치, [^0-9a-zA-Z]와 동일한 표현식이다.
    대문자로 사용된 것은 소문자와 반대임을 확인할 수 있다.

Dot(.)

정규 표현식의 Dot(.) 메타 문자는 줄바꿈 문자인 \n 을 제외한 모든 문자와 매치됨을 의미한다.

만일 \n 를 매치시키고자 한다면 re.DOTALL 옵션을 사용하면 된다.

a.b

위의 정규식의 의미는 다음과 같다.

"a + 모든 문자 + b"

즉 a와 b라는 문자 사이에 어떠한 문자가 들어가도 매치가된다는 의미다.

예를 들어 "aab", "a0b", "abc"는 정규식 a.b에 다음과 같이 매치된다.

  • "aab" 는 가운데 문자가 "a" 가 모든 문자를 의미하는 . 과 일치하므로 정규식이 매치된다.
  • "a0b" 는 가운데 문자가 "0" 가 모든 문자를 의미하는 . 과 일치하므로 정규식이 매치된다.
  • "abc" 는 "a", "b" 사이에 문자가 하나도 존재하지 않는다. 그렇기에 정규식과 매치되지 않는다.

문자 클래스 ([ ]) 내에 Dot(.) 메타 문자가 사용된다면 이것은 "모든 문자" 라는 의미가 아닌 . 그대로의 의미를 가진다.

"a[.]b" ==> "a + Dot(.) + b"

따라서 정규식 "a[.]b" 는 "a.b" 문자열과 매치되고, "a0b" 과는 매치되지 않는다.

반복(*)

다음과 같은 정규식이 있다.

ca*t

이 정규식에는 반복을 의미하는 메타문자가 사용되었다. 여기에서 사용한 는 * 앞의 문자 a 가 0 부터 무한히 반복이 가능하다는 의미를 가진다.

정규식문자열Match의 여부설명
ca*tctYes"a"가 0번 반복되어 매치
ca*tcatYes"a"가 0번 이상 반복되어 매치(1번)
ca*tcaaatYes"a"가 0번 이상 반복되어 매치(3번)

반복(+)

반복을 나타내는 또다른 메타문자로 + 가 있다. +는 최소한 한번은 반복되어야한다. 즉 * 가 반복 횟수가 0부터라면 +는 반복횟수가 1부터 인것이다.

ca+t ==> "c + a(1번 이상 반복) + t"

위 정규식에 대한 매치여부는 다음과 같다.

정규식문자열Match의 여부설명
ca+tctNo"a"가 0번 반복되어 매치되지 않는다.
ca+tcatYes"a"가 0번 이상 반복되어 매치(1번)
ca+tcaaatYes"a"가 0번 이상 반복되어 매치(3번)

반복({m,n},?)

메타 문자 { }를 사용하면 반복 횟수를 고정할 수 있다. {m, n} 정규식을 사용하면 반복횟수가 m 부터 n 까지 매치될 수 있다. 또한 m, n 의 자리를 생략할 수 있다.
만일 {3, } 처럼 사용하면 최소한 3번은 반복해야 한다는 의미고, { ,3}은 최대 3번까지 반복이 가능하다는 의미다.

메타 문자 *는 {0, }과 동일하며 +는 {1, }과 동일하다.

  1. {m}

    ca{2}t ==> "c + a(반드시 2번 반복) + t"

    정규식문자열Match 여부설명
    ca{2}tcatNo"a"가 1번만 반복되어 매치되지 않음
    ca{2}tcaatYes"a"가 2번 반복되어 매치
  2. {m, n}

    ca{2, 5}t ==> "c + a(2~5회 반복) + t"

    정규식문자열Match 여부설명
    ca{2,5}tcatNo"a"가 1번만 반복되어 매치되지 않음
    ca{2,5}tcaatYes"a"가 2번 반복되어 매치
    ca{2,5}tcaaaaatYes"a"가 5번 반복되어 매치
  3. ?
    메타 문자 ? 는 {0,1}을 의미한다.

    ab?c ==> "a + b(있어도 되고 없어도 된다) + c"

    정규식문자열Match 여부설명
    ab?cabcYes"b"가 1번만 사용되어 매치
    ab?cacYes"b"가 0번 사용되어 매치

*, +, ? 메타 문자는 모두 {m, n} 형태로 고쳐 쓰는 것이 가능하지만 가급적 이해하기 쉽고 간결한 메타 문자를 사용하는 것이 좋다.

파이썬의 re 모듈

파이썬은 정규 표현식을 지원하기 위해 re(regular expression의 약어) 모듈을 제공한다. re 모듈은 파이썬을 설치할 때 자동으로 설치되는 기본 라이브러리이다.

import re
p = re.compile("ab*")

re.compile 을 사용하여 정규 표현식(위에서는 "ab*")을 컴파일 한다. re.compile의 결과로 반환되는 객체 p(컴파일된 파일 객체)를 사용하여 작업을 수행한다.

정규식을 이용한 문자열 객체

컴파일된 패턴 객체는 다음과 같은 4가지 메서드를 제공한다.

Method목적
match()문자열의 처음부터 정규식과 매치되는지 조사한다.
search()문자열 전체를 검사하여 정규식과 매치되는지 조사한다.
findall()정규식과 매치되는 모든 문자열(substring)을 리스트로 돌려준다.
finditer()정규식과 매치되는 모든 문자열(substring)을 반복 가능한 객체 돌려준다.

match(), search()는 정규식과 매치될 때는 match 객체를 돌려주고, 매치되지 않을 때는 None을 반환한다.
match 객체란 정규식의 검색 결과로 돌려주는 객체이다.

위의 메서드를 사용하기 위해 먼저 컴파일된 객체를 생성한다.

import re
p = re.compile("[a-z]+")

match

match 메서드는 문자열의 처음부터 정규식과 매치되는지를 조사한다.

m = p.match("python")
print(m)

출력 결과:
<re.Match object; span=(0, 6), match='python'>

문자열 "python" 는 정규식 [a-z+] 에 부합하므로 match 객체를 반환한다.

m = p.match("3 python")
print(m)

출력 결과:
None

"3 python"은 처음에 나오는 문자 3이 정규식에 부합되지 않기에 None 을 반환한다.

match() 메소드는 match 객체 혹은 None을 반환하기에 보통 다음과 같은 흐름을 작성한다.

p = re.compile(정규표현식)
m = p.match( 'string goes here' )
if m:
    print('Match found: ', m.group())
else:
    print('No match')
m = p.search("python")
print(m)

출력 결과
<re.Match object; span=(0, 6), match='python'>

문자열 "python" 에 대해 search() 메서드를 수행하면 match() 와 동일한 결과가 나온다.

m = p.search("3 python")
print(m)

출력 결과:
<re.Match object; span=(2, 8), match='python'>

문자열 "3 python" 의 첫 번째 문자가 "3" 이지만 search() 메서드는 문자열의 처음부터 검색하는 것이 아닌 전체를 검색하기에 "3" 이후의 "python" 문자열과 매치된다.

이렇듯 match 메서드와 search 메서드는 문자열의 찾는 방식의 차이를 보인다.

findall

result = p.findall("life is too short")
print(result)

출력 결과:
['life', 'is', 'too', 'short']

위의 코드에서 확인 할 수 있드시 정규식와 매치되는 모든 문자열을 리스트로 반환하여준다.

finditer

finditer는 findall과 동일하지만 그 결과로 반복 가능한 객체(iterator object)를 돌려준다. 반복 가능한 객체가 포함하는 각각의 요소는 match 객체이다.

result = p.finditer("life is too short")
print(result)
for r in result: print(r)

출력 결과:
<callable_iterator object at 0x01F5E390>
<re.Match object; span=(0, 4), match='life'>
<re.Match object; span=(5, 7), match='is'>
<re.Match object; span=(8, 11), match='too'>
<re.Match object; span=(12, 17), match='short'>

match 객체 메서드

match() 메서드와, search() 메서드의 결과로 반환되는 match 객체는 다음과 같은 메서드를 가진다.

method목적
group()매치된 문자열을 반환한다.
start()매치된 문자열의 시작 위치를 반환한다.
end()매치된 문자열의 끝 위치를 반환한다.
span()매치된 문자열의 시작, 끝 위치를 튜플로 반환한다.

match()메서드의 예

m = p.match("python")

print(m.group())
print(m.start())
print(m.end())
print(m.span())

출력 결과:
'python'
0
6
(0, 6)

match 메서드로 반환된 match 객체의 start는 당연히 0이다. 이는 문자열의 시작부터 조사하기 때문이다.

search()의 예

m = p.search("3 python")

print(m.group())
print(m.start())
print(m.end())
print(m.span())

출력 결과:
'python'
2
8
(2, 8)

컴파일 옵션

정규식을 컴파일 할 때 다음 옵션을 추가할 수 있다.

option설명
DOTALL, S. 이 줄바꿈 문자(\n)를 포함하여 모든 문자와 매치할 수 있도록한다.
IGNORECASE, I대소문자 구분없이 매치할 수 있도록 한다.
MULTILINE, M여러줄과 매치할 수 있도록 한다.(^, & 메타문자의 사용과 관계가 있다)
VERBOSE, Xverbose 모드를 사용할 수 있게한다. ( 정규식을 보기 편하게 할 수 있으며 주석등을 사용할 수 있게 된다.)

옵션을 사용할 때 re.DOTALL 처럼 전체 옵션이름을 사용이 가능하며, re.S 처럼 약어를 써도된다.

DOTALL, S

. 메타 문자는 줄바꿈 문자(\n)를 제외한 모든 문자와 매치되는 규칙이 있다. 만약 \n 문자도 포함하여 매치하고 싶다면 re.DOTALL 또는 re.S 옵션을 사용해 정규식을 컴파일하면 된다.

보통 re.DOTALL 옵션은 여러 줄로 이루어진 문자열에서 \n에 상관없이 검색할 때 많이 사용한다.

import re
p = re.compile('a.b')
m = p.match('a\nb')
print(m)

출력 결과:
None

정규식이 a.b인 경우 문자열 a\nb는 매치되지 않음을 확인할 수 있다. 왜냐하면 \n은 . 메타 문자와 매치되지 않기 때문이다. \n 문자와도 매치되게 하려면 다음과 같이 re.DOTALL 옵션을 사용해야 한다.

p = re.compile('a.b', re.DOTALL)
m = p.match('a\nb')
print(m)

출력 결과:
<re.Match object; span=(0, 3), match='a\nb'>

IGNORECASE, I

re.IGNORECASE, re.I 옵션은 대소문자 구분없이 매치를 수행할 때 사용된다.

p = re.compile('[a-z]+', re.I)
print(p.match('python'))
print(p.match('Python'))
print(p.match('PYTHON'))

출력 결과: 
<re.Match object; span=(0, 6), match='python'>
<re.Match object; span=(0, 6), match='Python'>
<re.Match object; span=(0, 6), match='PYTHON'>

MULTILINE, M

메타 문자 ^는 문자열의 처음을, 메타 문자 &는 문자열의 끝을 의미한다. 만일 정규식 ^Python 이라면 문자열의 시작은 항상 python으로 시작해야하며 반대로 python&는 python으로 마무리 되어야한다.

import re
p = re.compile("^python\s\w+")

data = """python one
life is too short
python two
you need python
python three"""

print(p.findall(data)

출력 결과:
['python one']

위의 예시에서 "^python\s\w+"는
'시작은 python으로 그 뒤에 공백이 있어야하며 적어도 하나 이상의 문자, 숫자가 이어져야한다.'
라는 의미다.

그렇기에 메타 문자 ^에 의해 전체에서 python이라는 문자열을 사용한 첫 번째 줄만 매치된 것이다.

만일 메타문자^를 전체가 아닌 각 라인의 처음으로 인식시키고자 한다면 re.MULTILINE, re.M 옵션을 사용해야한다.

import re
p = re.compile("^python\s\w+", re.MULTILINE)

data = """python one
life is too short
python two
you need python
python three"""

print(p.findall(data))
출력 결과:
['python one', 'python two', 'python three']

즉 정리하면 re.MULTILINE 옵션은 ^, & 를 문자열의 각 줄마다 적용시켜주는 것이다.

VERBOSE, X

프로그래머의 재량에 따라, 그리고 찾아야 하는 문자열의 조건에 따라 점점 정규식은 길어서 이해하기 힘든 경우가 많아진다. 이 때문에 정규식에 주석을 달거나, 공백을 주어 상대적으로 보기 쉽게 할 수 있다. 이때 re.VERBOSE, re.X 를 사용한다.

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

위와 같이 복잡한 정규식을 그나마 보기 쉽게 나타낼 수 있다.

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)

둘을 비교해보면 후자가 그나마 주석과 여러 줄로 표현하기에 가독성이 좋다.

re.VERBOSE 옵션을 사용하면 문자열에 사용된 whitespace는 컴파일할 때 제거된다(단 [ ] 안에 사용한 whitespace는 제외). 그리고 줄 단위로 #기호를 사용하여 주석문을 작성할 수 있다.

백슬래시 문제

정규 표현식을 파이썬에서 사용할 때 백슬래시(\)가 혼란을 주는 요소중 한가지이다.
예를 들어 어떤 파일 안에 '\section' 문자열을 찾고자 한다면

\section

처럼 작성하면 안된다. 이 정규식에서 \s 를 whitespace로 해석되어 의도한 결과가 나타나지 않을 것이다.

위의 표현은 아래와 같다.

[ \t\n\r\f\v]ection

만일 의도한 대로 매치하고자한다면 다음과 같이 바꿔줘야한다.

\\section

즉 위의 정규식에서 사용한 백슬래시(\)가 문자열 그 자체임을 알려주기 위해 백슬래시 2개를 사용하여 이스케이프 처리를 해줘야한다.

따라서 위 정규식을 컴파일하고자 한다면 다음과 같이 작성해야한다.

p = re.compile('\section')

근데 파이썬에서는 저렇게 컴파일을 한다면 실제 파이썬 정규식 엔진(정규식 엔진은 정규식을 해석하고 수행하는 모듈을 말한다.)에서는 파이썬 문자열 리터럴 규칙에 의거하여 \\가 \로 변경되어 다시 \section이 전달된다.
물론 파이썬에서만 이렇다.

그렇기에 정규식 엔진에 \\을 전달하고자한다면 \\\\, 백슬래쉬를 4개를 써야한다.

p = re.compile('\\\\section')

하지만 이런식의 표현법은 지저분함과 동시에 복잡하기에 파이썬 정규식에서 Raw String이란 규칙이 생겨났다. 즉 컴파일해야 하는 정규식이 Raw String임을 알려 줄 수 있도록 파이썬 문법을 만든 것이다.

p = re.compile(r'\section')

위와 같이 정규식 문자열 앞에 r 를 기입한다면 Raw String 규칙에 의해 백슬래시(\) 그 자체를 나타낼 수 있다.

메타 문자

앞에서 살펴본 +, *, [], {} 등의 메타문자는 매치가 진행될 때 현재 매치되고 있는 문자열의 위치가 변경된다.(보통 소비된다고 표현한다). 하지만 이와 달리 문자열을 소비시키지 않는(zerowidth assertions) 메타 문자도 있다.

|

메타 문자 '|' or과 같은 의미를 지닌다. A|B 라는 정규식이 있다면 A 또는 B 라는 의미를 지닌다.

import re

text = re.compile("Crow|Servo")
result = text.match("CrowHello")
print(result)

출력 결과:
<re.Match object; span=(0, 4), match='Crow'>

^

^ 메타 문자는 문자열의 맨 처음과 일치함을 의미한다.

import re

text = re.compile("^Hello")
result1 = text.match("Hello world!")
result2 = text.match("world! Hello")
print(result1)
print(result2)

출력 결과:
<re.Match object; span=(0, 5), match='Hello'>
None

result2는 'Hello'가 뒤에 있기에 매치되지 않음을 확인할 수 있다.

$

메타 문자 $는 문자열의 맨 마지막을 의미한다.

import re

text = re.search('hello$','world hello')
print(text)

출력 결과:
<re.Match object; span=(6, 11), match='hello'>

\A

\A는 문자열의 처음과 매치됨을 의미한다. ^ 메타 문자와 동일한 의미이지만 re.MULTILINE 옵션을 사용할 경우에는 다르게 해석된다. re.MULTILINE 옵션을 사용할 경우 ^은 각 줄의 문자열의 처음과 매치되지만 \A는 줄과 상관없이 전체 문자열의 처음하고만 매치된다.

\Z

\Z는 문자열의 끝과 매치됨을 의미한다. 이것 역시 \A와 동일하게 re.MULTILINE 옵션을 사용할 경우 $ 메타 문자와는 달리 전체 문자열의 끝과 매치된다.

\b

\b는 단어 구분자(Word boundary)이다. 보통 단어는 whitespace에 의해 구분된다.

import re

text = re.compile(r'\bclass\b')
result1 = text.search("hi class man")
result2 = text.search("hiclass man")
print(result1)
print(result2)

출력 결과:
<re.Match object; span=(3, 8), match='class'>
None

위의 result1의 class의 양쪽에는 공백이 존재하기에 찾아졌지만, result2는 'hi'에 의해서 찾아지지 않았다.

\b 메타 문자를 사용할 때 주의해야 할 점이 있다. \b는 파이썬 리터럴 규칙에 의하면 백스페이스(BackSpace)를 의미하므로 백스페이스가 아닌 단어 구분자임을 알려 주기 위해 r'\bclass\b'처럼 Raw string임을 알려주는 기호 r을 반드시 붙여 주어야 한다.

\B

메타문자 \B는 \b와 반대의 경우이다. 즉 whitespace로 구분된 단어가 아닌 경우에만 매치된다.

import re

text = re.compile(r'\Bclass\B')
result1 = text.search("hi class man")
result2 = text.search("hiclass man")
result3 = text.search("hiclassman")
print(result1)
print(result2)
print(result3)

출력 결과:
None
None
<re.Match object; span=(2, 7), match='class'>

그룹핑

만일 ABC 문자열이 계속해서 반복되는지를 조사하고 싶다. 이럴 때 필요한 것이 그룹핑(Grouping)이다. 그룹을 만들어 주는 메타문자는 (, )이다.

import re

text = re.compile('(ABC)+')
result1 = text.search("ABCABCABC hello")
print(result1)

출력 결과:
<re.Match object; span=(0, 9), match='ABCABCABC'>

아래는 이름과 전화번호를 찾아내는 정규식이다.

import re

text = re.compile(r'\w+\s+\d+[-]+\d+[-]+\d+')
result1 = text.search("kim 010-123-1234")
print(result1)

출력 결과:
<re.Match object; span=(0, 16), match='kim 010-123-1234'>

여기서 만약 이름만 뽑아내고자 한다면 그룹핑을 해야한다.

보통 반복되는 문자열을 찾을 때 그룹을 사용하는데, 그룹을 사용하는 보다 큰 이유는 위에서 볼 수 있듯이 매치된 문자열 중에서 특정 부분의 문자열만 뽑아내기 위해서인 경우가 더 많다.

만일 이름과 번호를 따로 그룹핑을 하여 이름과 번호를 따로 뽑아내고자 한다면 다음과 같다.

import re

text = re.compile(r'(\w+)\s+(\d+[-]+\d+[-]+\d+)')
result1 = text.search("kim 010-123-1234")
print(result1.group(1))
print(result1.group(2))

출력 결과:
kim
010-123-1234

이름에 해당하는 \w+ 부분을 그룹 (\w+)으로 만들면 match 객체의 group(인덱스) 메서드를 사용하여 그루핑된 부분의 문자열만 뽑아낼 수 있다. group 메서드의 인덱스는 다음과 같은 의미를 갖는다.

group(인덱스)설명
group(0)매치된 전체 문자열
group(1)첫 번째 그룹에 해당되는 문자열
group(2)두 번째 그룹에 해당되는 문자열
group(n)n 번째 그룹에 해당되는 문자열

만일 번호안의 국번을 뽑아내고자 한다면 다음과 같이 그룹을 중첩하여 그룹핑을 할 수 있다.
그룹이 중첩되는 경우 바깥쪽에서 시작하여 안쪽으로 들어갈 수록 인덱스가 증가한다.

import re

text = re.compile(r'(\w+)\s+((\d+)[-]+\d+[-]+\d+)')
result1 = text.search("kim 010-123-1234")
print(result1.group(3))

출력 결과:
010

그룹핑된 문자열 재참조

그룹의 좋은점은 한번 그룹핑한 그룹을 다시 재참조를 할수 있다는 점이다.

import re

text = re.compile(r'(\b\w+)\s+\1')
result1 = text.search("paris in the the spring")
print(result1)

출력 결과:
<re.Match object; span=(9, 16), match='the the'>

정규식 (\b\w+)\s+\1은 (그룹) + " " + group(1)와 매치됨을 의미한다. 이렇게 정규식을 만들게 되면 2개의 동일한 단어를 연속적으로 사용해야만 매치된다. 이것을 가능하게 해주는 것이 바로 재참조 메타 문자인 \1이다. \1은 정규식의 그룹 중 첫 번째 그룹을 가리킨다.

\2를 하면 그룹 중 두 분쩌 그룹을 가리킨다.

그룹핑된 문자열에 이름붙이기

정규식에 그룹이 삭제 및 추가되고 또 그 수가 많아진다면 관리하기가 어려워질 것이다.
하지만 그룹은 인덱스로 가져오는 것이 아닌 이름으로 가져오는 것이 가능하다.
그렇기에 그나마 관리하기가 수월해질 것이다. 방법은 아래와 같다.

(?P<name>\w+)\s+((\d+)[-]\d+[-]\d+)

위는 앞의 이름과 전화번호를 추출하는 정규식이다.
달라진 점은 아래와 같다.

(\w+) -> (?P<name>\w+)

(\w+)라는 그룹에 name이라는 이름을 붙인 것에 불과하다. 여기에서 사용한 (?...) 표현식은 정규 표현식의 확장 구문이다. 이 확장 구문을 사용하기 시작하면 가독성이 상당히 떨어지긴 하지만 관리의 용이함을 얻을 수 있다.

예를 들면 아래와 같다.

import re

name = re.compile(r'(?P<name>\w+)\s+((\d+)[-]\d+[-]\d+)')
result = name.search("kin 010-123-1234")
print(result.group('name'))

word = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
result1 = word.search("paris in the the spring")
print(result1)

출력 결과:
kin
<re.Match object; span=(9, 16), match='the the'>

전방탐색

import re

text = re.compile(".+:")
result = text.search("http://google.com")
print(result)

출력 결과:
<re.Match object; span=(0, 5), match='http:'>

정규식 ".+:"를 통해 http:를 찾아냈다. 만일 http:에서 :를 제거하고자한다면 전방탐색을 고려할 수 있다.

  • 긍정형 전방탐색(?=...): ...에 해당하는 정규식과 매치되어야 하며 조건이 통과되어도 문자열이 소비되지 않는다.
  • 부정형 전방탐색(?!...): ...에 해당하는 정규식과 매치되지 않아야하며 조건이 통과되어도 문자열이 소비되지 않는다.

긍정형 전방탐색

import re

text = re.compile(".+(?=:)")
result = text.search("http://google.com")
print(result.group())

출력 결과:
http

정규식 중 :에 해당하는 부분에 긍정형 전방 탐색 기법을 적용하여 (?=:)으로 변경하였다. 이렇게 되면 기존 정규식과 검색에서는 동일한 효과를 발휘하지만 : 에 해당하는 문자열이 정규식 엔진에 의해 소비되지 않아(검색에는 포함되지만 검색 결과에는 제외됨) 검색 결과에서는 :이 제거된 후 돌려주는 효과가 있다.


.*[.].*$

이 정규식은 '파일 이름' + . + '확장자'를 나타내는 정규식이다. 이 정규식은 foo.bar, autoexec.bat, sendmail.cf 같은 형식의 파일과 매치될 것이다.

만일 .bat인 확장자는 제외하고자 한다면 가장 먼저 생각할 수 있는 것은 다음과 같다.

.*[.][^b].*$

하지만 이는 .bar과 같은 확장자도 제외시키기에 다른 방법을 모색해야한다.

.*[.]([^b]..|.[^a].|..[^t])$

이 정규식은 | 메타 문자를 사용하여 확장자의 첫 번째 문자가 b가 아니거나 두 번째 문자가 a가 아니거나 세 번째 문자가 t가 아닌 경우를 의미한다. 이 정규식에 의하여 foo.bar는 제외되지 않고 autoexec.bat은 제외되어 만족스러운 결과를 돌려준다. 하지만 이 정규식은 아쉽게도 sendmail.cf처럼 확장자의 문자 개수가 2개인 케이스를 포함하지 못하는 오동작을 하기 시작한다.

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

위와 같이 정규식을 작성한다면 만족으로운 결과를 도출해낼 수 있지만 만일 여기서 .exe 도 제외하고자 한다면 더 복잡해 질것이다.
이를 위한 방책으로 부정형 전방 탐색이 있다.

부정형 전방 탐색

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

확장자가 bat가 아닌 경우에만 통과된다는 의미이다. bat 문자열이 있는지 조사하는 과정에서 문자열이 소비되지 않으므로 bat가 아니라고 판단되면 그 이후 정규식 매치가 진행된다.

exe 역시 제외하라는 조건이 추가되더라도 다음과 같이 간단히 표현할 수 있다.

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

문자열 바꾸기

sub 메서드를 통해 정규식과 매치되는 부분을 다른문자로 쉽게 바꿀 수 있다.

import re

test = re.compile('(blue|white|red)')
result = test.sub('colour', 'blue socks and red shoes')
print(result)

출력 결과:
colour socks and colour shoes

sub 메서드의 첫 번째 매개변수는 "바꿀 문자열(replacement)"이 되고, 두 번째 매개변수는 "대상 문자열"이 된다. 위 예에서 볼 수 있듯이 blue 또는 white 또는 red라는 문자열이 colour라는 문자열로 바뀌는 것을 확인할 수 있다.

이 때 한 번만 변환을 하고 싶다면 세 번째 매개변수로 'count=값' 을 주면된다.

import re

test = re.compile('(blue|white|red)')
result = test.sub('colour', 'blue socks and red shoes', count=1)
print(result)

출력 결과:
colour socks and red shoes

sub와 유사한 기능을 하는 subn 메서드가 있는데 이는 기능은 동일하지만 튜플형태로 반환해주는 차이가 있다. 튜플의 첫 번째 요소는 변경된 문자열이며, 두 번째 요소는 바꾸기가 발생한 횟수이다.

import re

test = re.compile('(blue|white|red)')
result = test.subn('colour', 'blue socks and red shoes')
print(result)

출력 결과:
('colour socks and colour shoes', 2)

sub 메서드 사용 시 참조 구문 사용하기

sub 메서드를 사용할 때 참조 구문을 사용할 수 있다. 다음 예를 보자.

import re

test = re.compile(r"(?P<name>\w+)\s+(?P<phone>(\d+)[-]\d+[-]\d+)")
print(test.sub("\g<phone> \g<name>", "park 010-1234-1234"))

출력 결과:
010-1234-1234 park

위 예는 이름 + 전화번호의 문자열을 전화번호 + 이름으로 바꾸는 예이다. sub의 바꿀 문자열 부분에 \g<그룹이름>을 사용하면 정규식의 그룹 이름을 참조할 수 있게 된다.

sub 메서드의 매개변수로 함수 넣기

sub 메서드의 첫 번째 매개변수로 함수를 넣을 수도 있다. 다음 예를 보자.

import re

def hexrepl(match):
    value = int(match.group())
    # 16진수로 변환
    return hex(value)

test = re.compile(r'\d+')
print(test.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.'))

출력 결과:
Call 0xffd2 for printing, 0xc000 for user code.

hexrepl 함수는 match 객체(위에서 숫자에 매치되는)를 입력으로 받아 16진수로 변환하여 돌려주는 함수이다. sub의 첫 번째 매개변수로 함수를 사용할 경우 해당 함수의 첫 번째 매개변수에는 정규식과 매치된 match 객체가 입력된다. 그리고 매치되는 문자열은 함수의 반환 값으로 바뀌게 된다.

Greedy vs Non-Greedy

import re

s = '<html><head><title>Title</title>'
print(re.match('<.*>', s).group())

출력 결과:
<html><head><title>Title</title>

<.*> 정규식의 매치 결과로 <html> 문자열을 돌려주기를 예상할 수 있으나 메타 문자는 매우 탐욕스러워서 매치할 수 있는 최대한의 문자열인 <html><head><title>Title</title> 문자열을 모두 소비해 버렸다. 이때는 다음과 같이 non-greedy 문자인 ?를 사용하면 의 탐욕을 제한할 수 있다.

import re

s = '<html><head><title>Title</title>'
# greed
print(re.match('<.*>', s).group())
# Non-greed
print(re.match('<.*?>', s).group())

출력 결과:
<html><head><title>Title</title>
<html>

non-greedy 문자인 ?는 *?, +?, ??, {m,n}?와 같이 사용할 수 있다. 가능한 한 가장 최소한의 반복을 수행하도록 도와주는 역할을 한다.

0개의 댓글

관련 채용 정보