드디어 길고 길었던 정규 표현식의 마지막 포스팅을 하려 합니다. 물론 나중에 또 추가할 수도 있습니다 😄 메타 문자 중 아직 소개하지 않았던 그루핑 “()”과 사람들이 많이 헷갈리는 부분 중 하나인 전방 탐색, 그리고 간단히 Sub 메서드에 대해 정리했습니다!

그루핑(Grouping)

소괄호 "()"는 그룹을 만들어 주는 메타 문자입니다. 이를 이용하여 그루핑한 문자열이 계속해서 반복되서 나타나는지 조사할 수 있습니다. 예를 들어 “(기러기)+”는 “기러기”라는 문자가 1번 이상 반복되는 문자열을 의미하게 됩니다.

import re
a = re.compile('(기러기)+')
c = a.search("독수리독수리독수리 기러기기러기기러기")
print(c)
print(c.group())
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(10, 19), match='기러기기러기기러기'>
기러기기러기기러기

다른 예시를 들어보겠습니다. 아래는 전화번호 형태의 문자열을 찾는 정규식입니다.

import re
a = re.compile(r"\w+\s+\d+[-]\d+[-]\d+")
c = a.search("Shin 010-1234-5678")
print(c)
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 18), match='Shin 010-1234-5678'>

그런데 위의 결과 값에서 이름과 전화번호 문자열만 각각 뽑아내려 한다면 아래와 같이 이름과 전화번호에 매치되는 부분을 따로 그루핑하고, group()의 메서드를 이용하면 그루핑 된 부분의 문자열만 추출할 수 있습니다.

  • group(0) : 매치된 전체 문자열
  • group(1) : 첫 번째 그룹에 해당되는 문자열
  • group(2) : 두 번째 그룹에 해당되는 문자열
  • group(n) : n 번째 그룹에 해당되는 문자열
import re
a = re.compile(r"(\w+)\s+((\d+)[-]\d+[-]\d+)")
c = a.search("Shin 010-1234-5678")
print(c.group())
print(c.group(1))
print(c.group(2))
print(c.group(3))
--------------------------------------------------------------------------------------------------------------------------------
Shin 010-1234-5678
Shin
010-1234-5678
010

위의 결과에서 “010”과 같이 그룹이 중첩되어 있는 경우에는 바깥쪽부터 안쪽으로 인덱스가 증가하므로, c.group(3)이 돼서야 추출됩니다.


그루핑된 문자열과 동일한 단어를 뒤에 다시 매치시켜야 하는 경우에는 아래와 같이 편리한 방법이 있습니다. 이를 재참조라고 합니다.

  • \1 : 재참조 메타 문자(정규식의 그룹 중 첫 번째 그룹)
  • \2 : 재참조 메타 문자(정규식의 그룹 중 두 번째 그룹)

’(\w+)\s+\1’의 의미는 그루핑된 문자열 뒤에 whitespace가 있고, 그 뒤에 “\1”가 있으므로 그루핑된 문자열이 한번 더 있는 문자열을 매치시킨다는 뜻입니다. 따라서 2개의 동일한 단어를 연속적으로 사용해야만 매치됩니다.

import re
a = re.compile(r'(\w+)\s+\1')
a.search('국수 라면 짜장면 짬뽕 짬뽕 우동').group()
--------------------------------------------------------------------------------------------------------------------------------
'짬뽕 짬뽕'


그루핑된 문자열을 재참조시키는 방법은 인덱스 뿐만이 아닙니다. 그루핑된 문자열에 이름을 붙여주는 방법이 있습니다! 우선 그룹에 이름을 지어 주려면 “(?P<그룹 이름>정규표현식)“과 같은 확장 구문을 사용해야 합니다. 아래에서는 그룹 이름을 “성”이라고 지었고, group(“성”)을 프린트하면 해당 값이 도출됩니다.

import re
a = re.compile(r"(?P<성>\w+)\s+((\d+)[-]\d+[-]\d+)")
c = a.search("Shin 010-1234-5678")
print(c.group("성"))
--------------------------------------------------------------------------------------------------------------------------------
Shin

이렇게 그룹 이름을 지어준 다음, 뒤에 “(?P=그룹 이름)“과 같은 확장 구문을 이용하면 재참조가 가능합니다.

