세상의 변화에 대해 관심이 많은 이들의 Tech Blog search

파이썬 정규표현식(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번째 글은 숫자를 처리하는 예제에 관한 글이다.