Gorio Tech Blog search

파이썬 정규표현식(re) 사용법 - 08. 예제(단어, 행)

|

파이썬 정규표현식(re) 사용법 - 01. Basic
파이썬 정규표현식(re) 사용법 - 02. 문자, 경계, flags
파이썬 정규표현식(re) 사용법 - 03. OR, 반복
파이썬 정규표현식(re) 사용법 - 04. 그룹, 캡처
파이썬 정규표현식(re) 사용법 - 05. 주석, 치환, 분리
파이썬 정규표현식(re) 사용법 - 06. 치환 함수, 양방탐색, 조건문
파이썬 정규표현식(re) 사용법 - 07. 예제(숫자)
파이썬 정규표현식(re) 사용법 - 08. 예제(단어, 행)
파이썬 정규표현식(re) 사용법 - 09. 기타 기능


이 글에서는 정규표현식으로 처리할 수 있는 예제를 설명한다.

본 글에서 정규표현식은 regex와 같이, 일반 문자열은 ‘regex’와 같이 표시하도록 한다.

파이썬 버전은 3.6을 기준으로 하나, 3.x 버전이면 (아마) 동일하게 쓸 수 있다.
2.7 버전은 한글을 포함한 비 알파벳 문자 처리가 다르다.


단어

기초

문제 1: colour, Color, color 등 모든 버전의 color에 일치되는 정규식을 작성하라.

문제 1 정답보기

r'\b(?i)colou?r\b'


(?i) 모드 변경자 대신에 re.IGNORECASE 옵션을 주어도 상관없다.

다음 문제는 이전에 본 적이 있는 문제이다.

문제 2: cat에는 일치되지 않고, cat을 포함하면서 그보다 긴 단어, staccato, cats, tomcat 등에 일치되는 정규식을 작성하라.

문제 2 정답보기

r'\Bcat\B'



특정 단어 배제

문제 3: 이번에는 특정 단어를 제외시켜 보자. reg를 제외한 모든 단어에 일치되도록 하는 정규식을 작성하라.

문제 3 정답보기

r'\b(?!reg\b)\w+'


생각하기 조금 어려웠을 것 같다.

부정 전방탐색이 빛을 발하는 순간이다. 부정 전방탐색을 씀으로써 앞에 단어 경계가 있고(\b), ‘reg’로 이루어져 있으며(reg), 바로 뒤에 \b이 나와 끝나버리는, 즉 ‘reg’에 정확히 일치되는 순간이 되면 위 정규식은 일치에 실패한다.
따라서 특정 단어 ‘reg’를 배제할 수 있게 된다. 부정 전방탐색의 일치에 실패하면, 그 다음 부분인 \w+를 갖고 일치를 시도한다. 단어면 성공할 것이다.

문제 4: 이번엔 특정 단어뿐 아니라 그 단어를 포함하는 단어도 제외시켜 보자. reg뿐만 아니라 regex, register, dreg 등은 모두 제외되어야 한다.

문제 4 정답보기

r'\b(?:(?!cat)\w)+\b'


문제 3번을 생각해냈으면 이 문제도 풀 수 있을 것이라고 생각한다.

주어진 조건은 프로그래밍이 가능한 다음 세 문장으로 압축할 수 있다.

  1. 단어 중간의 어떤 부분에서든 그 앞에 ‘reg’가 와서는 안 된다. (?!cat)\w
  2. 1번을 만족했으면, 단어 문자가 1개 이상은 있어야 한다. ( ... \w)+
  3. 물론 양끝에 단어 경계는 있어야 한다. \b ... \b

위 세 조건을 적용시키면 ‘reg’가 단어에 포함된다면 일치되지 않는다는 사실을 알 수 있다.

문제 5: 바로 뒤에 숫자가 따라오는 단어를 무시하는, 다른 모든 단어를 검색하는 정규식을 작성하라. 단어와 숫자 사이에는 비 단어 문자가 온다고 가정하자.

문제 5 정답보기

r'\b\w+\b(?!\W+\d+\b'


이번에도 부정 전방탐색을 사용하면 된다. 무엇을 제외하고 싶다면 부정 양방탐색을, 다른 조건을 더 추가해야 한다면 긍정 양방탐색을 사용한다는 것을 기억하자.


빈 행 제거

문제 6: 빈 행을 제거하자. 예컨대 ‘\n\n’ 을 ‘\n’ 하나로 치환하여 불필요한 행을 제거하는 식이다. 운영체제에 독립적으로 작성되어야 한다.

문제 6 정답보기

re.sub(r'\n+', r'\n', string)


개행 문자 말고 모든 공백 문자를 하나로 치환하려면 다음을 쓰면 된다.

re.sub('\s+', ' ', string)

가로 공백 문자만 치환하려면 \s 대신 [ \t]를 사용하라.

중복 행 제거

문제 7: 중복 행을 제거하자. 똑같은 내용의 행이 두 번 이상 반복되는 행을 하나만 남겨두고 모두 제거하자.

문제 7 정답보기

re.sub(r'^(.*)(?:(?:\r?\n|\r)\1)+$', r'\1', re.MULTILINE)


복잡해 보이지만 크게 어렵진 않다. 중복 행이란 다음과 같은 구조일 것이다.

  1. 행 내용
  2. 행 구분자
  3. 행 내용
  4. 행 구분자
  5. 행 내용 …

행 내용은 (.*)으로 간단히 해결된다. 나중에 똑같은 것을 일치시켜야 하므로 캡처 그룹을 사용한다.
그리고 행 구분자를 사용하는데, 운영체제에 상관없이 사용하기 위해 조금 복잡하게(\r?\n|\r)이 들어갔지만, 자신이 무슨 운영체제에서 프로그래밍하고 있으며 어떤 운영체제에서 작성된 파일을 갖고 있는지 안다면 하나만 써도 무방하다.
그 다음은 행 내용을 재사용하기 위한 \1, 1회 이상 반복을 위한 +, 그리고 한 행만 남겨두고 제거하기 위한 r'\1'을 사용하면 해결된다.

주의할 점으로는 re.MULTILINE 옵션을 주어야 하며, re.DOTALL은 절대로 주어서는 안 되는 옵션이라는 점이다.


조금 더 복잡한 예제를 정리해 두면 좋겠지만, 그때그때 맞게 쓰는 것이 더 나을 것 같아서 굳이 따로 정리할 필요는 없을 것 같다.

다음 글에서는 re 패키지에 포함된 다른 함수들을 다루도록 하겠다.

Comment  Read more

파이썬 정규표현식(re) 사용법 - 07. 예제(숫자)

|

파이썬 정규표현식(re) 사용법 - 01. Basic
파이썬 정규표현식(re) 사용법 - 02. 문자, 경계, flags
파이썬 정규표현식(re) 사용법 - 03. OR, 반복
파이썬 정규표현식(re) 사용법 - 04. 그룹, 캡처
파이썬 정규표현식(re) 사용법 - 05. 주석, 치환, 분리
파이썬 정규표현식(re) 사용법 - 06. 치환 함수, 양방탐색, 조건문
파이썬 정규표현식(re) 사용법 - 07. 예제(숫자)
파이썬 정규표현식(re) 사용법 - 08. 예제(단어, 행)
파이썬 정규표현식(re) 사용법 - 09. 기타 기능


