이 포스팅은 Regular Expression 시리즈 12 편 중 8 번째 글 입니다.

  • Part 1 - 01: 개념잡기
  • Part 2 - 02: 간단한 메타문자
  • Part 3 - 03: re module
  • Part 4 - 04: match 객체의 메서드
  • Part 5 - 05: Compile Option(컴파일 옵션)
  • Part 6 - 06: 파이썬 백슬래시 문제
  • Part 7 - 07: 다양한 메타문자
  • Part 8 - This Post
  • Part 9 - 09: 전방 탐색(Lookahead Assertions)
  • Part 10 - 10: 문자열 바꾸기
  • Part 11 - 11: Greedy 와 Non-Greedy
  • Part 12 - 자주쓰이는 정규표현식 초급 정리 - 1
▼ 목록 보기

특정 문자열이 반복되는지에 대해서 알기 위해서는 어떠한 정규식을 작성해야 할까? 우리가 앞에서 배운 내용으로는 할 수 없다. 이렇게 특정 문자열이 단위로 구성되어 검색을 진행하고 싶을 때 사용하는 것이 그루핑이다.

(ABC)+
>>> p = re.compile('(ABC)+')
>>> m = p.search('ABCABCABC OK?')
>>> print(m)
<re.Match object; span=(0, 9), match='ABCABCABC'>
>>> print(m.group())
ABCABCABC

()을 사용하여 간단하게 그루핑을 진행할 수 있다.

의미론적으로 그루핑하기

이렇게 묶어서 무언가를 관리할 수 있다면 추가적으로 이점이 생긴다.

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

이렇게 작성하게 되면, 이름 + " " + 전화번호 형태의 문자열을 찾을 수 있다. 그런데 여기서 이름만 뽑아내고 싶다면 어떻게 해야할까? 사실 위의 반복의 목적보다 이런 의미론적으로 묶어서 뽑아내고자 하는 목적인 경우가 더 많다. 이 때 그루핑을 사용해보자.

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

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

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(2))
010-1234-1234

이렇게 하면 총 2개의 그룹이 발생하고, 전화번에 해당하는 부분에 접근하고 싶으면 group(2)를 사용하면 된다. 그럼 전화번호에서 통신사 번호를 뽑아내고자 하면 어떻게 할까?

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

이 예제에서 알 수 있는 것은 group을 중첩하여 사용하는 것이 가능하다는 것이다. 이런 경우 바깥쪽에서 안쪽으로 들어갈 수록 group의 index가 증가한다. zero-width assertion이기 때문에 이 모든 작업이 가능하다는 것을 염두해두자.

그루핑된 문자열 재참조하기

그룹의 또 하나 좋은 점은 한 번 그루핑한 문자열을 재참조(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를 사용하면 된다.

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

정규식 안에 그룹이 무척 많아진다고 생각해 보자. 예를 들어 정규식 안에 그룹이 10개 이상만 되어도 매우 혼란스러울 것이다. 거기에 더해 정규식이 수정되면서 그룹이 추가, 삭제되면 그 그룹을 인덱스로 참조한 프로그램도 모두 변경해 주어야 하는 위험도 갖게 된다. 만약 그룹을 인덱스가 아닌 이름(Named Groups)으로 참조할 수 있다면 어떨까? 즉 딕셔너리와 같은 형식이다. 그렇다면 이런 문제에서 해방되지 않을까?

이러한 이유로 정규식은 그룹을 만들 때 그룹 이름을 지정할 수 있게 했다. 그 방법은 다음과 같다.

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

위 정규식은 앞에서 본 이름과 전화번호를 추출하는 정규식이다. 기존과 달라진 부분은 다음과 같다.

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

대단히 복잡해진 것처럼 보이지만 (\w+)라는 그룹에 name이라는 이름을 붙인 것에 불과하다. 여기에서 사용한 (?...) 표현식은 정규 표현식의 확장 구문이다. 이 확장 구문을 사용하기 시작하면 가독성이 상당히 떨어지긴 하지만 반면에 강력함을 갖게 된다. 그룹에 이름을 지어 주려면 다음과 같은 확장 구문을 사용해야 한다.

(?P<그룹명>...)

그룹에 이름을 지정하고 참조하는 다음 예를 보자.

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

위 예에서 볼 수 있듯이 name이라는 그룹 이름으로 참조할 수 있다. 그룹 이름을 사용하면 정규식 안에서 재참조하는 것도 가능하다.

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

위 예에서 볼 수 있듯이 재참조할 때에는 (?P=그룹이름)이라는 확장 구문을 사용해야 한다.

Reference

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