Gorio Tech Blog search

파이썬 정규표현식(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

파이썬 정규표현식(re) 사용법 - 03. OR, 반복

|

파이썬 정규표현식(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’와 같이 표시하도록 한다.


정규표현식의 기초: OR, 반복

| : 다자택일

단어 ‘one’, ‘two’, ‘three’ 중 하나에 대응하고 싶다면 |를 쓰면 된다(백슬래시 또는 원화로 되어 있는 \ 키의 shift 버전이다).

matchObj = re.findall('one|two|three', 'one four two three zero')
print(matchObj)

결과

['one', 'two', 'three']

작동 과정을 살펴보자.

  1. 맨 앞에서 바로 ‘one’이 일치된다.
  2. 공백 한 개는 o, t 어느 것에도 일치되지 않으므로 건너뛴다. ‘f’도 마찬가지이다.
  3. ‘four’ 의 ‘o’에 도달했다. o는 일치되기 때문에, ‘u’에 n을 일치시켜본다. 물론 아니다.
  4. 계속 넘어가서 ‘two’의 ‘t’에 도달했다. ‘t’는 t에 일치된다.
  5. ‘w’에서는 wh 중 일치되는 것을 찾는다. 현재 tw까지 일치되었다.
  6. ‘o’까지 일치되어 ‘two`를 찾았다.
  7. 이와 비슷한 과정을 반복하여 ‘three’까지 찾고 종료한다.

일반적으로 |로 나열한 단어들의 순서가 중요하지는 않다. 하지만 중요한 순간이 있다.
다음 예시를 보자.

matchObj = re.findall('one|oneself|onerous', 'oneself is the one thing.')
print(matchObj)

결과

['one', 'one']

‘oneself’가 있음에도 oneself에 일치되지 않았다. 그 이유는 이미 ‘one’을 찾아버렸고, 정규식은 overlapping된 부분을 또 찾지 않기 때문에, ‘one’을 찾고 나서 남은 문자열은 ‘self is the one thing.’이다. 따라서 남은 문자열에서는 더 이상 oneself를 찾을 수 없는 것이다.

이 문제의 해결 방법은 두 가지다. 물론 더 있을 수도 있다.

  1. 당연하게도 더 긴 oneselfone 앞에다 두면 해결된다.
  2. 아니면 단어 경계를 활용한다. \bone\b|\boneself\b로 쓰면 된다.

* : 0회 이상 반복

어떤 문자나 기호 뒤에 *(asterisk)를 붙이면 그 문자가 일치되는 만큼 일치된다. 예를 들어 a*의 경우 ‘a’나 ‘aaa’ 혹은 ‘‘(빈 문자열)과도 일치된다.

예시를 보자.

print(re.match('a*', ''))
print(re.match('a*', 'a'))
print(re.search('a*', 'aaaa'))
print(re.fullmatch('a*', 'aaaaaa'))
print(re.findall('a*', 'aaabaaa aa  '))

matchObj = re.search('<p>.*</p>', '<p> Lorem ipsum... is boring. </p>')
print(matchObj)

결과

<_sre.SRE_Match object; span=(0, 0), match=''>
<_sre.SRE_Match object; span=(0, 1), match='a'>
<_sre.SRE_Match object; span=(0, 4), match='aaaa'>
<_sre.SRE_Match object; span=(0, 6), match='aaaaaa'>
['aaa', '', 'aaa', '', 'aa', '', '', '']
<_sre.SRE_Match object; span=(0, 34), match='<p> Lorem ipsum... is boring. </p>'>

여섯 번째 결과의 경우, 파이썬 버전에 따라 None이 반환될 수도 있다.

그런데 한 가지 이상한 결과가 보인다. 다섯 번째 실행문이다.

print(re.findall('a*', 'aaabaaa aa  '))
# ['aaa', '', 'aaa', '', 'aa', '', '', '']

빈 문자열이 이상하리만큼 많이 매칭되었다. 굉장히 비직관적인 결과이지만, 빈 문자열에도 일치된다는 것을 생각했을 때 아예 틀린 것은 분명히 아니다.
매칭되는 빈 문자열들은 a가 아닌 다른 문자들과의 경계에서 발생한다고 생각하면 될 듯하다. 하지만, 아마 대부분 이것은 원하는 결과가 아닐 것이기 때문에, ‘a’ 덩어리를 찾고 싶다면 다음 메타문자를 보자.

+ : 1회 이상 반복

*과 비슷하지만 무조건 한 번이라도 등장해야 한다. 위와 거의 같은 예시를 보자.

print(re.match('a+', ''))
print(re.match('a+', 'a'))
print(re.search('a+', 'aaaa'))
print(re.fullmatch('a+', 'aaaaaa'))
print(re.findall('a+', 'aaabaaa aa  '))

matchObj = re.search('<p>.+</p>', '<p> Lorem ipsum... is boring. </p>')
print(matchObj)

결과

None
<_sre.SRE_Match object; span=(0, 1), match='a'>
<_sre.SRE_Match object; span=(0, 4), match='aaaa'>
<_sre.SRE_Match object; span=(0, 6), match='aaaaaa'>
['aaa', 'aaa', 'aa']
<_sre.SRE_Match object; span=(0, 34), match='<p> Lorem ipsum... is boring. </p>'>

아마 이것이 여러분이 원하는 ‘a’ 덩어리를 찾은 결과일 것이다.
빈 문자열이 일치되지 않은 것을 기억하자.

{n, m} : 지정 횟수만큼 반복

중괄호는 지정한 횟수만큼 정규식을 반복시키는 것이다. 이 쓰임으로 중괄호를 쓸 때 쓰는 방법은 세 가지가 있다.

  1. {n} : 정확히 n회만큼 반복
  2. {n, m} : n회 이상 m회 이하 반복
  3. {n, } : n회 이상 반복. 무한히 일치될 수 있다.

물론 n은 자연수, m은 n보다 큰 정수이다. 그리 어렵지 않으므로 바로 예시를 보자.

print(re.search('a{3}', 'aaaaa'))
print(re.findall('a{3}', 'aaaaaaaa'))
print(re.findall('a{2,4}', 'a aa aaa aaaa aaaaa'))
print(re.findall('a{2,}', 'a aa aaa aaaa aaaaa'))

결과

<_sre.SRE_Match object; span=(0, 3), match='aaa'>
['aaa', 'aaa']
['aa', 'aaa', 'aaaa', 'aaaa']
['aa', 'aaa', 'aaaa', 'aaaaa']

예상과는 조금 다른 결과일지도 모르겠다. 오직 ‘aaa’만을 찾고 싶을 때 a{3}처럼 쓰면 ‘aaaaa’의 일부분인 ‘aaa’에도 일치될 수 있다. 따라서 정확히 ‘aaa’만을 찾으려면 \baaa\b처럼 단어 경계를 활용하는 쪽이 좋다.

참고로 {0, }*과 같고, {1,}+와 같다.

01

? : 0회 또는 1회 반복

이 메타문자도 어렵지는 않을 것이라 생각된다. ?{0,1}과 같다.

print(re.findall('ab?a', 'aa aba aaaa'))

결과

['aa', 'aba', 'aa', 'aa']

정규표현식은 항상 최대한 많은 부분을 일치시키려 한다는 것을 기억하자.

참고로, 앞에서 말한 반복 메타문자들(*, +, {n, m}, ? 등)을 정량자 또는 수량자라고 부른다.


Advanced: 탐욕 정량자 vs 나태 정량자

그리고 이런 정량자(수량자)들은 한 가지 중요한 특성이 있다.
일단 전체 문자열이 매치가 되도록 노력하고, 그 선을 지키는 선에서 일치되는 부분에는 최대한 많이 일치시키려고 한다. 즉 기본적으로 모든 정량자들은 탐욕적이며, 가능한 많은 문자열에 매치되려고 한다.

말이 복잡한데, 예시를 보면서 천천히 설명하도록 하겠다.

# 1번 예시
matchObj = re.search('<p>.*</p>', '<p> Lorem ipsum... is boring. </p>')
print(matchObj.group())

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

# 2번 예시
matchObj = re.search('<p>.*</p>', '''
<p> part 1 </p> part 2 </p>
<p> part 3 </p> part 4 </p>
''', re.DOTALL)
print(matchObj.group())

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

# 3번 예시
matchObj = re.search('<p>.*?</p>', '''
<p> part 1 </p> part 2 </p>
<p> part 3 </p> part 4 </p>
''', re.DOTALL)
print(matchObj.group())

결과

<p> Lorem ipsum... is boring. </p>
# ---------------------------------------------------------------- #
<p> part 1 </p> part 2 </p>
<p> part 3 </p> part 4 </p>
# ---------------------------------------------------------------- #
<p> part 1 </p>

전체 문자열이 매치가 되도록 노력한다.

  • 여러분은 조금 위에서 Lorem ipsum 예시를 보았을 것이다. 바로 위의 1번 예시는 이를 변형한 것이다.
    사실 마침표 .는 모든 문자에 일치되기 때문에, ‘</p>‘에 해당하는 부분도 마침표에 일치될 수 있다. 만약에 이 부분까지 .에 일치시켜 버린다면, .* 부분이 ‘<p>’ 뒤쪽의 모든 문자를 집어삼켜 버리고, 따라서 정규식의 남은 패턴인 </p> 부분은 대조해볼 문자열이 남아있지 않으므로 실패해야 한다고 생각할 수 있다.
  • 그러나, 정규식의 정량자들은 역행(backtracking)을 할 줄 안다. 이 말은, *+ 등은 탐욕적이기는 하지만, 전체 문자열에 일치되는 가능성마저 없애버리지는 않는다는 말과 갈다.
    1. 우선 .*가 모든 문자열을 집어삼켜 ‘</p>‘까지 해치웠다. 그러나, 정규식 패턴에는 </p>가 남아있기 때문에, .*는 자신이 집어삼킨 문자열을 하나 뱉어내고, 남은 정규식 패턴 </p>에 대조해보라고 한다.
    2. 마지막 문자 하나인 ‘>‘는 매치되지 않기 때문에, .*는 문자를 하나 더 뱉어낸다. 이제 ‘p>‘와 남은 정규식 패턴 </p>를 비교해보라고 시킨다.
    3. 역시 일치되지 않으므로, 이와 같은 과정을 정규식 패턴과 뱉어낸 문자열이 일치될 때까지 혹은 모든 문자를 뱉어낼 때까지 반복하게 된다.
    4. Lorem ipsum 예시의 경우 4개의 문자를 뱉어내면 일치된다. 따라서 모든 문자열이 정규식 패턴과 일치되고, 전체 문자열이 결과로 반환된다.
    5. .*가 먹어치웠던 문자열을 살펴보면 그 경계가 끝까지 갔다가 반대 방향으로 후퇴하는 것처럼 보인다. 그래서 이름이 역행이다.
  • 이는 2번 예시를 보아도 알 수 있다. .*가 최대로 일치시키려고 하기 때문에, ‘part 1’이나 ‘part 2’까지 일치되는 것이 아닌 최대로 일치되는 부분인 ‘part 4’까지 일치시키는 것을 볼 수 있다.

3번 예시는 나태 정량자를 보여준다. 나태 정량자는 별다른 것은 없고, 단지 정량자 바로 뒤에 ?를 붙여주기만 하면 된다. 그러면 탐욕적 정량자였던 *는 최대로 일치시키는 대신 문자열은 가장 적게 먹어치우면서 일치되도록 하는 방법을 찾는다. 그래서 딱 ‘part 1’까지만 일치되고, 나머지 문자열은 버려진다.

  • +?, {3, 5}?, ?? 등도 가능하다.
  • 사실 나태 정량자도 역행을 한다. 그러나 역행이 꼭 뒤로 가는 것을 의미하는 것이 아닌, 각 정량자가 선호하는 방향과 반대 방향으로 갈 때 역행이라고 한다. 따라서 나태 정량자는 우선 최소로 일치하는 부분을 찾은 뒤(빈 문자열), 문자열이 일치될 때까지 역행(문자열 방향으로는 뒤쪽)한다.

그래서 탐욕 정량자와 나태 정량자의 차이는, 유력 대조부를 제일 긴 것을 우선적으로 찾느냐, 제일 짧은 것을 우선적으로 찾느냐의 차이이다.
그리고 결과적으로 탐욕 정량자와 나태 정량자의 일치부가 같아지는 때도 있다. 다만 이때는 검색 순서만이 다를 뿐이다.

역행에 관해서는 나중에 조금 더 자세히 다루도록 하겠다.


응용 문제

문제 1: 1~8자리 10진수에 일치하는 정규표현식을 작성하라.

문제 1 정답보기

r'\b\d{1,8}\b'


문제 2: 4자리 또는 8자리 16진수에 일치하는 정규표현식을 작성하라. 16진수는 0~9, a~f를 사용한다. 예시는 abcd1992, 7fffffff, 2dfa9a00이다. 윈도우 오류에서 ‘0xC1900101’ 비슷한 에러를 많이 봤을 것이다.

문제 2 정답보기

r'\b[0-9a-f]{4}\b|\b[0-9a-f]{8}\b'


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

문제 3 정답보기

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


파이썬 버전 3.6 기준으로, \b를 쓰려면 r prefix를 붙여 주어야 한다고 했었다.


문제 3의 정답에 아직 설명하지 않은 소괄호 ( )가 있다. 이는 다음 글에서 설명한다.

Comment  Read more

파이썬 정규표현식(re) 사용법 - 02. 문자, 경계, flags

|

파이썬 정규표현식(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 버전은 한글을 포함한 비 알파벳 문자 처리가 다르다.


특수문자

메타문자

메타문자에 대해서는 이전 글에서 설명했다.

비인쇄 문자

\a, 이스케이프\e, 폼 피드\f, 라인 피드(개행문자)\n, 캐리지 리턴\r, 가로 탭\t, 세로 탭\v는 다음 두 가지 방식으로 쓸 수 있다.

\a \e \f \n \r \t \v
\x07 \x1B \f \n \r \t \v

정규표현식을 쓰면서 다른 것들은 거의 볼 일이 없을 것이지만, \t\n은 알아두는 것이 좋다.

matchObj = re.findall('\t ', 'a\tb\tc\t \t d')
print(matchObj)

결과

['\t ', '\t ']

탭 문자와 공백 문자가 붙어 있는 것은 2개임을 확인할 수 있다.

이스케이프 \

이스케이프 문자 \는 메타문자를 일반 리터럴 문자로 취급하게끔 해 준다.
예를 들어 여는 괄호 [는 메타 문자지만, \[와 같이 처리하면 리터럴 문자인 일반 대괄호 문자 ‘[‘와 매칭될 수 있게 된다.

하지만, 일반 영수 문자(알파벳 또는 숫자)를 이스케이프 처리하면 에러가 나거나 혹은 전혀 다른 의미의 정규식 토큰이 생성된다.
예를 들어 파이썬에서 \1의 경우에는 캡처한 문자열 중 첫번째를 재사용한다는 의미(나중에 자세히 설명할 것이다)가 되어 버린다. 따라서 \를 남용하면 안 된다.


[ ] 대괄호:여러 문자 중 하나와 일치

대괄호 [] 사이에 원하는 문자를 여러 개 넣으면, 문자열이 넣은 문자 중 하나와 일치하면 매칭이 이루어진다. 즉 OR 개념이라고 할 수 있다.
여기서 중요한 것은 [ ] 안에 얼마나 많은 문자 종류가 있는지에 상관없이 딱 한 문자와 일치된다는 것이다.

예를 들어 정규식 표현이 [abc]이고 문자열이 ‘a’이면 re.match는 매칭되었다고 할 것이다.
문자열이 ‘b’이거나 ‘c’이어도 매칭이 된다. 다만 문자열이 ‘d’이거나 ‘가나다’ 같은 것이면 매칭이 되지 않는다.

matchObj = re.fullmatch("You[;']re studying re module[.,]", \
                        'You;re studying re module,')
print(matchObj)

결과

<_sre.SRE_Match object; span=(0, 26), match='You;re studying re module,'>

사용자의 오타를 잡기에 괜찮은 기능이다.

대괄호 [ ]에는 다른 기능이 더 있다. 이전 글에서 semi-메타문자를 설명했었는데, 문자 -는 대괄호 안에서는 메타문자 역할을 한다.

하이픈 -는 범위를 형성한다. 예를 들어 [a-z]는 알파벳 소문자 중 하나이기만 하면 매칭이 된다. 또 [A-Z], [0-9]는 각각 알파벳 대문자와 숫자 하나에 매칭된다.
물론 위의 경우뿐만 아니라 넓은 범위도 가능하다. [가-힣]의 경우는 한글 한 글자에 일치된다.
[A-z]는 영문 대소문자와 몇 개의 특수문자를 포함한다. 하지만 여러분이 잘 모르는 문자까지 포함될 수 있으므로 영문자는 [A-Za-z]와 같이 쓰기를 권한다.

참고로, 대괄호 안에서는 메타문자 역할을 하는 것은 오직 \, ^, -, ] 4개뿐이다. 즉, 이전에 메타문자라고 설명했었던 ., *, + 등은 대괄호 안에서는 그냥 문자 ‘.’, ‘*’, ‘+’ 하나에 매칭된다.
그러나 헷갈릴 소지가 다분하기 때문에 원래 메타문자인 문자들은 그냥 대괄호 안에서도 \ 이스케이프 처리하는 것이 편할 것이다.
물론 IDE가 좋다면 redundant escape character라는 경고를 띄워 줄지도 모른다.

캐릿(caret)^ 문자가 여는 대괄호 바로 뒤에 있으면 문자가 반전된다. 바로 예시를 보도록 하자.

matchObj = re.search('Why [a-z]o serious\?', 'Why so serious?')
print(matchObj)
matchObj = re.search('Why [^0-9]o serious\?', 'Why so serious?')
print(matchObj)

결과

<_sre.SRE_Match object; span=(0, 15), match='Why so serious?'>
<_sre.SRE_Match object; span=(0, 15), match='Why so serious?'>

[a-z]는 영문 소문자 하나(‘s’)와 일치되므로 매칭 결과가 반환되었다.
[^0-9]는 숫자를 제외한 문자 하나에 일치되므로, ‘s’는 숫자가 아니기에 매칭이 되었다.

[z-a]와 같이 거꾸로 쓰는 것은 불가능하다.

대괄호 안의 -는 또 다른 기능이 있다. 바로 진짜 빼기(마이너스), 즉 차집합 연산이다.
대괄호 한 쌍을 집합으로 보면 차집합이란 말이 이해가 될 것이다. [a-z-[g-z]]의 경우 a-f와 같은 의미이다.
또 &&를 안에 쓰면 C언어 문법의 and 기능처럼 교집합을 의미한다고 한다.
하지만 필자가 글을 쓰는 시점에서 이 문법이 유효한지는 확인되지 않았다. 파이썬 버전에 따라 다를 수도 있고, 지원하지 않는 기능일 수도 있다.


. 마침표: 모든 문자와 일치

개행문자를 제외한 모든 문자와 일치하는 정규표현식은 마침표 . 이다. 정말로 모든 문자와 일치되기 때문에 별다른 설명은 필요 없을 것 같다.

matchObj = re.findall('r..n[.]', 'ryan. ruin rain round. reign')
print(matchObj)

결과

['ryan.']

대괄호 [ ] 안에서는 .가 메타문자로 동작하지 않는다고 하였다. 따라서 일치되는 문자열은 ‘ryan’ 하나뿐이다.

마침표는 개행 문자와 일치 옵션

파이썬 re 패키지의 많은 함수들은 다음과 같은 인자들을 받는다고 이전 글에서 설명했었다.

re.match(pattern, string, flags)

여기서 flags는 다음과 같은 종류들이 있다.

syntax long syntax inline flag meaning
re.I re.IGNORECASE (?i) 대소문자 구분 없이 일치
re.M re.MULTILINE (?m) ^와 $는 개행문자 위치에서 일치
re.S re.DOTALL (?s) 마침표는 개행문자와 일치
re.A re.ASCII (?a) {\w, \W, \b, \B}는 ascii에만 일치
re.U re.UNICODE (?u) {\w, \W, \b, \B}는 Unicode에 일치
re.L re.LOCALE (?L) {\w, \W, \b, \B}는 locale dependent
re.X re.VERBOSE (?x) 정규표현식에 주석을 달 수 있음

우선 다른 것들은 나중에 살펴보고, 마침표 옵션만을 보자.

syntax long syntax meaning
re.S re.DOTALL 마침표는 개행문자와 일치
print(re.findall('a..', 'abc a  a\na'))
print(re.findall('a..', 'abc a  a\na', re.S))
print(re.findall('a..', 'abc a  a\na', re.DOTALL))

결과

['abc', 'a  ']
['abc', 'a  ', 'a\na']
['abc', 'a  ', 'a\na']

개행 문자도 마침표에 일치되는지를 설정할 수 있음을 확인하였다.
문자열을 행 단위로 처리하거나 아니면 전체 문자열을 대상으로 처리할 수 있다는 것에 이 옵션의 존재 의의가 있다.

모드 변경자

아니면 다른 방법도 있다. 정규표현식 내에서 사용할 수도 있다.
문자열 앞에 (?s) 토큰을 넣으면 된다.

print(re.findall('(?s)a..', 'abc a  a\na'))

결과

['abc', 'a  ', 'a\na']

모드 변경자는 여러 개를 중첩하여 사용할 수도 있다.
또한 일부분에만 사용하고 싶으면 (?s<regex>)처럼 모드 변경자의 소괄호 안에 집어넣으면 된다.

print(re.findall('(?is)a..', 'Abc'))
print(re.findall('(?is:a..) and abc is good',
'''
Abc and abc is good.
abc and Abc is good. 
'''))

결과

['Abc']
['Abc and abc is good']

두 번째 findall에서 문장을 한 개만 찾은 것을 유의하라.


문자 집합: \w \W, \d \D, \s \S, \b \B

\w, \W: 단어 문자, 비 단어 문자

\w는 단어 문자 1개와 일치된다. 단어 문자는 영문 대소문자, 숫자 0-9, 언더바 ‘_’ 를 포함한다.
한글 등 알파벳 이외의 단어는 파이썬 버전에 따라 다른데, Unicode를 기본으로 사용하는 파이썬 3이라면 아마 \w의 범위에 한글도 포함될 것이다. 여러분이 스스로 확인해 봐야 할 것이다.

\W는 단어 문자 이외의 문자 1개에 일치된다. 즉 공백 문자, 특수 문자 등에 일치된다고 보면 된다.
\w와 정확히 반대의 역할을 한다.

matchObj = re.search('\w\w\w', 'a_가')
print(matchObj)
matchObj = re.findall('\w\W\w', 'a (9_a a')
print(matchObj)

결과

<_sre.SRE_Match object; span=(0, 3), match='a_가'>
['a a']

첫 번째 출력 결과의 경우 단어 3개를 나타내는 정규표현식에 ‘a_가’가 매칭되었다. 두 번째 출력 결과는 잘 보면
1) 단어 문자(a)
2) 비 단어 문자( )
3) 단어 문자(a)
순으로 되어 있는데, 그런 결과는 ‘a a’ 하나뿐이다.

\d, \D: 숫자 문자, 비 숫자 문자

\d는 숫자 문자 1개에 일치된다. 마찬가지로 \D는 비 숫자 문자 1개에 일치된다.

matchObj = re.search('\d\d', '12abc34')
print(matchObj)
matchObj = re.findall('\d\d\D\D', '11aa11c1')
print(matchObj)

결과

<_sre.SRE_Match object; span=(0, 2), match='12'>
['11aa']

첫 번째 출력 결과는 매칭되는 문자열은 두 군데로 ‘12’와 ‘34’이다. 하지만 re.search는 제일 처음 하나만 찾아내기 때문에 하나만 반환하였다. 두 번째 출력 결과는 숫자 2개에 비 숫자 문자 2개가 붙어 있는 문자열 ‘11aa’를 잘 찾아 주었다.

\s, \S: 공백 문자, 비 공백 문자

\s는 공백 문자(빈칸 ‘ ‘, 탭 ‘\t’, 개행 ‘\n’) 1개에 일치된다. 마찬가지로 \S\s의 반대 역할이다. 즉, 공백 문자 이외의 모든 문자 1개에 일치된다.

matchObj = re.search(
    'Oh\smy\sgod\s\S',
    '''Oh my\tgod
!''')
print(matchObj)

결과

<_sre.SRE_Match object; span=(0, 11), match='Oh my\tgod\n!'>

\b, \B: 단어 경계, 비 단어 경계

단어 경계 \b 는, 문자 하나와 일치되는 것이 아니라 정말로 단어 경계와 일치된다. 단어 문자와 비 단어 문자 사이와 매칭된다고 보면 된다.

비 단어 경계 \B 는 마찬가지로 반대의 역할을 수행한다. 즉, 단어 문자와 단어 문자 사이 혹은 비 단어 문자와 비 단어 문자 사이와 일치된다.

다른 말로는, \b\w에 일치되는 한 문자와 \W에 일치되는 한 문자 사이에서 일치되고, \B\w에 일치되는 두 문자 사이 또는 \W에 일치되는 두 문자 사이에서 일치된다.

한 가지 주의할 점으로는 \b\B를 사용하기 위해서는 정규표현식 앞에 r prefix를 붙여줘야 한다는 것이다.
예시를 보자.

matchObj = re.findall(r'\w\b\W\B', 'ab  c d  == = e= =f')
print(matchObj)

결과

['b ', 'd ', 'e=']

위의 예시는
1) 단어 문자
2) 단어 경계
3) 비 단어 문자
4) 비 단어 경계

순으로 되어 있는 문자열을 찾는다. 위의 조건을 만족시키려면 단어 문자 + 비 단어 문자 + 비 단어 문자 조합을 찾아야 한다. 그리고 실제로 매칭되는 문자열은 단어 문자 + 비 단어 문자이다.
(주: 여기서 2) 단어 경계는 쓸모가 없다. 이유는 여러분이 알아서 생각하면 된다.)

응용 문제

문제 1: ‘line’과는 일치하지만, ‘outline’나 ‘linear’ 등과는 일치하지 않는 정규표현식을 작성하라. 즉, 정확히 ‘line’ 단어와만 일치해야 한다.

문제 1 정답보기

\bline\b


문제 2: ‘stacatto’에는 일치하지만, ‘cat’이나 ‘catch’, ‘copycat’ 등과는 일치하지 않는 정규표현식을 작성하라.

문제 2 정답보기

\Bcat\B


\b는 단어 경계로, 다음에 일치된다.

  1. 첫 문자가 단어 문자인 경우, 첫 문자 앞에서
  2. 인접한 두 문자 중 하나만 단어 문자인 경우, 그 사이에서
  3. 끝 문자가 단어 문자인 경우, 끝 문자 뒤에서

즉 문자열의 맨 앞과 맨 끝은 비 단어인 것으로 처리된다.

\B는 비 단어 경계로, 다음에 일치된다.

  1. 첫 문자가 비 단어 문자인 경우, 첫 문자 앞에서
  2. 두 단어 문자 사이 또는 두 비 단어 문자 사이에서
  3. 끝 문자가 비 단어 문자인 경우, 끝 문자 뒤에서
  4. 빈 문자열에서

(헷갈리는) 예시를 보자.

print(re.findall(r'\b', 'a'))
print(re.findall(r'\B', 'a'))

print(re.findall(r'\b', 'a aa'))
print(re.findall(r'\B', 'a aa'))

결과

['', '']
[]
['', '', '', '']
['']

각각 어디에서 일치된 것인지 이해해 보기 바란다.

옵션: r prefix

원래 r prefix란 이스케이프 문자 \를 이스케이프 처리 문자가 아닌 일반 리터럴 문자로 인식하게끔 하는 역할을 한다. 영문 설명을 가져오면 아래와 같다.

When an “r” or “R” prefix is present, a character following a backslash is included in the string without change, and all backslashes are left in the string. For example, the string literal r”\n” consists of two characters: a backslash and a lowercase “n”. String quotes can be escaped with a backslash, but the backslash remains in the string; for example, r”"” is a valid string literal consisting of two characters: a backslash and a double quote; r”" is not a valid string literal (even a raw string cannot end in an odd number of backslashes). Specifically, a raw string cannot end in a single backslash (since the backslash would escape the following quote character). Note also that a single backslash followed by a newline is interpreted as those two characters as part of the string, not as a line continuation.

해석하면,

“r”이나 “R” 접두사가 있으면, \ 뒤에 있는 문자는 문자열에 변화 없이 그대로 남아 있게 되고, 모든 \ 또한 문자열에 남아 있게 된다. 예를 들어, 리터럴 문자열 r”\n”은 \와 소문자 n 2개의 문자로 구성된다. 따옴표 문자열 역시 \가 있으면 이스케이프 처리될 수 있지만, \는 여전히 문자열에 남아 있게 된다. 예를 들어 r”\"”의 경우 \와 “ 두 개로 구성된 유효한 문자열이다. r”\“는 유효하지 않다(raw string은 홀수 개의 \로 끝날 수 없다). 특별히, raw string은 한 개의 \로 끝날 수 없다(\는 다음에 오는, 즉 문자열의 끝을 알리는 따옴표를 이스케이프 처리하므로). newline이 다음에 오는 한 개의 \는 문자열의 일부로서 두 개의 문자로 취급되지, 개행으로 처리되지 않는다.

예시를 보자.

>>> r'\'
SyntaxError: EOL while scanning string literal
>>> r'\''
"\\'"
>>> '\'
SyntaxError: EOL while scanning string literal
>>> '\''
"'"
>>> 
>>> r'\\'
'\\\\'
>>> '\\'
'\\'
>>> print r'\\'
\\
>>> print r'\'
SyntaxError: EOL while scanning string literal
>>> print '\\'
\

Unicode/Locale dependent 옵션

파이썬3은 기본적으로 한글도 “단어 문자”에 포함되기 때문에 쓸 일이 있을지는 모르지만, 이 옵션들도 소개해 본다.

syntax long syntax inline flag meaning
re.A re.ASCII (?a) {\w, \W, \b, \B}는 ascii에만 일치
re.U re.UNICODE (?u) {\w, \W, \b, \B}는 Unicode에 일치
re.L re.LOCALE (?L) {\w, \W, \b, \B}는 locale dependent

파이썬3은 기본적으로 Unicode를 기준으로 처리되기 때문에 re.U는 쓸모가 없다. 그러나 호환성을 위해 아직까지는 살아 있는 옵션이다.
아스키에만 일치하는 옵션을 쓰고 싶으면 re.ASCII 옵션을 사용하면 된다.

조금 더 자세한 설명은 여기를 참조하라.

다른 flags 사용법과 똑같으므로 생략하도록 하겠다.


^, $, \A, \Z: 문자열 전체 또는 행의 시작이나 끝의 대상을 대조

\A는 문자열 시작을, \Z는 문자열 끝과 일치된다.

이들은 일명 앵커라고 부르는데, 문자와 일치되는 것이 아니라 정규식 패턴을 특정 위치에 고정시켜서 그 위치에 일치시키는 역할을 한다.

^$는 기본적으로 행 시작과 행 끝에 일치된다.

여기서 행은 문자열의 시작과 개행문자 사이, 개행문자와 개행문자 사이, 개행문자와 문자열의 끝 사이 부분이다. 문자열에 개행문자가 없으면 전체 문자열이 한 개의 행이 된다.

^$는 일반적으로 \A\Z 앵커와 효과가 같다. 다른 경우는 옵션을 설정하는 경우인데, re.MULTILINE 옵션을 설정하면 ^$는 문자열 전체의 시작/끝이 아닌 행의 시작/끝에서 일치된다.

print(re.findall('\Aryan\d\Z', 'ryan1'))
print(re.findall('^ryan\d$', 'ryan1'))

print(re.findall('\A ryan\d\s\Z', ' ryan1 \n ryan2 \n rain1 \n ryan3 '))
print(re.findall('^ ryan\d\s$', ' ryan1 \n ryan2 \n rain1 \n ryan3 '))
print(re.findall('^ ryan\d\s$', ' ryan1 \n ryan2 \n rain1 \n ryan3 ', re.M))
print(re.findall('^ ryan\d\s$', ' ryan1 \n ryan2 \n rain1 \n ryan3 ', re.MULTILINE))

결과

['ryan1']
['ryan1']
[]
[]
[' ryan1 ', ' ryan2 ', ' ryan3 ']
[' ryan1 ', ' ryan2 ', ' ryan3 ']

Java, .NET 등에서는 \z 옵션이 있지만, 파이썬에는 bad escape 에러를 보게 되므로 사용하지 말자.

응용으로, 빈 문자열 혹은 빈 행을 검사할 수 있다.

print(re.fullmatch('\A\Z', ''))
print(re.fullmatch('\A\Z', '\n'))
print(re.fullmatch('^$', ''))
print(re.fullmatch('^$', '\n'))
print(re.findall('^$', '\n', re.M))

결과

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

^, $도 마침표 .처럼 옵션을 인라인으로 설정할 수 있다.

print(re.findall('(?m)^$', '\n'))

결과

['', '']

참고로, 옵션을 여러 개 쓰려면 |로 OR 연산을 시켜주면 된다.

print(re.findall('^ ryan\d\s$', ' ryan1 \n Ryan2 \n rain1 \n RYAN3 ', re.M | re.IGNORECASE))

결과

[' ryan1 ', ' Ryan2 ', ' RYAN3 ']

위의 예시처럼 full-name과 약자를 같이 써도 되지만, 가독성을 생각한다면 굳이 그렇게 할 이유는 없다.

유니코드 번호

한 글자 일치와 사용법은 같다.

print(re.findall('\u18ff\d', '0᣿1頶᣿2䅄ሲ᣿3456'))

결과

['\u18ff1', '\u18ff2', '\u18ff3']

참고로 ‘\u18ff’는 ‘᣿’이다.


다음 글에서는 다자택일(OR), 반복 등을 다루도록 하겠다.

Comment  Read more

파이썬 정규표현식(re) 사용법 - 01. Basic

|

파이썬 정규표현식(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 버전은 한글을 포함한 비 알파벳 문자 처리가 다르다.


정규표현식의 기초

일대일 매칭되는 문자

정규표현식 안에서, 바로 다음 절에서 설명하는 메타문자를 제외한 모든 문자 하나는 일반 문자열 하나와 매칭된다. 예를 들어, a는 a와 매칭되고, 는 ‘가’와 매칭되는 식이다.
당연히 a가 ‘b’ 또는 ‘가’와 매칭되지는 않는다.

메타문자

어떤 프로그래밍 언어의 정규표현식이든 메타문자라는 것이 존재한다.
이는 특수한 기능을 하는 문자로, import 등 파이썬의 예약어와 비슷한 역할을 맡는 문자라고 생각하면 된다.

파이썬 re 모듈의 메타문자는 총 12개로 다음과 같은 것들이 있다.

` $()*+.?[\^{ `

이들 메타문자는 각각의 문자 하나에 매칭되지 않는다.
예를 들어 일반 문자인 a는 문자 ‘a’에 매칭하지만, 여는 소괄호 (는 문자 ‘(‘와 매칭하지 않는다.

그럼 찾고자 하는 문자열에 소괄호가 있으면 어떻게 하나?

위의 문자들의 앞에 백슬래시 \를 붙여 주면 일반 문자처럼 한 글자에 매칭된다. 예를 들어 \(는 문자 ‘(‘와 매칭된다.

이들의 사용법은 차차 알아보도록 하자.

semi-메타문자

사실 이건 필자가 붙인 이름이지만… 이들 문자는 평소에는 메타문자가 아니지만, 특수한 상황에서는 메타문자 역할을 하는 문자들이다.
], -, ) 가 있다.

닫는 괄호야 당연히 여는 괄호에 대응된다는 것은 알 수 있을 것이다. -는 이후에 설명한다.


re 패키지 기본 method

import

물론 py 파일에서는 import re를 해주어야 쓸 수 있다.

re.match(pattern, string, flags)

01_match

re.match 함수는 “문자열의 처음”부터 시작하여 패턴이 일치되는 것이 있는지를 확인한다.
다음과 같이 사용한다.

matchObj = re.match('a', 'a')
print(matchObj)

print(re.match('a', 'aba'))
print(re.match('a', 'bbb'))
print(re.match('a', 'baa'))
# 사실 match의 결과를 바로 print하지는 않는다. 결과를 활용하는 방법은 나중에 설명할 matchObj.group 함수를 쓰는 것이다.

결과

<_sre.SRE_Match object; span=(0, 1), match='a'>
<_sre.SRE_Match object; span=(0, 1), match='a'>
None
None

re.match 함수는 문자열의 처음부터 시작하여 패턴이 일치되는 것이 있는지를 확인한다.
위의 예시에서 첫번째는 패턴과 문자열이 동일하므로 매치되는 것을 확인할 수 있다.
두번째 예시는 문자열이 ‘a’로 시작하기 때문에 매치가 된다.
나머지 두 개는 ‘a’로 시작하지 않아 패턴 a와 매치되지 않는다. 매치되지 않을 때 re.match 함수는 None을 반환한다.

매치가 되었을 때는 match Object를 반환한다. 위의 결과에서 _sre.SRE_Match object를 확인할 수 있다.

re.match 함수는 인자로 1)pattern 2)string 3)flags를 받는다. 3번은 필수 인자는 아닌데, 어떤 옵션이 있는지는 뒤에서 설명한다.
각 인자는 각각 1)패턴 2)패턴을 찾을 문자열 3)옵션을 의미한다.

re.search(pattern, string, flags)

02_search

re.search 함수는 re.match와 비슷하지만, 반드시 문자열의 처음부터 일치해야 하는 것은 아니다.

다음 예시를 보자.

matchObj = re.search('a', 'a')
print(matchObj)

print(re.search('a', 'aba'))
print(re.search('a', 'bbb'))
print(re.search('a', 'baa'))

결과

<_sre.SRE_Match object; span=(0, 1), match='a'>
<_sre.SRE_Match object; span=(0, 1), match='a'>
None
<_sre.SRE_Match object; span=(1, 2), match='a'>

네 번째 결과가 달라졌음을 볼 수 있다. re.search 함수는 문자열의 처음뿐 아니라 중간부터라도 패턴과 일치되는 부분이 있는지를 찾는다.
따라서 네 번째 문자열 ‘baa’의 경우 1번째 index(두 번째 문자) ‘a’와 매치된다.

위의 결과에서 span=(0, 1) 를 확인할 수 있다. 위의 두 결과는 span=(0, 1)인데,
이는 0번째 문자부터 1번째 문자 전까지(즉, 0번째 문자 하나인 ‘a’)가 패턴과 매치되었음을 뜻한다.
span=(1, 2)의 경우 1번째 문자(‘baa’ 의 첫 번째 ‘a’이다)가 패턴과 매치되었음을 볼 수 있다.

re.findall(pattern, string, flags)

03_findall

이름에서 알 수 있듯이 re.findall 함수는 문자열 중 패턴과 일치되는 모든 부분을 찾는다.

matchObj = re.findall('a', 'a')
print(matchObj)

print(re.findall('a', 'aba'))
print(re.findall('a', 'bbb'))
print(re.findall('a', 'baa'))
print(re.findall('aaa', 'aaaa'))

결과

['a']
['a', 'a']
[]
['a', 'a']
['aaa']

각 예시에서, 문자열의 a의 개수를 세어 보면 잘 맞는다는 것을 확인할 수 있다.

함수 설명을 잘 보면, “non-overlapping” 이라고 되어 있다. 즉 반환된 리스트는 서로 겹치지 않는다는 뜻이다. 마지막 예시가 이를 말해주고 있다. 겹치는 것을 포함한다면 두 개가 반환되어야 했다.

re.finditer(pattern, string, flags)

04_finditer

re.findall과 비슷하지만, 일치된 문자열의 리스트 대신 matchObj 리스트를 반환한다.

matchObj_iter = re.finditer('a', 'baa')
print(matchObj_iter)

for matchObj in matchObj_iter:
    print(matchObj)

결과

<callable_iterator object at 0x000002795899C550>
<_sre.SRE_Match object; span=(1, 2), match='a'>
<_sre.SRE_Match object; span=(2, 3), match='a'>

iterator 객체 안에 matchObj가 여러 개 들어 있음을 확인할 수 있다.

re.fullmatch(pattern, string, flags)

05_fullmatch

re.fullmatch는 패턴과 문자열이 남는 부분 없이 완벽하게 일치하는지를 검사한다.

matchObj = re.fullmatch('a', 'a')
print(matchObj)

print(re.fullmatch('a', 'aba'))
print(re.fullmatch('a', 'bbb'))
print(re.fullmatch('a', 'baa'))
print(re.fullmatch('aaa', 'aaaa'))

결과

<_sre.SRE_Match object; span=(0, 1), match='a'>
None
None
None
None

맨 위의 예시만 문자열이 남는 부분 없이 정확하게 일치하므로 매칭 결과를 반환했다. 나머지 예시는 문자열이 뒤에 남기 때문에 매치되는 결과 없이 None을 반환했다.

match Object의 메서드들

match Object를 그대로 출력해서 쓰고 싶은 사람은 별로 없을 것이다. re.match 등의 결과로 얻은 matchObj를 활용하는 방법을 정리하면 다음과 같다.

Method Descrption
group() 일치된 문자열을 반환한다.
start() 일치된 문자열의 시작 위치를 반환한다.
end() 일치된 문자열의 끝 위치를 반환한다.
span() 일치된 문자열의 (시작 위치, 끝 위치) 튜플을 반환한다.
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())

결과

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

잘 세어보면 ‘match’가 1번째 문자부터 6번째 문자 직전까지임을 알 수 있다. 인덱스는 0부터이다.


다음 글에서는 정규표현식의 기초를 더 살펴보도록 한다.

Comment  Read more