이 글에서는 정규표현식으로 처리할 수 있는 예제를 설명한다.

본 글에서 정규표현식은 regex와 같이, 일반 문자열은 ‘regex’와 같이 표시하도록 한다.

파이썬 버전은 3.6을 기준으로 하나, 3.x 버전이면 (아마) 동일하게 쓸 수 있다.
2.7 버전은 한글을 포함한 비 알파벳 문자 처리가 다르다.


숫자

십진 정수

문제 1: 가장 간단한 십진 정수를 찾는 정규식을 작성하라.

문제 1 정답보기

r'\b[0-9]+\b'


문제 2: 가장 일반적인 형태의 십진 정수를 찾는 정규식을 작성하라. 맨 앞에 ‘+’ 혹은 ‘-‘ 기호가 붙어 있을 수 있으며, 기호와 숫자 사이에는 공백이 하나 있을 수 있다.

문제 2 정답보기

r'(?:[+-] ?)?\b\d+\b'

비 아스키 숫자를 포함하고 싶다면 \d를, 오로지 아라비아 숫자 10개만 숫자로 인식하게 하고 싶다면 [0-9]를 사용하도록 한다.
또한 문자열 전체에 일치되게 하고 싶다면 \A\Z를 사용하면 된다.


십진 정수의 leading-zero 제거

문제 3: 십진 정수의 앞에 오는 0들을 제거하는 정규식을 작성하라. 기호는 없다고 가정하자.

문제 3 정답보기

re.sub(r'\b0*([1-9][0-9]*|0)\b', r'\1', string)


leading-zero가 될 수 있는 부분과 그렇지 않은 부분을 구분하기만 하면 이 문제는 풀린다. 그리고 leading-zero를 제거하는 것이 목표이므로 캡처된 부분으로 re.sub 메서드를 쓰면 해결된다.

한 가지 주의점은 ‘0’ 또는 ‘00’ 등 진짜 0밖에 없는 경우이다. 이 경우 0을 하나 남겨두어야 하기 때문에, leading-zero 뒤 숫자 부분에서 0을 한 개 남겨 두었다(|0).


16진수

문제 4: 16진수를 찾는 정규식을 작성하라. 0-9의 숫자와 A-F 알파벳만 쓴다고 하자.

문제 4 정답보기

r'\A[0-9A-F]+\Z'


문제 5: 16진수를 찾는 정규식을 작성하라. 16진수는 아라비아 숫자 10개 또는 A-F, a-f를 사용하되 대문자와 소문자를 섞어 쓰지는 않는다. 또한, 16진수임을 나타내기 위해 숫자 바로 앞에 ‘0x’나 ‘&H’(hexa의 의미)를 붙이거나, 혹은 숫자 뒤에 ‘H’를 붙인다.

문제 5 정답보기

r'(\b0x|&H)?(?:[0-9A-F]+|[0-9a-f]+)(?(1)|H?)\b'


꽤 어려워 보일 것이다. 정규식은 복잡해지면 주석이 꼭 필요하다.

  1. (\b0x|&H)?: 보통 단어 경계를 맨 앞에 두는데, 이번엔 다자택일 중 하나에 두었다. 이유는 엠퍼샌드(‘&’)는 문자 집합에 포함되지 않으므로 단어 경계를 쓰면 안 된다.
    1. 앞에 16진수 식별자를 두는 것은 선택적이므로 ?를 쓴다.
    2. 그리고 이 부분은 첫 번째 캡처 그룹을 형성한다.
  2. (?:[0-9A-F]+|[0-9a-f]+): 16진수 숫자를 나타내는 부분만 빼고 싶다면 앞부분의 비 캡처 그룹임을 의미하는 ?:를 빼면 된다.
    1. 대소문자 혼용을 허용하지 않았으므로 대문자를 쓸 때와 소문자를 쓸 때를 분리하였다.
  3. (?(1)|H?): 조건문이다. 첫 번째 캡처 그룹에서 만약에 ‘0x’나 ‘&H’가 일치되었으면, 뒤에 또 ‘H’ 식별자를 두지는 않는다. 따라서 일치되었으면(‘맞으면’) 아무것도 없는 경우를, 일치되지 않았으면 ‘H’ 식별자를 선택적으로 일치시킬 수 있도록 한다.
  4. \b: 단어 경계이다.

참고: 지정된 범위의 숫자인지 확인

이는 정규식으로 작성하길 추천하지 않는다. 아니면 함수로 따로 빼 두는 것을 추천한다.
그냥 숫자인지만 정규식으로 확인하고, 주어진 범위인지는 다른 함수로 확인하자.

그래도 예를 하나 들어 보겠다. 얼마나 복잡한지를 느끼면 된다.

문제 6: 1 이상 365 이하의 정수인지를 확인하는 정규식을 작성하라.

문제 6 정답보기

r'(36[0-5]|3[0-5][0-9]|[12][0-9]{2}|[1-9][0-9]?)'


그냥 따로 씁시다.

number = re.search('\d+', '몇 이상 몇 이하의 수 38개')
number = int(number.group())

if 1 <= number <= 365:
    print('Yei!')

결과

Yei!

부동소수점 수

3번째 글의 끝에서 다음과 같은 문제가 있었다.

문제 7: 1.2이나 3.72e3, 1.002e-12 같은 수를 부동소수점 수 또는 과학적 표기법으로 표기한 수라고 한다. 이와 같은 수에 일치하는 정규표현식을 작성하라.

문제 7 정답보기

r'\b\d*\.\d+(e\d+)?'


조금 더 일반화한 버전을 문제로 하나 더 내 보았다.

문제 8 : 다음 조건에 맞는 부동소수점 수를 찾는 정규식을 작성하라.

  1. 부호부, 정수부, 가수부는 선택이다.
  2. 지수부 역시 선택이다.
  3. 정수부가 없으면 가수부는 필수이다.
  4. 가수부가 없으면 소수점은 선택이다.
문제 8 정답보기

r'[+-]?(\b[0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+\b)?'


정수부와 가수부가 동시에 없으면 안 되는 조건은 조건문을 사용해도 되고, 다자택일을 사용해도 된다. 이 경우는 다자택일을 사용했다.


로마 숫자

문제 9: 현대식 로마 숫자에 일치되는 정규식을 작성하라. ‘IV’는 가능하고, ‘IIII’는 불가능하다.

문제 9 정답보기

r'\b(?=[MDCLXVI])M*(C[MD]|D?C*)(X[CL]|L?X*)(I[XV]|V?I*)\b'


  1. (?=[MDCLXVI]): 로마 숫자는 0이 없기 때문에, 문자가 하나라도 있는지 체크하는 전방탐색 구문이다.
  2. M*: 로마 숫자에서 1000은 개수만큼 집어넣는다.
  3. ` (C[MD] D?C*)`: 작은 숫자가 앞에 오는 경우를 따로 빼서 다자택일로 처리하였다. 로마 숫자의 규칙을 생각하면 그리 어려운 부분은 아니다.
  4. 나머지 부분은 3번과 같다.

