Post List

2015년 8월 24일 월요일

Python 기초 #15 Regular Expressions (정규식)

1. Regular Expression 이란 ?

기초라고 하기에 Regular Expression 은 좀 어려운 주제일 수도 있습니다.
Python 같은 Script 언어의 가장 큰 특징이 같은 기능을 구현하더라도 다른 언어보다 좀 더 간결한 Code로 빠르게 개발이 가능하단 점입니다.
Regular Expression (정규식)은 String(문자열) 처리에 대한 높은 수준의 처리기법 중 하나로 프로그래밍 언어 내에 속한 무언가가 아닌 하나의 기술입니다.
C++, C#, Java 등 현재 대부분의 언어들이 다 Regex(Regular Expression의 줄임말)를 지원합니다.

Regex의 자세한 내용은 아래 Wiki를 참조하시면 됩니다.
https://en.wikipedia.org/wiki/Regular_expression

온라인 상에서 쉽게 Regex의 표현식 연습이 가능합니다. 아래 Link에서 연습하시면 됩니다.
http://www.regexr.com

2. Regex를 왜 써야하나 ?

Regex를 사용하지 않고도 문자열 처리를 직접 다 하는 것이 가능합니다.
Regex를 사용하면 훨씬 더 짧은 문장으로 표현이 가능합니다.
단순히 문장만 짧아지는게 아니라 논리적으로도 훨씬 더 단순해져서
복잡하게 여러가지 함수들을 사용하고 문자열을 치환하고 이런 작업들이 생략됩니다.
개발을 하는 입장에서도 해당 Code를 보고 유지보수하는 입장에서도 훨씬 더 간단해집니다.

일단 예제를 한번 살펴보겠습니다.

특정 문자열 내에 존재하는 주민등록번호의 패턴들 (숫자6자리-숫자7자리) 를 찾아서 뒤에 숫자 6자리만을 '*'로 표시해야 하는 기능을 구현해야 할 경우 Regex를 사용하지 않고 구현 한다면 다음과 같이 해야 합니다.

step 1. Text를 줄 단위로 나눕니다. (split to line)
step 2. line을 space 단위로 나눕니다. (split to word)
step 3. word가 주민등록번호 형식인지 확인 한 후 맞다면 뒤에 6자리를 '*'로 치환합니다.
step 4. word들을 space 단위로 재 조립합니다. (join to line)
step 5. line들을 줄 단위로 재 조립합니다. (join to Text)
step 6. Text를 출력합니다.

입력된 Text는 다음과 같다고 가정합니다.

str = """
Luna 801231-1938472
Star 870702-2739172
"""

구현 Code는 다음과 같습니다.

result = []
for line in str.split('\n'):
    tokenList = []
    for word in line.split(' '):
        if len(word) == 14 and word[6] == '-' and word[:6].isdigit() and word[7:].isdigit():
            word = word[:8] + '******'
        tokenList.append(word)
    result.append(' '.join(tokenList))
print('\n'.join(result))

결과는 다음과 같이 출력됩니다.

Luna 801231-1******
Star 870702-2******

위 구현 Code와 똑같은 기능을 하도록 Regex로 표현해 보겠습니다.

import re
 
regex = re.compile("(\d{6}[-]\d{1})\d{6}")
print(regex.sub("\g<1>******", str))

3. Regex 표현 규칙

표현 규칙은 Python만의 예기가 아니므로 다음에 따로 자세히 Posting 하겠습니다.
이 Posting에서는 Python에서 Regex를 어떻게 사용하는지를 중심으로 설명드리겠습니다.

Regex 표현 규칙에 대한 설명은 자세히 소개된 Site 들이 많습니다.

앞서 소개한 http://www.regexr.com 에서도 Reference 확인이 가능합니다.

MSDN에서도 Quick Reference 및 자세한 글들을 제공합니다.
https://msdn.microsoft.com/ko-kr/library/az24scfc(v=VS.110).aspx

wiki에서도 아래 Link부터 쭉 읽어 나가시면 되구요.
https://en.wikipedia.org/wiki/Regular_expression#Basic_concepts

Jump to Python E-Book에서도 쉽게 설명되어 있습니다.
https://wikidocs.net/1642

4. Regex 시작하기

Regex는 앞서 본 예제와 같이 re라는 모듈을 import하면 사용할 수 있습니다.
Python 설치시 같이 설치되는 기본 라이브러리 입니다.

import re

정규식을 먼저 compile 한 후에 사용을 해야 합니다.

regex = re.compile("(\d{6}[-]\d{1})\d{6}")

compile 함수에는 옵션을 줄 수가 있습니다.

regex = re.compile('luna*', re.IGNORECASE)

