Python/공통이론

Python - 고급 정규표현식

zeroup 2023. 5. 5. 11:53

 

태깅 문제 고치기

(?: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()