다음 글에서는 단어와 행 처리에 관한 예제를 다루도록 하겠다.

Comment  Read more

파이썬 정규표현식(re) 사용법 - 06. 치환 함수, 양방탐색, 조건문

|

파이썬 정규표현식(re) 사용법 - 01. Basic
파이썬 정규표현식(re) 사용법 - 02. 문자, 경계, flags
파이썬 정규표현식(re) 사용법 - 03. OR, 반복
파이썬 정규표현식(re) 사용법 - 04. 그룹, 캡처
파이썬 정규표현식(re) 사용법 - 05. 주석, 치환, 분리
파이썬 정규표현식(re) 사용법 - 06. 치환 함수, 양방탐색, 조건문
파이썬 정규표현식(re) 사용법 - 07. 예제(숫자)
파이썬 정규표현식(re) 사용법 - 08. 예제(단어, 행)
파이썬 정규표현식(re) 사용법 - 09. 기타 기능


이 글에서는 정규표현식 고급 기술에 대해서 설명한다.

본 글에서 정규표현식은 regex와 같이, 일반 문자열은 ‘regex’와 같이 표시하도록 한다.

파이썬 버전은 3.6을 기준으로 하나, 3.x 버전이면 (아마) 동일하게 쓸 수 있다.
2.7 버전은 한글을 포함한 비 알파벳 문자 처리가 다르다.


정규표현식 고급: 치환 함수 사용

01

여러분은 re.sub을 쓸 때 특정 문자열 혹은 정규식 내의 일부분은 그대로 갖다 쓰는 법을 배웠다.

그런데 일치부에 나타나지도 않고, literal text에도 나타나지 않는 문자열로 치환하고 싶은 경우가 있을 수 있다.
예를 들면 문자열 안에서 소수로 표현된 숫자를 찾은 다음 퍼센티지(%)로 변환하는 것을 정규식으로 쓴다고 하자.

re.sub 메서드는 인자 repl을 받을 때 정규식 일치부나 literal text 말고도 함수를 받을 수도 있다.
이 함수는 인자로 matchObj를 받으며, 문자열을 최종적으로 반환해야 한다.

예시를 보자.

def convertToPercentage(matchObj):
    number = float(matchObj.group())
    return str(number * 100) + '%'

print(re.sub(pattern=r'\b0\.\d+\b',
             repl=convertToPercentage,
             string='Red 0.250, Green 0.001, Blue 0.749, Black 1.5'))

결과

Red 25.0%, Green 0.1%, Blue 74.9%, Black 1.5

아주 어려운 내용이라서 advanced인 것은 아니고, 활용도가 높기 때문에 고급 기능으로 분류하였다.


정규표현식 중급: 행 단위 검색

행 단위로 자르는 것은 사실 어렵지 않다.

string = '''java - Click on button until alert is present in Selenium	2017년 5월 23일
Click OK on alert Selenium Webdriver	2016년 6월 5일
python - Click the javascript popup through webdriver	2012년 5월 29일
selenium - Read Message From Alert and click on OK	2012년 5월 17일
stackoverflow.com 검색결과 더보기'''

lines = re.split('\r?\n', string)
reObj = re.compile('(\d+)년\s+(\d+)월\s+(\d+)일')

for line in lines:
    matchObj = reObj.search(line) 
    if matchObj:
        string = matchObj.group(1) + '.' + matchObj.group(2) + '.' + matchObj.group(3)
        print(string)
    else:
        print('Not exists')

결과

2017.5.23
2016.6.5
2012.5.29
2012.5.17
Not exists

그러나 한 가지 주의할 점은 운영체제(OS: Windows, Mac, Linux)별로 라인피드(개행문자)가 다르다는 것이다. Windows는 ‘\r\n’를 행 구분자로 가지며, 각각 캐리지 리턴과 라인피드이다. 유닉스 계열인 Mac과 Linux는 ‘\n’을 가진다.
이 구분이 절대적인 것은 아니지만, 운영체제의 개행문자가 뭔지 확신이 서지 않는다면 \r?\n으로 쓰는 것이 안전하다.


정규표현식 고급: 전방탐색과 후방탐색

먼저 전방탐색이란 정규표현식이 진행하는 (원래) 방향으로, 문자열 처음에서 끌으로 가는 방향으로 탐색하는 것이다.
물론 후방탐색은 그 반대이다.

전방/후방탐색, 즉 양방탐색은 그 자체로 문자열에 일치된다기보다는, 문자열이 일치되기 위한 조건을 설정하는 것이다.
이미 여러분은 실제 문자열에 일치되지 않으면서 조건 설정을 하는 정규표현식을 몇 개 봐 왔다. \b, \A, ^ 등이 그 예이다.

어쨌든 양방탐색의 종류는 네 가지이다. 각각 긍정/부정 전방/후방 탐색이다.

  1. 긍정 전방탐색은 (?=<regex>)
  2. 긍정 후방탐색은 (?<=<regex>)
  3. 부정 전방탐색은 (?!<regex>)
  4. 부정 후방탐색은 (?<!<regex>)

으로 쓴다.

<p> 태그 안의 모든 텍스트를 검사한다고 하자. 그러나 여러분은 <p> 태그까지 문자열로 뽑아내고 싶지는 않을 것이므로, 이는 정규식의 일치부에 넣지 않도록 한다. 이때 후방탐색과 전방탐색을 문자 덩어리 앞뒤에 쓰면, <p> 태그 안이라는 조건을 설정하면서 동시에 <p> 태그는 일치부에서 제외시킬 수 있다.

예시를 보자.

print(re.search('(?<=<p>)\w+(?=</p>)', 
                'Kakao <p>ryan</p> keep a straight face.').group())

결과

ryan

<p> 태그는 제외된 것을 볼 수 있다.
위의 예시를 해석하면, 단어 문자에 해당하는 연속된 글자를 찾는데(\w+), 그 앞에는 <p> 문자열이 반드시 있어야 하고, 문자열 바로 뒤에는 역시 </p> 태그가 있어야 한다는 뜻이다.

실제로 문자열이 일치되는 것은 아님을 보여주기 위해, 쓸데없이 복잡하게 만든 예시를 하나 만들어 보았다.

print(re.search('<p>(?<=<p>)(\w+)(?=</p>)</p>',
                'Kakao <p>ryan</p> keep a straight face.').group(1))

결과

ryan

이러한 양방탐색 조건을 지정하는 것도, 명시적인 캡처로 인식되지 않는다.

부정 양방탐색도 사용법은 같다. 하지만 긍정 양방탐색은 <regex>에 해당하는 문자열이 있어야만 일치에 성공하는 데 반해, 부정 양방탐색은 <regex>와 일치에 실패해야만 그 다음 비교를 이어나간다.