re.IGNORECASE는 대소문자 구분없이 문자열을 찾으라는 뜻입니다.
위 표현식은 대소문자 구분없이 LUNA로 시작하는 문자열을 찾으라는 뜻입니다.

compile 할때 주의해야 할 점이 있습니다.
바로 백슬래시 (\) 를 사용할때 주의해야 합니다.
(이 문제는 다른 언어들도 마찬가지 입니다.)
정규식 내에서는 백슬래시를 이용한 표현식들이 있습니다.
하지만 대부분의 프로그래밍 언어에서 문자열 내부에서 백슬래시를 사용하여 특수문자를 표현하는 것이 많습니다.
그래서 정규표현식으로 백슬래시를 표현하기 위해서는 백슬래시를 2번 적어서 백슬래시라는 문자열을 표현해야 합니다.

즉 \luna* 를 표현하기 위해서는

regex = re.compile('\\luna*')

라고 적어줘야 합니다.

5. 검색

4가지 검색 Method를 제공합니다.

- match() : 문자열의 처음부터 정규식과 일치하는지를 조사합니다.
- search() : 문자열에 정규식과 일치하는 표현이 있는지를 조사합니다.
- findall() : 정규식과 일치하는 모든 문자열을 list로 return 합니다.
- finditer() : 정규식과 일치하는 모든 문자열을 iterator 로 return 합니다.

아래와 같이 소문자로만 이루어진 문자열을 찾는 정규식을 compile 합니다.

import re= re.compile('[a-z]+')

먼저 match()부터 살펴보겠습니다.

>>> m = r.match("python")
>>> print(m)
<_sre.SRE_Match object; span=(0, 6), match='python'>

맞을 경우 우와 같은 결과를 return 합니다.

>>> m = r.match("Luna")
>>> print(m)
None

아닐 경우 None 을 return 합니다.
compile에 re.IGNORECASE 옵션을 주었을 경우에는 Luna도 match가 됩니다.

프로그램에서는 if m 만으로도 구분이 됩니다.

= r.match("python")
if m:
    print('Matched : ', m.group())
else:
    print('Unmatched')
---
Matched :  python

search()는 문자열이 처음부터 일치하는지를 보지 않고 문자열 내에 해당되는 표현식이 있는지 찾습니다.

= r.search("python")
if m:
    print('Matched : ', s.group())
else:
    print('Unmatched')

위의 결과는 match() 와 똑같은 결과가 나옵니다.

= r.search("Luna")
if m:
    print('Matched : ', s.group())
else:
    print('Unmatched')
... 
Matched :  una

'Luna' 의 경우 match()에서는 None이 return 되었는데 search()에서는 앞에 L을 제외한 뒤에 una 는 표현식과 일치하므로 찾았다고 return 됩니다.

findall()은 일치하는 모든 표현식을 list로 return 해줍니다.

>>> fa = r.findall('Luna Star DevelopMent Story Blog')
>>> print(fa)
['una', 'tar', 'evelop', 'ent', 'tory', 'log']

finditer()findall()과 같지만 그 결과가 문자열의 list가 아니라 match object의 iterator를 return 합니다.

>>> fi = r.finditer('Luna Star DevelopMent Story Blog')
>>> for m in fi: print(m.group())
una
tar
evelop
ent
tory
log

6. match object

검색의 결과로 return 되는 것은 match object 입니다.
이것에 대해서 자세히 알고 넘어가도록 합시다.
match object의 대표적인 method들은 다음과 같습니다.

- group( ) : match 된 string을 return
- start( ) : match 된 string의 start index
- end( ) : match 된 string의 end index
- span( ) : match 된 string 의 ( start , end ) 를 tuple로 return

>>> r = re.compile('Star')
>>> m = r.match('Star')
>>> m.group()
‘Star’
>>> m.start()
0
>>> m.end()
4
>>> m.span()(0, 4)

match( ) 로 수행한 결과 입니다. match( )는 문자열이 정확히 일치해야 하므로 수행 결과가 None이 아닌이상 start( ) 는 무조건 0 입니다. 하지만 search( )는 string 내에 match 하는 경우가 있으면 되므로 start( )가 0이 아닌 값도 올 수 있습니다.

>>> m = r.search('Luna Star Story')
>>> m.group()
‘Star’
>>> m.start()
5
>>> m.end()
9
>>> m.span()
(5, 9)

위 문장을 아래와 같이 compile match 과정을 하나로 할 수도 있습니다.

>>> m = re.match('[a-z]+', 'star')
>>> m.span()
(0, 4)
>>> fi = re.finditer('[a-z]+', 'Luna Star Story')
>>> for m in fi : print(m.span())
(1, 4)
(6, 9)
(11, 15)

7. compile options

