07-2 ~ 07-3 (p.293 ~ p.320)

 

이 책의 마지막 부분이다. 그리고 여태 공부했던 분량 중에 가장 많았다...

이 장의 초반에서 '이번 장은 초보자가 이해하기에는 약간 어려운 부분이다.'라고 했었는데 정말로 앞의 내용들보다는 꽤 어려웠다ㅠㅠ 그래도 차분히 공부해 보니 아예 이해를 못 할 정도는 아니라서 오래 걸리긴 했어도 어느 정도 이해는 한 것 같다..!

 

후우...

 


 

07-2 정규 표현식 시작하기

 

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

정규 표현식에서 사용하는 메타 문자(meta characters)에는 다음과 같은 것이 있다.

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

문자 클래스 [ ]

[ ] 사이의 문자들과 매치

[abc] # a, b, c 중 한 개의 문자와 매치

 

ex) 문자열 : before - 정규식과 일치하는 문자인 "b"가 있으므로 매치
     문자열 : dude - 정규식과 일치하는 문자인 a, b, c 중 어느 하나도 포함하지 않아서 매치X

[ ] 안의 두 문자 사이에 하이픈(-)을 사용하면 두 문자 사이의 범위(From - To)를 의미 ex) [a-c] = [abc]

 

* 자주 사용하는 문자 클래스

정규 표현식 설명
\d 숫자와 매치, [0-9]와 동일한 표현식이다.
\D 숫자가 아닌 것과 매치, [^0-9]와 동일한 표현식이다.
\s whitespace 문자(space나 tab처럼 공백을 표현하는 문자)와 매치, [ \t\n\r\f\v]와 동일한 표현식이다.
맨 앞의 빈 칸은 공백 문자(space)를 의미한다.
\S whitespace 문자가 아닌 것과 매치, [^ \t\n\r\f\v]와 동일한 표현식이다.
\w 문자+숫자와 매치, [a-zA-Z0-9_]와 동일한 표현식이다.
\W 문자+숫자가 아닌 문자와 매치, [^a-zA-Z0-9_]와 동일한 표현식이다.

 

Dot(.)

줄바꿈 문자인 \n을 제외한 모든 문자와 매치됨

a.b # a와 b사이에 줄바꿈 문자를 제외한 어떤 문자가 들어가도 모두 매치
# = "a + 모든 문자 + b"

ex) 문자열 : aab - 가운데 문자 "a"가 모든 문자를 의미하는 .과 일치하므로 정규식과 매치

     문자열 : a0b - 가운데 문자 "0"이 모든 문자를 의미하는 .과 일치하므로 정규식과 매치

     문자열 : abc - "a" 문자와 "b" 문자 사이에 어떤 문자라도 하나는 있어야 하는 이 정규식과 일치하지 않으므로 매치X

 

반복(*)

ca*t # * 문자 바로 앞에 있는 a가 0번 이상 반복되면 매치

ex) 문자열 : ct - "a"가 0번 반복되어 매치

     문자열 : cat - "a"가 0번 이상 반복되어 매치(1번 반복)

     문자열 : caaat - "a"가 0번 이상 반복되어 매치(3번 반복)

 

반복(+)

ca+t # + 문자 바로 앞에 있는 a가 1번 이상 반복되면 매치

ex) 문자열 : ct - "a"가 0번 반복되어 매치되지 않음

     문자열 : cat - "a"가 1번 이상 반복되어 매치(1번 반복)

     문자열 : caaat - "a"가 1번 이상 반복되어 매치(3번 반복)

 

반복({m,n}, ?)

# {m}
ca{2}t # a가 2번 반복되면 매치 = "c + a(반드시 2번 반복) + t"

# {m,n}
ca{2,5}t # a가 2~5번 반복되면 매치 = "c + a(2~5번 반복) + t"

# ?
ab?c # b가 0~1번 사용되면 매치 = "a + b(있어도 되고 없어도 된다) + c"

 

파이썬에서 정규 표현식을 지원하는  re 모듈

re 모듈은 파이썬을 설치할 때 자동으로 설치되는 기본 라이브러리이다.

# re 모듈 사용법
import re # 정규 표현식 컴파일
p = re.compile('ab*') # p는 컴파일된 패턴 객체

 

정규식을 사용한 문자열 검색

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

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

match, search는 정규식과 매치될 때는 match 객체를 돌려주고, 매치되지 않을 때는 None을 돌려준다.

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

# match
m = p.match("python")
print(m)
>>> <re.match object; span=(0, 6), match='python'> # match 객체를 돌려줌
m = p.match("3 python")
print(m)
>>> None

# search
m = p.search("python")
print(m)
>>> <re.Match object; span=(0, 6), match='python'>
m = p.search("3 python")
print(m)
>>> <re.Match object; span=(2, 8), match='python'>

# findall
result = p.findall("life is too short")
print(result)
>>> ['life', 'is', 'too', 'short']