보통 사람들은 비밀번호를 설정할 때 영문자 몇 개, 숫자 몇 개(보통 생일과 관련이 있다), 그리고 특수문자를 넣으라는 홈페이지의 요구대로 느낌표 하나를 끝에 붙인다.
그런데 여기서는 특수문자조차 없는 비밀번호를 찾으려고 한다. 즉 마지막에 ‘!’가 없는 비밀번호를 모두 찾는다고 하자.

이를 찾는 정규식은 다음과 같다.

print(re.findall(r'[a-z]+\d+(?!!)\b', 'tube1109! gorio303 ryan416'))

결과

['gorio303', 'ryan416']

느낌표가 없는 비밀번호는 두 개이다. 그리고 이를 잘 찾아 주었다.

물론 이렇게 간단한 데 양방탐색을 지정하여 쓰면 오히려 더 귀찮다. 하지만 이 기능을 쓸 데가 언젠가는 있을 것이다.

참고로, 파이썬의 후방탐색은 좀 비효율적으로 구현되어 있다고 알려져 있다. 따라서 후방탐색을 써야만 하는 경우에는 다음과 같이 대체하는 방법도 고려할 수 있다.

조금 전 <p> 태그 예시를 대체한 것이다.

print(re.search('<p>(\w+)</p>',
                'Kakao <p>ryan</p> keep a straight face.').group(1))

결과

ryan

이 방법이 더 깔끔할 수 있다.


정규표현식 고급: 조건문

이 부분은 한빛미디어의 ‘한 권으로 끝내는 정규표현식’ 책을 참고하였다.

여러분은 프로그래밍 언어에서 삼항 연산자로 조건문을 쓰는 것을 본 적이 있을 것이다.

C++의 예시는 다음과 같다.

x = a % 2 == 1 ? 3 : 4;

파이썬은 다음과 같다.

x = 1 if a % 2 == 1 else 4

정규표현식에서의 조건문은 다음과 같다. 조건문은, 캡처 그룹을 사용해 앞에서 문자열이 일치되었는지 아닌지에 따라 다음 부분에서는 다른 일치 조건을 제시해야 할 때 쓰인다.

(?(숫자)맞으면|아니면)

맞으면 또는 아니면에는 어떤 정규식이든 사용할 수 있다. 다만 그 안에서도 다자택일(|, OR)을 사용하려면 그 전체를 ( )로 묶어주어야 한다.

참고로 (숫자)는 여러분이 알고 있는 그 캡처랑 비슷한 것이다. 재참조부를 쓸 때 \1과 같이 썼는데, 조건문에서는 단지 (1)로 바뀌었을 뿐이다.

예시로, (a)?b(?(1)c|d)를 살펴보자. 이는 abc|bd와 같다.

  1. 먼저 a를 검사한다. 만약 찾는 문자열에 ‘a’가 있으면 첫 번째 명시적 캡처에는 ‘a’가 들어간다. 만약 ‘a’가 없으면, 빈 문자열이 (1)에 저장된다.
  2. 그 다음은 b이다. 만약 ‘b’가 없으면 일치에 실패하고 다음 위치로 넘어가게 된다. ‘b’가 있다고 가정하자.
  3. 그리고 이제 조건문이다. 만약 앞에서 ‘a’가 일치되었으면, 좀 전 일치된 ‘b’ 바로 다음에 ‘c’가 있는지를 검사한다. 만약 ‘a’가 없었으면, ‘b’ 다음에 ‘d’가 있는지를 찾게 된다.

다른 굉장히 복잡한 예시를 하나 들도록 하겠다(출처는 위에서 밝힌 책이다).

  1. 콤마로 구분되고
  2. one, two, three와 일치되되
  3. 각 단어는 최소 한번씩은 등장해야 하며
  4. 각 단어가 몇 개가 있든지 일치되어야 한다.

정답은

r'\b(?:(?:(one)|(two)|(three))(?:,|\b)){3,}(?(1)|(?!))(?(2)|(?!))(?(3)|(?!))'

이다.

하나씩 살펴보면,

  1. \b: 단어 경계는 이제 알 것이다.
  2. (?:(one)|(two)|(three)): one이나 two이거나 three를 캡처한다. 각각 따로 캡처되도록 바깥쪽 캡처 그룹은 비 캡처 그룹으로 지정하였다.
  3. (?:,|\b): 문제의 조건에서 각각의 단어는 콤마(‘,’)로 구분되고 마지막 단어 뒤에는 콤마가 없을 것이므로, , 또는 \b 다자택일로 묶어 놓았다. 콤마를 캡처하고 싶지는 않으므로 역시 비 캡처 그룹이다.
  4. (?:2번 3번){3,} 너무 길어서 2번과 3번을 따로 뺐다. 위에서 설명한 2번과 3번 전체를 감싸는 비 캡처 그룹이다. 세 단어가 각각 한 번씩은 나와야 하기 때문에, 반복 횟수는 최소 3이다.
  5. (?(1)|(?!)): 이제부터 조건문이다. 이 조건문은 캡처 그룹 1번((1), ‘one’)이 일치되었으면 그냥 일치되는 부분이다(‘맞으면’ 부분이 빈 문자열이다). ‘아니면’ 부분은 빈 부정 전방탐색 (?!)인데, 빈 정규식은 항상 일치되므로(즉, (1)에) 부정 전방탐색은 항상 실패한다. 따라서 이 조건문 (?(1)|(?!))(1)에 ‘one’이 일치되거나, 아니면 그냥 일치에 실패한다.
  6. 나머지 두 개의 조건문은 각각 ‘two’, ‘three’에 대응하며, 5번 설명과 같다. 이 세 가지 조건문을 조합하면, (1), (2), (3) 중 하나라도 일치에 실패한 단어가 있다면 이 전체 정규식은 일치에 실패한다. 따라서 세 단어가 한 번이라도 나와야 한다는 조건을 만족시킨다.

조건문 안에 양방탐색을 사용할 수도 있다. 이는 보통의 정규식과 거의 비슷하게 작동한다. 정규식 대신 양방탐색 조건이 일치하면 ‘맞으면’ 부분이, 일치에 실패하면 ‘아니면’ 부분이 바로 다음 문자열에 일치되는지를 시도한다.
부정 양방탐색을 쓸 수도 있으나, ‘맞으면’과 ‘아니면’ 부분이 바뀌는 효과를 가져오므로 지양하자.


다음 글부터는 예제를 다루도록 하겠다. 7번째 글은 숫자를 처리하는 예제에 관한 글이다.

Comment  Read more

파이썬 정규표현식(re) 사용법 - 05. 주석, 치환, 분리

|

파이썬 정규표현식(re) 사용법 - 01. Basic
파이썬 정규표현식(re) 사용법 - 02. 문자, 경계, flags
파이썬 정규표현식(re) 사용법 - 03. OR, 반복
파이썬 정규표현식(re) 사용법 - 04. 그룹, 캡처
파이썬 정규표현식(re) 사용법 - 05. 주석, 치환, 분리
파이썬 정규표현식(re) 사용법 - 06. 치환 함수, 양방탐색, 조건문
파이썬 정규표현식(re) 사용법 - 07. 예제(숫자)
파이썬 정규표현식(re) 사용법 - 08. 예제(단어, 행)
파이썬 정규표현식(re) 사용법 - 09. 기타 기능


