TIL - 정규 표현식 II

한성봉·2021년 4월 27일
0

이 글은 도서 '점프 투 파이썬' 연습문제를 토대로 작성하였습니다.

정규 표현식

메타 문자

  • +, *, [], {} : 매치가 진행될 때 현재 매치되고 있는 문자열의 위치가 변경된다.(소비된다라고 표현)
  • |, ^, $, \A, \Z, \b, \B : 매치가 진행될 때 현재 매치되고 있는 문자열의 위치를 소비하지않는다.(zero-width assertions)

1. |

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

>>> p = re.compile('Crow|Servo')
>>> m = p.match('CrowHello')
>>> print(m)

<re.Match object: span=(0,4), match='Crow'>

2. ^

^ : 문자열의 맨 처음과 일치함을 의미.

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

3. $

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

>>> 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
  • ^, $ 문자를 메타 문자가 아닌 문자 그대 자체로 매치하고 싶은 경우 \^, $로 사용하면 된다.

4. \A

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

5. \Z

\Z : 문자열의 끝과 매치됨을 의미한다. $와 같은 의미지만 re.MULTILINE 옵션을 사용할 때 $와 달리 전체 문자열의 끝과 매치된다.

6. \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라는 단어와 매치됨을 의미한다.

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('the declassified algorithm'))
None

class 문자열이 있긴하지만 whitespace로 구분된 단어가 아니기에 None이 출력되는 것을 볼 수 있다.

7. \B

\B : \b와 반대의 경우, 즉 whitespace로 구분된 경우가 아닐 때 매치

그루핑

문자열이 계속해서 반복되는지 조사하는 정규식을 작성하고 싶을 때 사용한다.

그루핑 정규식은 (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("HAN 010-1234-1234")

\w+\s+\d+[-]\d+[-]\d+ 는 '이름 + "" + 전화번호'를 찾는 정규식이다. 다음 정규식에서 이름만 추출하고 싶다면 어떻게 해야할까?

>>> p = re.compile(r"(\w+)\s+\d+[-]\d+[-]\d+")
>>> m = p.search("HAN 010-1234-1234")
>>> print(m.group(1))
HAN

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

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

만약 전화번호 부분을 추출하고 싶다면 다음과 같이 그루핑하면 된다.

>>> p = re.compile(r"(\w+)\s+(\d+[-]\d+[-]\d+)")
>>> m = p.search("HAN 010-1234-1234")
>>> print(m.group(2))
010-1234-1234

전화번호에서 국번만 따로 추출하고 싶다면 다음과 같이 그루핑하면 된다.

>>> p = re.compile(r"(\w+)\s+((\d+)[-]\d+[-]\d+)")
>>> m = p.search("HAN 010-1234-1234")
>>> print(m.group(3))
010

- 그루핑된 문자열 재참조(backreferences)

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

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

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

정규식 안에 그룹이 많아진다면 혼란스러울 것이다. 그리고 정규식이 수정,삭제를 통해 그룹을 변경해야하는 경우라면 위험할 것이다.
이럴 때는 이름으로 참조하는 방법이 있다.
다음 예시를 살펴보자.

(?P<name>\w+)\s+((\d+)[-]\d+[-]\d+) 앞의 이름 + 전화번호를 추출하는 예제의 정규식이다.

이름을 추출하여 보지.

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

전방탐색

전방 탐색에는 긍정형과 부정형 전방 탐색이 있다.

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

문자열 바꾸기

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

예시를 살펴보자.

>>> p = re.compile('(blue|white|red)')
>>> p.sub('color', blue socks and red shoes')
'color socks and color shoes'

딱 한번만 바꾸고 싶은 경우가 있을 수 있다. 이렇게 바꾸기 횟수를 제어하려면 3번째 매개변수로 count값을 입력하면 된다.

>>> p.sub('color', blue socks and red shoes', count=1)
'color socks and red shoes'
  • subn메서드를 사용하면 정규식과 매치되는 부분을 다른 문자로 쉽게 대체하지만 결과값을 튜플로 돌려주고 대체된 횟수를 반환해준다.

예시를 살펴보자.

>>> p = re.compile('(blue|white|red)')
>>> p.subn('color', blue socks and red shoes')
('color socks and color shoes', 2)

- sub 메서드를 사용할 때 참조 구문 사용하기

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

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

"\g<phone>\g<name>" 대신에 "\g<2>\g<1>" 참조 번호를 사용하여도 똑같은 결과값을 나타낸다.

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

def hexrepl(match):
    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.'

hexrepl 는 16진수로 변환하여 주는 함수이다. 이처럼 sub메서드의 매개변수로 함수값을 사용해서 활용할 수 있다.

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>

메타문자 *는 매우 Greedy(탐욕스러운)해서 매치할 수 있는 최대한의 문자열인 <html><head><title>Title</title> 문자열을 모두 소비해 버린다.
Non-greedy 한 방법을 살펴보자.

Non-greedy 문자인 ?을 사용하면 *의 탐욕을 제한할 수 있다.

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

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

0개의 댓글