# finditer
result = p.finditer("life is too short")
print(result)
>>> <callable_iterator object at 0x01F5E390>
for r in result: print(r)
...
<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 객체의 메서드

메서드 목적
group() 매치된 문자열을 돌려준다.
start() 매치된 문자열의 시작 위치를 돌려준다.
end() 매치된 문자열의 끝 위치를 돌려준다.
span() 매치된 문자열의 (시작, 끝)에 해당하는 튜플을 돌려준다.
import re
p = re.compile('[a-z]+')
m = p.match("python")
m.group()
>>> 'python'
m.start()
>>> 0
m.end()
>>> 6
m.span()
>>> (0, 6)

m = p.search("3 python")
m.group()
>>> 'python'
m.start()
>>> 2
m.end()
>>> 8
m.span()
>>> (2, 8)

* 모듈 단위로 수행하기

p = re.compile('[a-z]+')
m = p.match("python")

# =
m = re.match('[a-z]+', "python")

 

컴파일 옵션

옵션 이름 약어 설명
DOTALL S dot 문자(.)가 줄바꿈 문자를 포함하여 모든 문자와 매치한다.
IGNORECASE I 대, 소문자에 관계 없이 매치한다.
MULTILINE M 여러 줄과 매치한다. (^, $ 메타 문자의 사용과 관계가 있는 옵션이다.)
VERBOSE X verbose 모드를 사용한다.
(정규식을 보기 편하게 만들 수도 있고 주석 등을 사용할 수도 있다.)
# DOTALL, S
import re
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
p = re.compile('[a-z]', re.I)
p.match('python')
>>> <re.Match object; span=(0, 1), match='p'>
p.match('Python')
>>> <re.Match object; span=(0, 1), match='p'>
p.match('PYTHON')
>>> <re.Match object; span=(0, 1), match='p'>

# MULTILINE, 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'] # ^, $ 메타 문자를 각 줄마다 적용

# VERBOSE, X
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)

 

백슬래시 문제

정규 표현식을 파이썬에서 사용할 때 혼란을 주는 요소가 한 가지 있는데, 바로 백슬래시(\)이다.

예를 들어 어떤 파일 안에 있는 "\section"문자열을 찾기 위한 정규식을 만든다고 가정해 보자.

이 정규식은 \s 문자가 whitespace로 해석되어 의도한 대로 매치가 이루어지지 않는다.

이 표현은 다음과 동일한 의미이다.

[ \t\n\r\f\v]ection # \s 문자가 이스케이프 코드 \t, \n, \r, \f, \v로 해석됨

의도한 대로 매치하고 싶다면 다음과 같이 변경해야 한다.

\\section

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

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

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

그런데 여기에서 또 하나의 문제가 발견된다. 위처럼 정규식을 만들어서 컴파일하면 실제 파이썬 정규식 엔진에는 파이썬 문자열 리터럴 규칙에 따라 \\\로 변경되어 \section이 전달된다.

결국 정규식 엔진에 \\ 문자를 전달하려면 파이썬은 \\\\처럼 백슬래시를 4개나 사용해야 한다.

하지만 이는 너무 복잡해서 파이썬 정규식에는 Raw String 규칙이 생겨나게 되었다. 즉 컴파일해야 하는 정규식이 Raw String임을 알려 줄 수 있도록 파이썬 문법을 만든 것이다. 그 방법은 다음과 같다.

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

 

07-3 강력한 정규 표현식의 세계로

 

메타 문자

아직 살펴보지 않은 메타 문자에 대해서 모두 살펴보자. 여기에서 다룰 메타 문자는 앞에서 살펴본 메타 문자와 성격이 조금 다르다. 앞에서 살펴본 +, *, [ ], { } 등의 메타문자는 매치가 진행될 때 현재 매치되고 있는 문자열의 위치가 변경된다(보통 소비된다고 표현한다.) 하지만 이와 달리 문자열을 소비시키지 않는 메타 문자도 있다. 이번에는 이런 문자열 소비가 없는(zero-width assertions) 메타 문자에 대해 살펴보자.

 

|

| 메타 문자는 or과 동일한 의미로 사용된다. A|B라는 정규식이 있다면 A 또는 B라는 의미가 된다.

p = re.compile('Crow|Servo')
m = p.match('CrowHello')
print(m)
>>> <re.Match object; span=(0, 4), match='Crow'>

 

^

^ 메타 문자는 문자열의 맨 처음과 일치함을 의미한다. 앞에서 살펴본 컴파일 옵션 re.MULTLINE을 사용할 경우에는 여러 줄의 문자열일 때 각 줄의 처음과 일치하게 된다.

print(re.search('^Life', 'Life is too short'))
>>> <re.Match object; span=(0, 4), match='Life'>
print(re.search('^Life', 'My Life'))
>>> None

 

$

$ 메타 문자는 ^ 메타 문자와 반대의 경우이다. 즉 $는 문자열의 끝과 매치함을 의미한다.

print(re.search('short$', 'Life is too short'))
>>> <re.Match object; span=(12, 17), match='short'>
print(re.search('short$', 'Life is too short, you need python'))
>>> None