이 글에서는 정규표현식 중급 기술과 python library인 re 패키지 사용법에 대해서 설명한다.

본 글에서 정규표현식은 regex와 같이, 일반 문자열은 ‘regex’와 같이 표시하도록 한다.

파이썬 버전은 3.6을 기준으로 하나, 3.x 버전이면 (아마) 동일하게 쓸 수 있다.
2.7 버전은 한글을 포함한 비 알파벳 문자 처리가 다르다.


정규표현식 중급: 주석 추가

복잡한 정규식은 써 놓고 나중에 보면 한 줄밖에 안 되는 주제에 다른 스파게비 코드만큼이나 읽기 힘들다. 위에 주석으로 한 줄 이 정규식의 의미를 써놓는 게 부족한 경우가 있을지도 모른다. 그래서 정규식 내부에 주석을 넣는 기능이 있다.

주석은 특별히 어려운 부분이 아니다. 단지 옵션 중 하나인 re.VERBOSE 옵션을 사용하기만 하면 된다. 약자는 re.X이다.

위 옵션을 추가하면 정규식 내의 공백들이 무시된다.

print(re.search(r'''
010-# 핸드폰 앞자리 
\d{4}-# 중간자리
\d{4}# 뒷자리''',
                '010-1234-5678',
                re.VERBOSE).group())

결과

010-1234-5678

인라인으로 쓰기 위해서는 모드 변경자 (?x)를 쓰면 된다.

위의 예시처럼 re.VERBOSE를 쓸 때는 삼중따옴표를 쓰는 것이 가장 좋다.
또 일반 공백 문자는 무시되기 때문에, 정규식 내에 공백문자를 넣고 싶으면 \ (공백문자를 이스케이프 처리)를 사용하거나 [ ]처럼 대괄호 내에 공백문자를 집어넣으면 된다.
탭 문자는 \t로 동일하고, 개행 문자는 \n은 무시되기 때문에(예시에선 개행을 했음에도 문자열이 이를 무시하고 일치되었다) '\\n' 또는 r'\n'으로 하는 것이 라인피드와 일치된다.


정규표현식 중급: 치환

사실 치환도 어려운 개념은 아니지만, 활용하는 방법은 꽤 많기 때문에 중급으로 분류하였다.

치환은 말 그대로 정규식에 일치하는 부분 문자열을 원하는 문자열로 치환하는 것이다.
파이썬 문자열은 기본적으로 replace 메서드를 갖고 있기 때문에 일반적인 문자열은 그냥 치환이 가능하다.

origin_str = 'Ryan keep a straight face.'
edited_str = origin_str.replace('keep', 'kept')
print(edited_str)

결과

Ryan kept a straight face.

그러나 이 replace는 정규식 패턴에 대응하는 문자열을 찾아주지는 못한다. 그래서 re.sub 메서드가 필요하다.

re.sub(pattern, repl, string, count, flags)

01

간단한 사용법은 다음과 같다.

print(re.sub('\d{4}', 'XXXX', '010-1234-5678'))

결과

010-XXXX-XXXX

인자만 보아도 대략 감이 올 것이다. pattern, string, flags는 우리가 지금까지 써 왔던 것들이다.

나머지 두 개도 어려운 부분은 아니다. re.sub은 패턴에 일치되는 문자열은 다른 문자열로 바꿔주는 것이므로, repl은 당연히 그 ‘다른 문자열’에 해당하는 부분이다.

count 인자는, 최대 몆 개까지 치환할 것인가를 지정한다. 만약 일치되는 문자열이 3인데 count=2로 지정되어 있으면 마지막 세 번째 문자열은 치환되지 않는다.
물론 일치되는 문자열이 count보다 적으면 그냥 전부 다 치환된다.

print(re.sub(pattern='Gorio', repl='Ryan', count=2, \
             string='Gorio, Gorio, Gorio keep a straight face.'))

결과

Ryan, Ryan, Gorio keep a straight face.

인자가 많으므로 이번엔 파이썬의 특징인 인자 명시적 지정을 사용해 보았다.

결과에서 이해되지 않는 부분은 딱히 없을 것이다.
참고로 re.sub은 일치된 위치를 따로 반환해 주지 않는다.

re.subn(pattern, repl, string, count, flags)

02

re.subnre.sub과 매우 유사하지만, 리턴하는 값이 치환된 문자열과 더불어 치환된 개수의 튜플이라는 것이 다른 점이다.

print(re.subn(pattern='Gorio', repl='Ryan', count=2, \
              string='Gorio, Gorio, Gorio keep a straight face.'))

결과

('Ryan, Ryan, Gorio keep a straight face.', 2)

문자열이 두 개 치환되었으므로 2를 같이 리턴한다.

정규식 일치부를 문자열에서 제거

왜 파이썬에서는 제거 메서드를 따로 만들지 않았는지는 잘 모르지만, re.sub으로 간단히 구현 가능하다.

print(re.sub('Tube', '', 'Tube Ryan'))

결과

 Ryan

문자열을 제거할 때 공백 문자 하나가 남는 것은 흔히 하는 실수이다. 결과를 보면 ‘Ryan’ 앞에 공백 문자가 하나 있는데, 이를 실제 상황에서 본다면 은근히 신경 쓰일 것이다. 제거할 문자열 앞이나 뒤에 공백 문자를 하나 넣어서 같이 제거하도록 하자.

치환 텍스트에 정규식 일치부 삽입

이번에는 문자열 치환 시 그냥 literal text가 아닌 일치된 부분을 재사용하는 방법을 알아보겠다. 재참조부와 약간 비슷한 개념이다.

URL을 markdown link로 변환해 보겠다. Markdown link는

[이름](URL)

이렇게 구성된다.

파이썬에서 전체 일치부를 치환 텍스트에 삽입하려면 \g<0>이라는 토큰을 사용해야 한다.

print(re.sub('https?://\S+',
             '[링크](\g<0>)',
             'http://www.google.com and https://greeksharifa.github.io'))

결과