import re
a = re.compile(r'(?P<중식>\w+)\s+(?P=중식)')
a.search('국수 라면 짜장면 짬뽕 짬뽕 우동').group()
--------------------------------------------------------------------------------------------------------------------------------
'짬뽕 짬뽕'


전방 탐색(Lookahead Assertions)

전방 탐색은 원하는 문자열만을 추출할 때 매우 유용하지만, 사용하면 정규 표현식이 암호문처럼 아주 어렵게 바뀌기 때문에 초보자들이 어려워하는 기법 중 하나입니다. 아래와 같은 예시를 들어보겠습니다. 정규표현식 “.+:”은 어떠한 문자들이 쭉 있다가 “:”으로 끝나는 문자열과 매치됩니다. 즉, 결과값으로 “https:”가 도출됩니다.

import re
a = re.compile('.+:')
c = a.search('https://github.com/WOONGSONVI')
print(c.group())
--------------------------------------------------------------------------------------------------------------------------------
https:

그런데 만약 콜론 기호 “:”을 제외하고 출력하려면 어떻게 해야 할까요? 이렇듯 정규표현식에는 매치되지만 그 중에서도 원하는 문자열만을 추출하고 싶을 때 사용할 수 있는 것이 전방 탐색입니다. 전방 탐색에는 긍정형(Positive)과 부정형(Negative)가 있습니다.

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


긍정형 전방 탐색의 예시를 들어보겠습니다. 기존의 “.+:”을 “.+(?=:)”으로 바꿔줬습니다. 이렇게 되더라도 검색에서는 동일한 문자열을 찾아냅니다. 그러나 검색 결과에서는 콜론 기호 “:”가 제외됩니다. 따라서 아래와 같이 원하는 결과가 나옵니다.

import re
a = re.compile('.+(?=:)')
c = a.search('https://github.com/WOONGSONVI')
print(c.group())
--------------------------------------------------------------------------------------------------------------------------------
https


부정형 전방 탐색의 예시를 들어보겠습니다. 파일 이름과 확장자를 추출하기 위해 아래와 같은 정규 표현식을 사용했습니다.

import re
a = re.compile('.*[.].*$')
b = a.search('GIT 사용법.hwp')
c = a.search('딥러닝 특강.pdf')
d = a.search('사건의 지평선.mp4')
print(b)
print(c)
print(d)
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 11), match='GIT 사용법.hwp'>
<re.Match object; span=(0, 10), match='딥러닝 특강.pdf'>
<re.Match object; span=(0, 11), match='사건의 지평선.mp4'>

이 중에 관련 없는 mp4 음악 파일만 걸러내고 싶습니다. 따라서 아래와 같이 정규 표현식을 만들어서 사용했습니다. 이 정규 표현식은 mp4 파일을 잘 걸러내 주지만 확장자 문자 길이가 3인 것만 매치가 됩니다. “정규 표현식.py”와 같이 확장자 문자 길이가 2인 파일은 매치가 되지 않는다는 단점이 존재합니다.

import re
a = re.compile('.*[.]([^m]..|.[^p].|..[^4])$')
b = a.search('GIT 사용법.hwp')
c = a.search('딥러닝 특강.pdf')
d = a.search('사건의 지평선.mp4')
e = a.search('정규 표현식.py')
print(b)
print(c)
print(d)
print(e)
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 11), match='GIT 사용법.hwp'>
<re.Match object; span=(0, 10), match='딥러닝 특강.pdf'>
None
None

물론 아래와 같이 정규표현식을 수정하면 확장자 문자 길이가 2인 파일들도 매치되지만, 확장자가 3보다 긴 파일들은 매치할 수 없고 정규식은 점점 더 복잡해질 것입니다.

import re
a = re.compile('.*[.]([^m].?.?|.[^p]?.?|..?[^4]?)$')
b = a.search('GIT 사용법.hwp')
c = a.search('딥러닝 특강.pdf')
d = a.search('사건의 지평선.mp4')
e = a.search('정규 표현식.py')
print(b)
print(c)
print(d)
print(e)
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 11), match='GIT 사용법.hwp'>
<re.Match object; span=(0, 10), match='딥러닝 특강.pdf'>
None
<re.Match object; span=(0, 9), match='정규 표현식.py'>