앞에서 re.IGNORECASE 를 사용하여 대소문자 구분 없이 match 하는 것을 잠깐 본적이 있습니다. re.I 로 사용해도 똑같은 기능을 합니다. 이런 류의 option들 중 대표적인 몇가지를 살펴보겠습니다.

- DOTALL , S

도트( . ) 문자가 줄바꿈을 포함한 모든 문자와 match 되도록 해줍니다. 원래 도트 ( . ) 는 줄바꿈 (\n)을 제외한 모든 문자와 match 되지만 해당 옵션을 이용하면 줄바꿈 (\n) 도 포함시킵니다.

>>> m = re.match('a.b', 'a\nb')
>>> print(m)
None
>>> m = re.match('a.b', 'a\nb', re.DOTALL)
>>> print(m)
<_sre.SRE_Match object; span=(0, 3), match='a\nb'>
>>> m = re.match('a.b', 'a\nb', re.S)
>>> print(m)
<_sre.SRE_Match object; span=(0, 3), match='a\nb'>

- IGNORECASE, I

대소문자 구분없이 match 합니다.

>>> m = re.match('[a-z]+', 'STaR', re.I)
>>> print(m)
<_sre.SRE_Match object; span=(0, 4), match='STaR'>

- MULTILINE, M

여러줄과 match 할 수 있도록 합니다. string 가 여러 줄로 된 경우 각각의 line 마다 정규식에 해당되는 match되는 string를 찾도록 합니다.

str = """Star t Go ! Go !
Star Craft Go ! Go !
Luna Star Story !
Star Story !
Star"""

>>> print(re.findall('^Star\s\w+', str)) # Star  시작하고 space 한칸  뒤에 단어가 나와야 하는 pattern
[‘Star t’]

위 Code를 보면 str에 Star로 시작하고 한칸 띄운 뒤 단어가 나오는 패턴이 여러 줄에 걸쳐서 몇개가 되더라도 str의 시작 부분 밖에 못 찾았습니다. 이럴 경우 MULTILINE 옵션을 사용하면 각각의 line마다 match을 합니다.

>>> print(re.findall('^Star\s\w+', str, re.M))
['Star t', 'Star Craft', 'Star Story']

참고로 ^ (해당 문자로 시작) , $ (해당 문자로 종료) 와 같은 것을 메타문자라 합니다.

- VERBOSE, X

verbose 모드로 정규식 작성이 가능해집니다. [ ] 안에 있지 않은 whitespace 는 제거 되며 주석을 적을 수 있게 됩니다.

NUMERIC = re.compile(r'&[#](0[0-7]+|[0-9]+|x[0-9a-fA-F]+);')

위 문장을 아래와 같이 보기 편하게 표현이 가능해집니다.

NUMERIC = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hex form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

8. sub

정규표현식은 문자열 찾기 뿐만 아니라 match된 문자열을 바꾸는 기능도 제공합니다.
Python 에서는 sub( ) 함수로 해당 기능을 제공합니다.

= re.compile('(Oracle|MS-SQL|PetaSQL)')
ret = p.sub('Database', 'Oracle , MS-SQL and PetaSQL')
>>> print(ret)
Database , Database and Database

compile에 해당되는 단어를 찾아서 sub의 첫번째 파라메터의 단어로 바꿔줬습니다.
위 결과를 보면 모든 match 되는 단어들을 다 바꿔줬는데, 횟수를 제한 할 수도 있습니다.

ret = p.subn('Database', 'Oracle , MS-SQL and PetaSQL', 2)
>>> print(ret)
Database , Database and PetaSQL

subn( ) 함수를 이용하면 횟수를 지정 할 수 있습니다.

group을 이용하여 참조를 활용해 문자열을 바꿀 수도 있습니다.

= re.compile(r"(?P<name>\w+)\s+(?P<phone>\d+[-]\d+[-]\d+)")
ret = p.sub('\g<phone> \g<name>', 'LunaStar 010-1234-5678')

>>> print(ret)
010-1234-5678 LunaStar

위 Code는 names group를 사용한 예제 입니다.
아래에는 참조번호를 이용한 예제입니다.

ret = p.sub('\g<2> \g<1>', 'LunaStar 010-1234-5678')

>>> print(ret)
010-1234-5678 LunaStar

sub( ) 의 첫번째 파라메터로 함수를 받을 수도 있습니다.
그럴 경우에는 match 되는 문자열을 해당 함수로 보내서 그 return 값으로 변환합니다.

def decimalToHex(m): return hex(int(m.group()))
= re.compile(r'\d+')
ret = p.sub(decimalToHex, '255 Code , 194 Code')

>>> print(ret)
0xff Code , 0xc2 Code

댓글 없음:

댓글 쓰기