[링크](http://www.google.com) and [링크](https://greeksharifa.github.io)

치환 텍스트에 정규식 부분 일치부 삽입

여러분은 여기에서 재참조부 일부를 사용하는 방법을 배웠다. 그러면 치환 텍스트에도 이처럼 일부분을 삽입하는 방법이 있을 것이다.

print(re.sub('(\d{4})-(\d{2})-(\d{2})', 
             r'\1.\2.\3',
             '1900-01-01'))

결과

1900.01.01

yyyy-mm-dd 형식이 yyyy.mm.dd 형식으로 바뀌었다.
\1과 같은 문법을 쓸 때에는 r prefix를 붙여야 한다는 것을 기억하고 있을 것이다.

만약 위와 같은 상황에서 \4를 사용한다면, 아무것도 캡처된 것이 없으므로 에러 메시지를 볼 수 있을 것이다.

명명 그룹을 사용한 경우

조금 위에서 \g<0>을 사용했었다. 그러면 명명 그룹의 경우 \g<name>을 사용한다는 것을 눈치챌 수 있을 것이다. 비 명명 그룹은 \g<1>, \g<2>, … 을 사용한다.

print(re.sub('(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
             '\g<year>.\g<month>.\g<day>',
             '1900-01-01'))

결과

1900.01.01

물론 명명 그룹과 비 명명 그룹을 혼용하는 것은 여러 번 말했듯이 좋은 생각이 아니다.


정규표현식 중급: split

re.sub 말고도 유용한 함수는 re.split이다. 이 메서드는 파이썬 문자열의 기본 메서드인 split과 매우 유사하나, 정규식을 처리할 수 있다.

이 역시 어려운 함수는 아니기 때문에, 예시 하나를 바로 보도록 하겠다.
html 태그 내에서 태그를 제외한 부분으로 split하는 예제이다.

print(re.split('<[^<>]*>',
               '<html> Wow <head> header </head> <body> Hey </body> </html>'))

결과

['', ' Wow ', ' header ', ' ', ' Hey ', ' ', '']

물론 이렇게만 하면 빈 문자열 등도 많이 나온다. 이는 정규식으로 따로 처리하거나, 다음과 같이 쓰면 된다.

result = re.split('<[^<>]*>',
                  '<html> Wow <head> header </head> <body> Hey </body> </html>')

result = list(map(lambda x: x.strip(), result))
result = list(filter(lambda x: x != '', result))
print(result)

결과

['Wow', 'header', 'Hey']

정규식이 깔끔하긴 하지만, 한 번에 모든 것을 처리하려고 하면 힘들 수 있다. 파이썬 기본 기능도 잘 활용하자.


정규표현식 초급: re.compile

여기서는 re.compile 메서드를 알아볼 것이다.

여러분은 지금까지 import re를 사용하여 re로부터 직접 메서드를 호출해왔다. re.match, re.sub 등이 그 예시이다.

이 방식은 한두번 쓰고 말기에는 괜찮지만, 같은 정규식 패턴을 반복문 내에서 반복적으로 사용해야 할 경우 성능상의 부담이 있다.
이는 정규식은 컴파일이란 과정을 거치기 때문인데, re 모듈로부터 직접 갖다 쓰면 매번 컴파일이란 비싼 계산을 해야 하기 때문에 성능이 떨어진다.

re.compile은 컴파일을 미리 해 두고 이를 저장할 수 있다. 예시를 보자.

여러분은 지금까지 이렇게 해 왔다.

print(re.search(r'\b\d+\b', 'Ryan 1 Tube 2 345'))

이를 한 번 정도 쓰는 것이 아닌, 반복문 내에서 여러 번 쓴다면 이렇게 쓰는 것이 좋다.

with open('ryan.txt', 'r', encoding='utf-8') as f_in:
    reObj = re.compile(r'\b\d+\b')
    for line in f_in:
        matchObj = reObj.search(line)
        print(matchObj.group())

미리 컴파일해 두면 성능상의 이점이 있다.

사용법이 조금 달라진 것이 눈에 띌 것이다.

여러분은 지금까지,

  1. re 모듈로부터 직접 match, search 등 메서드를 써 왔다.
    • 인자는 기본적으로 1) 정규식 패턴과 2) 찾을 문자열이 있었다.

컴파일을 미리 하는 버전은,

  1. re.compile 메서드로부터 reObj를 만든다.
    • 인자는 기본적으로 1) 정규식 패턴 하나이다.
  2. reObj.match 혹은 search 등으로 문자열을 찾는다.
    • 인자는 정규식 패턴은 이미 저장되어 있으므로 search 메서드에는 1) 찾을 문자열 하나만 주면 된다.

reObj가 무슨 정규식 패턴을 가졌는지 보려면 다음을 수행해 보라.

print(re.compile('\d+'))

결과

re.compile('\\d+')

’'를 구분하기 위해는 ‘' 도 \에 의해 이스케이프 처리되어야 하므로 ‘'가 두 개 있다는 점을 주의하라.


다음 글에서는 정규표현식 고급 기술을 다루도록 하겠다.

Comment  Read more

파이썬 정규표현식(re) 사용법 - 04. 그룹, 캡처

|

파이썬 정규표현식(re) 사용법 - 01. Basic
파이썬 정규표현식(re) 사용법 - 02. 문자, 경계, flags
파이썬 정규표현식(re) 사용법 - 03. OR, 반복
파이썬 정규표현식(re) 사용법 - 04. 그룹, 캡처
파이썬 정규표현식(re) 사용법 - 05. 주석, 치환, 분리
파이썬 정규표현식(re) 사용법 - 06. 치환 함수, 양방탐색, 조건문
파이썬 정규표현식(re) 사용법 - 07. 예제(숫자)
파이썬 정규표현식(re) 사용법 - 08. 예제(단어, 행)
파이썬 정규표현식(re) 사용법 - 09. 기타 기능


이 글에서는 정규표현식 중급 기술과 python library인 re 패키지 사용법에 대해서 설명한다.

본 글에서 정규표현식은 regex와 같이, 일반 문자열은 ‘regex’와 같이 표시하도록 한다.

파이썬 버전은 3.6을 기준으로 하나, 3.x 버전이면 (아마) 동일하게 쓸 수 있다.
2.7 버전은 한글을 포함한 비 알파벳 문자 처리가 다르다.


정규표현식의 중급: 그룹, 캡처 = ( )

소괄호 ( )에는 중요한 기능이 두 가지 있다. 그룹화와 캡처인데, 정규식의 여러 문자를 그룹으로 묶어주는 것과 정규식의 일부분에 해당하는 문자열에만 관심이 있을 때 그 부분을 따로 빼서 캡처하는 기능이다.
여담으로 그룹화는 기초 과정이지만 캡처와 더불어 중급 과정에 넣었다.

그룹화

그룹화는 말 그대로 그룹으로 묶어주는 것이다. 지금까지의 글에서는 정규식 메타문자들의 효력은 대개 한 문자에만 적용이 되었다.

print(re.findall('12+', '12 1212 1222'))

결과

['12', '12', '12', '1222']

‘1212’와 같은 문자열을 찾고 싶었는데, ‘12’ 혹은 ‘1222’만 찾아진다. 즉 메타문자 +2에만 적용이 된 것이다. 이를 12 모두에 적용시키려면 소괄호 ( )로 그룹화시켜주면 된다.

print(re.match('(12)+', '1212'))
print(re.search('(12)+', '1212'))
print(re.findall('(12)+', '1212'))
print(re.fullmatch('(12)+', '1212'))

결과

<_sre.SRE_Match object; span=(0, 4), match='1212'>
<_sre.SRE_Match object; span=(0, 4), match='1212'>
['12']
<_sre.SRE_Match object; span=(0, 4), match='1212'>

정규식은 항상 최대로 일치시키는 쪽으로 문자열은 탐색하기 때문에, ‘12’가 아닌 ‘1212’를 잘 찾았다. 그런데 한 가지 이상한 결과는 re.findall 결과이다.

다른 예시를 한번 보자.