이때 부정형 전방 탐색을 사용하면 아주 편리합니다. "(?!mp4$)"는 확장자가 mp4가 아닌 경우에만 매치시킨다는 의미입니다.

import re
a = re.compile('.*[.](?!mp4$).*$')
b = a.search('GIT 사용법.hwp')
c = a.search('딥러닝 특강.pdf')
d = a.search('사건의 지평선.mp4')
e = a.search('정규 표현식.py')
print(b)
print(c)
print(d)
print(e)
--------------------------------------------------------------------------------------------------------------------------------
<re.Match object; span=(0, 11), match='GIT 사용법.hwp'>
<re.Match object; span=(0, 10), match='딥러닝 특강.pdf'>
None
<re.Match object; span=(0, 9), match='정규 표현식.py'>

mp4뿐만 아니라 hwp 파일도 제외하라는 조건이 추가되더라도 아래와 같이 간단히 표현됩니다.

import re
a = re.compile('.*[.](?!mp4$|hwp$).*$')
b = a.search('GIT 사용법.hwp')
c = a.search('딥러닝 특강.pdf')
d = a.search('사건의 지평선.mp4')
e = a.search('정규 표현식.py')
print(b)
print(c)
print(d)
print(e)
--------------------------------------------------------------------------------------------------------------------------------
None
<re.Match object; span=(0, 10), match='딥러닝 특강.pdf'>
None
<re.Match object; span=(0, 9), match='정규 표현식.py'>

참고로, 전방탐색 뒤에 ".*$"를 붙이는 이유는 전방 탐색은 검색에만 포함되고, 검색 결과에서는 제외되기 때문입니다. 즉, 확장자가 도출되지 않습니다. 따라서 파일 이름 뒤의 확장자까지 도출되기 위해 필요합니다.

import re
a = re.compile('.*[.](?!mp4$|hwp$)')
b = a.search('GIT 사용법.hwp')
c = a.search('딥러닝 특강.pdf')
d = a.search('사건의 지평선.mp4')
e = a.search('정규 표현식.py')
print(b)
print(c)
print(d)
print(e)
--------------------------------------------------------------------------------------------------------------------------------
None
<re.Match object; span=(0, 7), match='딥러닝 특강.'>
None
<re.Match object; span=(0, 7), match='정규 표현식.'>


Sub method

앞에서 그루핑된 문자열의 그룹 이름을 지정해주는 방법을 배웠습니다. 이는 sub 메서드를 이용함에도 상당히 유용합니다. 아래와 같이 “이름 전화번호” 형식의 문자열을 “전화번호 이름”형식으로 순서를 바꾸려 합니다. 이 때, sub 메서드에서 “\g<그룹 이름>“을 통해 참조가 가능합니다. 당연히 그룹 이름 대신에 인덱스 번호를 사용해도 같은 결과가 나옵니다.

import re
a = re.compile(r"(?P<name>\w+)\s+(?P<phone>(\d+)[-]\d+[-]\d+)")
print(a.sub("\g<name> \g<phone>", "Shin 010-1234-5678"))
print(a.sub("\g<phone> \g<name>", "Shin 010-1234-5678"))
print(a.sub("\g<1> \g<2>", "Shin 010-1234-5678"))
print(a.sub("\g<2> \g<1>", "Shin 010-1234-5678"))
--------------------------------------------------------------------------------------------------------------------------------
Shin 010-1234-5678
010-1234-5678 Shin
Shin 010-1234-5678
010-1234-5678 Shin


sub 메서드를 이용할 때, 첫 번째 인수로 함수를 사용하여 매치시킨 문자열에 이를 적용할 수도 있습니다. 아래는 “\d+”로 숫자를 매치시키고, 10을 곱해 도출시키는 정규 표현식입니다.

import re

def decuple(match):
     value = int(match.group())
     return str(value * 10)

p = re.compile(r'\d+')
p.sub(decuple, '이 소금물에는 소금이 3 % 들어있다.')
--------------------------------------------------------------------------------------------------------------------------------
'이 소금물에는 소금이 30 % 들어있다.'


정규 표현식 포스팅은 여기까지 입니다! 다음으로는 다시 연속형 분포에 대해 정리해보겠습니다!


Reference

  1. 점프 투 파이썬 08-2
  2. 점프 투 파이썬 08-3


DATA_100%_LOGO_LIGHT



댓글남기기