* ^ 또는 $ 문자를 메타 문자가 아닌 문자 그 자체로 매치하고 싶은 경우에는 \^, \$ 로 사용하면 된다.

 

\A

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

 

\Z

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

 

\b

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

p = re.compile(r'\bclass\b')
print(p.search('no class at all'))
>>> <re.Match object; span=(3, 8), match='class'>
# '\bclass\b' 정규식은 앞뒤가 whitespace로 구분된 class라는 단어와 매치됨을 의미한다.
# 따라서 no class all의 class라는 단어와 매치됨을 확인할 수 있다.

print(p.search('one subclass is'))
>>> None # class 앞에 sub 문자열이 더해져 있으므로 매치되지 않는다.

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

 

\B

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

p = re.compile(r'\Bclass\B')
print(p.search('no class at all'))
>>> None
print(p.search('the declassified algorithm'))
>>> <re.Match object; span=(6, 11), match='class'>
print(p.search('one subclass is'))
>>> None

class 단어의 앞뒤에 whitespace가 하나라도 있는 경우에는 매치가 안되는 것을 확인할 수 있다.

 

그루핑

ABC 문자열이 계속해서 반복되는지 조사하는 정규식을 작성하고 싶다고 하자. 어떻게 해야 할까? 지금까지 공부한 내용으로는 위 정규식을 작성할 수 없다. 이럴 때 필요한 것이 바로 그루핑(Grouping)이다.

(ABC)+

그룹을 만들어 주는 메타문자는 ( )이다.

p = re.compile('(ABC)+')
m = p.search('ABCABCABC OK?')
print(m)
>>> <re.Match object; span=(0, 9), match='ABCABCABC'>
print(m.group(0))
>>> ABCABCABC
p = re.compile(r"(\w+)\s+\d[-]\d+[-]\d+")
m = p.search("park 010-1234-1234")
print(m.group(1))
>>> park
group(인덱스) 설명
group(0) 매치된 전체 문자열
group(1) 첫 번째 그룹에 해당하는 문자열
group(2) 두 번째 그룹에 해당하는 문자열
group(n) n 번째 그룹에 해당하는 문자열
p = re.compile(r"(\w+)\s+((\d+)[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
print(m.group(3))
>>> 010
# 그루핑된 문자열 재참조하기(Backreferences) (재참조 메타문자 = \1)
p = re.compile(r'(\b\w+)\s+\1') # = '(그룹) + " " + 그룹과 동일한 단어'
p.search('Paris in the the spring').group() # 2개의 동일한 단어 연속적으로 사용해야만 함
>>> 'the the'

 

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

(\w+) -> (?P<name>\w+)
# (?P<그룹 이름>...)
p = re.compile(r"(?P<name>\w+)\s+((\d+)[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
print(m.group("name"))
>>> park

 

전방 탐색

정규식 종류 설명
(?=...) 긍정형 전방 탐색 ...에 해당하는 정규식과 매치되어야 하며 조건이 통과되어도 문자열이 소비되지 않는다.
(?!...) 부정형 전방 탐색 ...에 해당하는 정규식과 매치되지 않아야 하며 조건이 통과 되어도 문자열이 소비되지 않는다.
# 긍정형 전방 탐색
p = re.comile(".+(?=:)") # 긍정형 전방 탐색 기법 적용(?=:)
m = p.search("http://google.com")
print(m.group())
>>> http # :에 해당하는 문자열이 정규식 엔진에 의해 소비되지 않아서 결과에서는 :이 제거

# 부정형 전방 탐색
.*[.](?!bat$).*$ # 확장자가 bat가 아닌 경우에만 통과

 

문자열 바꾸기

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

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) # 바꾸기 횟수 제어 : count
>>> 'colour socks and red shoes'

# subn
p = re.compile('(blue|white|red)')
p.subn( 'colour', 'blue socks and red shoes')
>>> ('colour socks and colour shoes', 2) # 튜플로 반환(변경된 문자열, 바꾸기가 발생한 횟수)

sub 메서드의 첫 번째 매개변수는 '바꿀 문자열(replacement)'이 되고, 두 번째 매개변수는 '대상 문자열'이 된다.

위 예에서 볼 수 있듯이 blue 또는 white 또는 red라는 문자열이 colour라는 문자열로 바뀌는 것을 확인할 수 있다.

 

Greedy vs Non-Greedy

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>

위 코드에서 * 메타 문자는 매우 탐욕스러워서 매치할 수 있는 최대한의 문자열인 <html><head><title>Title</title> 문자열을 모두 소비해 버렸다. 어떻게 하면 이 탐욕스러움을 제한하고 <html> 문자열 까지만 소비하도록 막을 수 있을까?

print(re.match('<.*?>', s).group())
>>> <html>

*다음과 같이 non-greedy 문자인 ?를 사용하면 *의 탐욕을 제한할 수 있다.

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

+ Recent posts