print(re.findall('A(12)+B', 'A12B'))
print(re.findall('A(12)+B', 'A1212B'))
print(re.findall('A(12)+B', 'A121212B'))
print(re.findall('A(12)+B', 'A12121212B'))

결과

['12']
['12']
['12']
['12']

‘A’와 ‘B’를 통해 문자열 전체가 정규식과 일치된 것을 확인할 수 있으나, ‘12’가 몇 개인지에 관계없이 딱 ‘12’만 일치되어 결과로 반환되었다. 이는 괄호가 가진 다른 기능인 캡처 때문이다.

캡처

캡처란 원하는 부분만을 추출하고 싶을 때 사용하는 것이다. 예를 들어 ‘yyyy-mm-dd’와 같이 날짜를 나타내는 문자열 중 월, 일을 각각 따로 빼서 쓰고 싶다고 하자.
그러면 따로 빼고 싶은 부분인 ‘mm’과 ‘dd’ 부분에만 소괄호의 캡처 기능을 사용하면 된다.

print(re.findall('\d{4}-(\d\d)-(\d\d)', '2028-07-28'))
print(re.findall('\d{4}-(\d\d)-(\d\d)', '1999/05/21 2018-07-28 2018-06-31 2019.01.01'))

결과

[('07', '28')]
[('07', '28'), ('06', '31')]

월과 일에 해당하는 부분만 따로 빠졌음을 알 수 있다. 그리고 날짜 형식이 맞지 않는 경우에는 아예 캡처되지 않았음을 확인할 수 있다.

여기서 한 가지 문제점은, 6월 31일은 존재하지 않는 날짜란 점이다. 위의 정규식은 숫자로만 처리를 했기 때문에 ‘9999-99-99’도 일치된다는 문제가 있다. 이러한 문제를 해결하는 방법은 함수를 정규식에 쓰는 것인데, 이 방법에 대해서는 나중에 알아보도록 한다.

matchObj.groups()

여러분은 첫 번째 글에서 다음 예시를 보았을 것이다.

matchObj = re.search('match', "'matchObj' is a good name, but 'm' is convenient.")
print(matchObj)

print(matchObj.group())
print(matchObj.start())
print(matchObj.end())
print(matchObj.span())
# matchObj를 오랜만에 가져와 보았다. 캡처를 잘 쓰기 위해서는 matchObj가 필요하다.

결과

<_sre.SRE_Match object; span=(1, 6), match='match'>
match
1
6
(1, 6)

이제 정규식을 캡처를 포함한 식으로 바꿔보자.

matchObj = re.search('match', "'matchObj' is a good name, but 'm' is convenient.")
print(matchObj)

print(matchObj.group())
print(matchObj.groups())

print('# ---------------------------------------------------------------- #')

m = re.search('\d{4}-(\d?\d)-(\d?\d)', '1868-12-10')
print(m)

print(m.group())
print(m.groups())

결과

<_sre.SRE_Match object; span=(1, 6), match='match'>
match
()
# ---------------------------------------------------------------- #
<_sre.SRE_Match object; span=(0, 10), match='1868-12-10'>
1868-12-10
('12', '10')

matchObj의 group 메서드는 정규식 전체의 일치부를 찾는다. 반면에 groups 메서드는 명시적으로 캡처(( )로 감싼 부분)한 부분을 반환한다.

위의 matchObj는 캡처 구문이 없기 때문에 groups 결과가 빈 튜플이 되는 것이다.
반면 m의 경우 월과 일에 해당하는 부분을 반환하였다.

groupgroups의 사용법을 좀 더 보도록 하자.

01

m = re.search('\d{4}-(\d?\d)-(\d?\d)', '1868-12-10')
print('m:', m)

print('m.group():', m.group())

for i in range(0, 3):
    print('m.group({}): {}'.format(i, m.group(i)))

print('m.groups():', m.groups())

결과

m: <_sre.SRE_Match object; span=(0, 10), match='1868-12-10'>
m.group(): 1868-12-10
m.group(0): 1868-12-10
m.group(1): 12
m.group(2): 10
m.groups(): ('12', '10')

결과를 보면 대략 사용법을 알 수 있을 것이다.

  1. group(i)는 i번째 소괄호에 명시적으로 캡처된 부분만을 반환한다.
  2. group(0)은 전체 일치부를 반환하며, group()과 효과가 같다.
  3. groups()는 명시적으로 캡처된 모든 부분 문자열을 반환한다.

i번째 캡처된 부분은, i번째 여는 괄호와 대응된다고 생각하면 된다. 캡처를 중첩해서 사용하는 경우((12)+), 첫 번째 캡처는 바깥쪽 소괄호이다.

주의할 점은 group(0)이 0번째 캡처를 의미하는 것이 아니라 전체 일치부를 반환한다는 것이다.


비 캡처 그룹

그룹화를 위해 소괄호를 반드시 써야 하는데, 굳이 캡처하고 싶지는 않을 때가 있다. 예를 들어 다음과 같이 쓴다고 하자.

matchObj = re.search('((ab)+), ((123)+) is repetitive\.', 'Hmm... ababab, 123123 is repetitive.')
print(matchObj.group())
print(matchObj.group(1))
print(matchObj.group(2)) # don't want
print(matchObj.group(3)) 
print(matchObj.group(4)) # don't want

결과

ababab, 123123 is repetitive.
ababab
ab
123123
123

캡처 기능을 사용할 때 위의 ‘ababab’, ‘123123’을 얻고 싶을 뿐 ‘ab’나 ‘123’을 얻고 싶지는 않을 때가 있다. 그러나 소괄호는 기본적으로 캡처 기능을 갖고 있기 때문에 group(2)에는 ‘123123’ 대신 ‘ab’가 들어가 있다.
이는 원하는 결과가 아닐 때가 많다. 그래서 정규표현식은 비 캡처 기능을 지원한다.

비 캡처 그룹은 (?:<regex>)와 같이 사용한다. 위의 예시를 다시 써 보자.

matchObj = re.search('((?:ab)+), ((?:123)+) is repetitive\.', 'Hmm... ababab, 123123 is repetitive.')
print(matchObj.group())
print(matchObj.group(1))
print(matchObj.group(2))

결과

ababab, 123123 is repetitive.
ababab
123123

예상대로 동작하였다.

비 캡처 그룹의 장점은 캡처 그룹의 번호를 이상하게 만들지 않게 할 수 있다는 것과, 쓸데없는 캡처 그룹을 group의 반환값에 집어넣지 않게 되므로 성능상의 이점이 있다.
그러나 성능 향상은 보통 상황이라면 체감하기 어려울 정도이긴 하다.

참고로 모드 변경자나 비 캡처 그룹처럼 여는 소괄호 뒤에 ?가 있으면, 0회 또는 1회 반복이나 기타 다른 의미가 아닌 특별한 기능을 하는 토큰이 된다. 앞으로 이러한 토큰들을 여럿 볼 수 있을 것이다.

모드 변경자가 있는 그룹

여기에서 (?s)와 같은 모드 변경자를 본 적이 있을 것이다.

이러한 모드 변경자는 소괄호를 쓰긴 하지만 캡처 그룹으로 작동하지 않는다.

