Python - 고급 정규표현식
태깅 문제 고치기
(?:expr)
- 이 문법은 expr을 하나의 단위로 인식하지만 패턴이 일치했을 때, 일치한 문자를 태그로 남기지 않는다.
>>> import re
>>> pat = r'\d{1,3}(?:,\d{3})*(?:\.\d*)?\b'
>>> s = '12,000 monkeys on 100 typewriters for 53.12 days.'
>>> lst = re.findall(pat, s)
>>> for item i lst :
... print(item)
12,000
100
53.12
- 원래라면 그룹핑이 되어 (',000', '') ('', '.12') 가 출력되어야 하지만, "?:"를 추가함으로써 그룹핑을 막은것.
탐욕적 일치 vs 게으른 일치
>>> import re
>>>
>>> pat = r'<.*>'
>>> the_line = '<h1>This is an HTML heading.</h1>'
>>> m = re.match(pat, the_line)
>>> print(m.group())
<h1>This is an HTML heading.</h1>
- 기본적으로 탐욕적 일치의 결과가 나온다.
- 탐욕적 일치 : <h1>This is an HTML heading.</h1>
- 게으른 일치 : <h1>
- 게으른 일치 명시 방법
>>> import re
>>>
>>> pat = r'<.*?>' # 표현식 뒤에 ? 붙임
>>> the_line = '<h1>This is an HTML heading.</h1>'
>>> m = re.match(pat, the_line)
>>> print(m.group())
<h1>
전방탐색
(?=expr)
- 문자들을 태그하지 않는다 > 테깅 그룹에서 제외
- 문자들을 소비하지 않는다 > 전방탐색으로 탐색된 문자열은 이후 정규식의 탐색범위로 재인식 된다.
- 예를들어 정규표현식이 A(?=B)일 경우 대상 문자열 "A AB"에서 2번째 A만 검색 되는 것.
>>> import re
>>> pat = r'[A-Z].*?[.!?](?= [A-Z]|$)' # 문장인식 패턴
>>> s = '''See the U.S.A. today. It's right here, not
... a world away. Average temp. is 66.5.''' # 여러줄 문자열 작성을 위해 ''' 사용
>>> m = re.findall(pat, s, flags=re.DOTALL | re.MULTILINE)
>>> for i in m :
... print('->', i)
-> See the U.S.A. today.
-> It's right here, not
a world away.
->Average temp. is 66.5.
- 문장인식 패턴을 분석해보면 아래와 같다.
- [A-Z] 첫문자는 대문자
- .*? 모든문자, 게으른 탐색으로 그 뒤의 표현식은 포함안하도록 방지
- [.!?] 마침표
- (?= [A-Z]|$) 표현식 뒤에 빈칸과 다음문장의 시작을 의미하는 대문자가 오거나 혹은 문장이 끝이난다면(전방탐색)
- 전방탐색 없이 단순 마침표 [.!?] 로 구분했다면 U.S.A. 와 같은 문자열 또한 문장으로 인식됬을 것이다.
- re.DOTALL : .이 개행문자도 인식한다.
- re.MULTILINE : $가 개행문자를 문자열 종료 조건으로 인식한다.
[ 다중패턴 전방검색 ]
>>> import re
>>>
>>> # 비밀번호 규칙
>>> pat1 = r'(\w|[!@#$%^&*+-]){8,12}$' # 8~12 글자. 숫자, 특수문자 포함
>>> pat2 = r'(?=.*[a-zA-Z])' # 반드시 글자 포함
>>> pat3 = r'(?=.*\d)' # 반드시 숫자 포함
>>> pat4 = r'(?=.*[!@#$%^&*+-])' # 반드시 특수문자 포함
>>> pat = pat2 + pat3 + pat4 + pat1
>>>
>>> print(pat) # 패턴연산은 문자열과 같다
(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*+-])(\w|[!@#$%^&*+-]){8,12}$
>>> # 전방탐색 pat2,3,4를 앞에 둠으로써 해당 조건들의 일치를 확인 후 최종적으로 pat1을 검사한다.
>>> # pat2,3,4는 전방탐색이므로 문자들을 소비하지 않기 때문에 8~12글자 제한 조건 탐색에 영향이 없다.
>>>
>>> passwd = 'HenryThe5!'
>>> print( bool(re.match(pat, passwd)) )
True
[ 부정정 전방탐색 ]
(?!expr)
- 다음 문자가 특정 하위 패턴에 일치하지 않았을 때 성공
>>> import re
>>>
>>> # 문장인식 패턴을 부정형으로 만들었다.
>>> pat = r'[A-Z].*?[.!?](?! [a-z]|\w)'
>>> s = '''See the U.S.A today. It's right here, not
... a world away. Average temp. is 70.5. It's fun!'''
>>>
>>> s = re.sub(r'\n', '', s) # 보기 편하게 개행제거
>>> m = re.findall(pat, s)
>>> for i in m :
>>> print('->', i)
-> See the U.S.A. today.
-> It's right here, not a world away.
-> Average temp. is 70.5.
-> It's fun!
명명그룹
- 표현식 : (?P<이름>expr)
- 호출 : (?P=이름)
>>> import re
>>>
>>> pat = r'(?P<first>\w+) (?P<last>\w+)'
>>> s = 'Jane Austen'
>>> m = re.match(pat, s)
>>>
>>> print('first name = ', m.group('first'))
>>> print('last name = ', m.group('last'))
first name = Jane
last name = Austen
>>>
>>> # 반복패턴
>>> pat = r'(\w+) \1'
>>> pat = r'(?P<word>\w+ (?P=word)'
>>> m = re.search(pat, 'The the dog.', flags=re.I)
split
- 스플릿 기능
리스트 = re.split(패턴, 문자열, maxsplit=0, flags=0)
>>> import re
>>>
>>> pat = r', *| +'
>>> lst = re.split(pat, '3, 5 7 8,10, 11')
>>> print(lst)
['3', '5', '7', '8', '10', '11']
Scanner
- 토큰을 인식하여 원하는 형식(값)으로 반환
scanner_nm = re.Scanner([
(토큰_패턴1, 함수1),
(토큰_패턴2, 함수2),
...
)]
>>> import re
>>>
>>> def sc_oper(scanner, tok): return tok
>>> def sc_int(scanner, tok): return int(tok)
>>> def sc_float(scanner, tok): return float(tok)
>>>
>>> scanner = re.Scanner ([
... (r'[*+/-]', sc_oper),
... (r'\d+\.\d*', sc_float),
... (r'\d+', sc_int),
... (r'\s+', None)
... ])
>>>
>>> print(scanner.scan('32 6.67+ 10 5- *'))
([32, 6.67, '+', 10, 5, '-', '*'], '')
- 숫자는 정수/실수로 반환되었고, 연산자는 문자열로 반환되었다.
[ RPN 번역기 구현 ]
>>> import re
>>>
>>> the_stk = [ ]
>>>
>>> scanner = re.Scanner ([
... (r'[*+/-]', lambda s, t: bin_op(t)),
... (r'\d+\.\d*', lambda s, t: the_stk.append(float(t))),
... (r'\d+', lambda s, t: the_stk.append(int(t))),
... (r'\s+', None)
... ])
>>>
>>> def bin_op(tok):
... op2, op1 = the_stk.pop(), the_stk.pop()
... if tok == '+' :
... the_stk.append(op1 + op2)
... elif tok == '*' :
... the_stk.append(op1 * op2)
... elif tok == '/' :
... the_stk.append(op1 / op2)
... elif tok == '-' :
... the_stk.append(op1 - op2)
>>>
>>> def main() :
... input_str = input('Enter RPN string: ')
... tokens, unknown = scanner.scan(input_str)
... if unknown :
... print('Unrecognized input : ', unknown)
... else :
... print('Answer is ', the_stk.pop())
>>>
>>> main()
- operator를 이용한 간소화
>>> import re
>>> import operator
>>>
>>> the_stk = [ ]
>>>
>>> scanner = re.Scanner ([
... (r'[+]', lambda s, t: bin_op(operator.add)),
... (r'[*]', lambda s, t: bin_op(operator.mul)),
... (r'[-]', lambda s, t: bin_op(operator.sub)),
... (r'[/]', lambda s, t: bin_op(operator.truediv)),
... (r'\d+\.\d*', lambda s, t: the_stk.append(float(t))),
... (r'\d+', lambda s, t: the_stk.append(int(t))),
... (r'\s+', None)
... ])
>>>
>>> def bin_op(tok):
... op2, op1 = the_stk.pop(), the_stk.pop()
... the_stk.append(oper(op1, op2))
>>>
>>> def main() :
... input_str = input('Enter RPN string: ')
... tokens, unknown = scanner.scan(input_str)
... if unknown :
... print('Unrecognized input : ', unknown)
... else :
... print('Answer is ', the_stk.pop())
>>>
>>> main()