matchObj = re.search('case sensitive(?i) irrelevant', 'case sensitive IrreLEVant')
print(matchObj.group(0))
print(matchObj.group(1))

결과

case sensitive IrreLEVant
Traceback (most recent call last):
  File "<input>", line 3, in <module>
IndexError: no such group

\ (숫자): 앞서 일치된 문자열을 다시 비교

앞뒤가 똑같은 세 글자 단어를 찾는다고 해보자. 이를 위해서는 조금 전 살펴본 캡처가 꼭 필요하다.

i번째 캡처된 문자열은 group(i) 메서드를 통해 접근할 수 있다고 하였다. 그런데 그건 matchObj을 얻은 후의 얘기고, 정규식 내에서는 다른 방법을 쓴다. 바로 \(숫자)이다. 예를 들면 \1, \2, …이다.
이를 재참조부라 한다.

아마 그럴 리는 없겠지만 재참조부가 10개 이상인 경우 그냥 두 자리 수를 쓰면 된다. \10, \11, …

\b와 마찬가지로 \1과 같은 문법을 쓸 때에는 앞에 r prefix를 붙여 주어야 한다.

우선 예시를 보자. 단어 경계는 정규식이 더 복잡해 보이므로 일부러 넣지 않았다. 분리된 단어만을 보고 싶다면, \b를 넣으면 된다.

print(re.search(r'(\w)\w\1', '토마토 ABC aba xyxy ').group())
print(re.findall(r'(\w)\w\1', '토마토 ABC aba xyxy '))

결과

토마토
['토', 'a', 'x']

첫 번째 결과는 원하는 결과이다. 그러나 search는 하나밖에 찾지 못하므로 완벽한 답은 아니다.
두 번째 결과는 원하는 결과가 아닐 것이다. 이는 ( )가 들어가면 앞에서 말했듯 캡처 그룹만을 반환하기 때문이다.

전체를 참조하려면 여러 방법이 있지만, 세 가지를 소개한다.

첫 번째는 search로 하나를 찾은 다음 남은 문자열로 다시 search를 하는 것이다. 그러나 이는 괜한 코딩량이 늘어난다.

두 번째는 캡처를 하나 더 만드는 것이다.

match_list = re.findall(r'((\w)\w\2)', '토마토 ABC aba xyxy ')

for match in match_list:
    print(match[0])

결과

토마토
aba
xyx

재참조부가 \1이 아니라 \2인 이유는, 여는 소괄호(opening parenthesis)의 순서를 잘 살펴보라. 바깥쪽 소괄호인, 전체를 감싸는 소괄호가 첫 번째 캡처 부분이다. 따라서 안쪽 (\w)\2에 대응된다.

그러나 이 방법은 나쁘지 않지만, findall로 찾기 때문에 위치를 찾아주지는 않는다는 단점이 있다.
일치부의 시작/끝 위치까지 알고 싶을 때에는 finditer을 이용한다.

matchObj_iter = re.finditer(r'((\w)\w\2)', '토마토 ABC aba xyxy ')

for matchObj in matchObj_iter:
    print('string: {}, \t start/end position={}, \t 반복 부분: {}'.
          format(matchObj.group(), matchObj.span(), matchObj.group(2)))

결과

string: 토마토, 	 start/end position=(0, 3), 	 반복 부분: 토
string: aba, 	 start/end position=(8, 11), 	 반복 부분: a
string: xyx, 	 start/end position=(12, 15), 	 반복 부분: x

참고로, 이러한 \1, \2, … 들은 비 명명 그룹이라고도 한다. 그 이유는, 바로 다음에 설명할 명명 그룹 때문이다.

명명 그룹

\1, \2, …는 간편하긴 하지만, 그다지 눈에 잘 들어오지는 않는다. 코딩할 때 변수명을 ‘a’, ‘b’ 같은 것으로 지어 놓으면 남이 알아보기 힘든 것과 갈다.

많은 프로그래밍 언어의 정규표현식은 명명 그룹 기능을 지원한다.
언어마다 쓰는 방법이 다르지만, 파이썬 기준으로는 (?P<name>regex) 형식으로 쓴다.

앞 절의 내용을 이해했으면 어려운 내용이 아니다.

예시를 하나 보자.
‘2018-07-28 2018.07.28’처럼, 형식만 다른 똑같은 날짜가 있는지를 확인하는 상황을 생각하자.

matchObj = re.match(
    r'(?P<year>\d{4})-(?P<month>\d\d)-(?P<day>\d\d) (?P=year)\.(?P=month)\.(?P=day)',
    '2018-07-28 2018.07.28')

print(matchObj.group())
print(matchObj.groups())
print(matchObj.group(1))

결과

2018-07-28 2018.07.28
('2018', '07', '28')
2018

명명 그룹의 재참조는 (?P=name) 형식으로 쓰면 된다.

사실 명명 그룹과 비 명명 그룹을 섞어 쓸 수는 있다.

matchObj = re.match(
    r'(?P<year>\d{4})-(?P<month>\d\d)-(?P<day>\d\d) (?P=year)\.\2\.\3',
    '2018-07-28 2018.07.28')

print(matchObj.group())

결과

2018-07-28 2018.07.28

하지만 기껏 가독성 높이려고 명명 그룹을 썼는데 저렇게 쓰면 가독성이 더 나빠진다. 지양하도록 하자.

한 가지 주의할 점은 name 부분은 \w에 일치되는 문자들로만 구성해야 한다. 그렇지 않으면 ‘invalid group name’이라는 메시지를 볼 수 있을 것이다.


반복 부분의 캡처

이 글의 앞부분에서 12를 반복시키려고 (12)+ 정규식을 썼는데 원치 않는 결과가 나온 것을 보았을 것이다.

print(re.findall('A(12)+B', 'A121212B'))

결과

['12']

위의 예시처럼 문자가 한 종류(12)로 정해져 있으면 그냥 전체에다 캡처 그룹을 하나 더 만드는 것으로 해결 가능하지만, 정해진 것이 아닌 문자 집합 같은 것이라면 꽤 어려워진다.

print(re.findall(r'\b(\d\d)+\b', '1, 25, 301, 4000, 55555'))

결과

['25', '00']

위의 예시는 길이가 짝수인 정수를 찾고 싶은 것이다.
그러나 ‘4000’ 대신 ‘00’을 찾고 싶은 사람은 별로 없을 것 같다.

이를 캡처 그룹으로 한번에 묶어내는 우아한 방법은 없지만, 다른 괜찮은 해결 방법은 있다.

matchObj_iter = re.finditer(r'\b(\d\d)+\b', '1, 25, 301, 4000, 55555')

for matchObj in matchObj_iter:
    print(matchObj.group())

결과

25
4000

stackoverflow에서 찾은 답변 중에는 패턴을 expand하거나 일치하는 부분만 잘라낸 다음 추가 처리를 하라는 답변이 있었는데, 그런 것보다는 위의 방법이 더 깔끔한 것 같다.


다음 글에서는 주석, 치환, 컴파일 등을 살펴보도록 한다.

Comment  Read more