-
Chapter 07-3 강력한 정규 표현식의 세계로_2Do it! 점프 투 파이썬 2022. 6. 9. 20:23
전방 탐색
정규식에 막 입문한 사람들이 가장 어려워하는 것이 바로 전방 탐색(Lookahead Assertions)확장 구문이다. 정규식 안에 이 확장 구문을 사용하면 순식간에 암호문처럼 알아보기 어렵게 바뀌기 때문이다. 하지만 이 전방 탐색이 꼭 필요한 경우가 있으며 매우 유용한 경우도 많으니 꼭 알아 두자.
다음 예를 보자.
>>> p = re.compile(".+:")
>>> m = p.search("http://google.com")
>>> print(m.group( ))
http:정규식 ".+:"과 일치하는 문자열로 http:를 돌려주었다. 만약 http:라는 검색 결과에서 :을 제외하고 출력하려면 어떻게 해야 할까? 위 예는 그나마 간단하지만 훨씬 복잡한 정규식이어서 그루핑은 추가로 할 수 없다는 조건까지 더해진다면 어떻게 해야 할까?
이럴 때 사용할 수 있는 것이 바로 전방 탐색이다. 전방 탐색에는 긍정(Positive)과 부정(Negative)의 2종류가 있고 다음과 같이 표현한다.
정규식 종류 설명 (?=...) 긍정형 전방 탐색 ...에 해당하는 정규식과 매치되어야 하며 조건이 통과되어도 문자열이 소비되지 않는다. (?!...) 부정형 전방 탐색 ...에 해당하는 정규식과 매치되지 않아야 하며 조건이 통과되어도 문자열이 소비되지 않는다. 긍정형 전방 탐색
긍정형 전방 탐색을 사용하면 http:의 결과를 http로 바꿀 수 있다. 다음 예를 보자.
>>> p = re.compile(".+(?=:)")
>>> m = p.search("http://google.com")
>>> print(m.group( ))
http정규식 중 :에 해당하는 부분에 긍정형 전방 탐색 기법을 적용하여 (?=:)으로 변경하였다. 이렇게 되면 기존 정규식과 검색에서는 동일한 효과를 발휘하지만 :에 해당하는 문자열이 정규식 엔진에 의해 소비되지 않아(검색에는 포함되지만 검색 결과에는 제외됨) 검색 결과에서 :이 제거된 후 돌려주는 효과가 있다.
자, 이번에는 다음 정규시을 보자.
.+[.].*$ 이 정규식은 '파일 이름 + . + 확장자'를 나타내는 정규식이다. 이 정규식은 foo.bar, autoexec.bat, sendmail.cf 같은 형식의 파일과 매치될 것이다.
이 정규식에 확장자가 'bat인 파일은 제외해야 한다'는 조건을 추가해 보자. 가장 먼저 생각할 수 있는 정규식은 다음과 같다.
.*[.]([^b]..|.[^a].|..[^t])$ <- 문자 클래스 [ ] 안의 ^ 메타 문자는 반대(not)를 의미함 이 정규식은 | 메타 문자를 사용하여 확장자의 첫 번째 문자가 b가 아니거나 두 번째 문자가 a가 아니거나 세 번째 문자가 t가 아닌 경우를 의미한다. 이 정규식에 의하여 foo.bar는 제외되지 않고 autoexec.bat은 제외되어 만족스러운 결과를 돌려준다. 하지만 이 정규식은 아쉽게도 sendmail.cf처럼 확장자의 문자 개수가 2개인 케이스를 포함하지 못하는 오동작을 하기 시작한다.
따라서 다음과 같이 바꾸어야 한다.
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$ 확장자의 문자 개수가 2개여도 통과되는 정규식이 만들어졌다. 하지만 정규식은 점점 더 복잡해지고 이해하기 어려워진다.
그런데 여기에서 bat 파일말고 exe 파일도 제외하라는 조건이 추가로 생긴다면 어떻게 될까? 이 모든 조건을 만족하는 정규식을 구현하려면 패턴은 더욱더 복잡해질 것이다.
부정형 전방 탐색
이러한 상황의 구원 투수는 바로 부정형 전방 탐색이다. 위 예는 부정형 전방 탐색을 사용하면 다음과 같이 간단하게 처리된다.
.*[.](?!bat$).*$ 확장자가 bat가 아닌 경우에만 통과된다는 의미이다. bat 문자열이 있는지 조사하는 과정에서 문자열이 소비되지 않으므로 bat가 아니라고 판단되면 그 이후 정규식 매치가 진행된다.
exe 역시 제외하라는 조건이 추가되더라도 다음과 같이 간단히 표현할 수 있다.
.*[.](?!bat$|exe$).*$ 문자열 바꾸기
sub 메서드를 사용하면 정규식과 매치되는 부분을 다른 문자로 쉽게 바꿀 수 있다.
다음 예를 보자.
>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'sub 메서드의 첫 번째 매개변수는 '바꿀 문자열(replacement)'이 되고, 두 번째 매개변수는 '대상 문자열'이 된다. 위 예에서 볼 수 있듯이 blue 또는 white 또는 red라는 문자열이 colour라는 문자열로 바뀌는 것을 확인할 수 있다.
그런데 딱 한 번만 바꾸고 싶은 경우도 있다. 이렇게 바꾸기 횟수를 제어하려면 다음과 같이 세 번째 매개변수로 count 값을 넘기면 된다.
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'처음 일치하는 blue만 colour라는 문자열로 한 번만 바꾸기가 실행되는 것을 알 수 있다.
*sub 메서드와 유사한 subn 메서드
subn 역시 sub와 동일한 기능을 하지만 반환 결과를 튜플로 돌려준다는 차이가 있다. 돌려준 튜플의 첫 번째 요소는 변경된 문자열이고, 두 번째 요소는 바꾸기가 발생한 횟수이다.
>>> p = re.compile('(blue|white|red)')
>>> p.subn( 'colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)sub 메서드를 사용할 때 참조 구문 사용하기
sub 메서드를 사용할 때 참조 구문을 사용할 수 있다. 다음 예를 보자.
>>> p = re.compile(r"(?P<name>\w+)\s+(?P<phone>(\d+)[-]\d+[-]\d+)")
>>> print(p.sub("g<phone> \g<name>", "park 010-1234-1234"))
010-1234-1234 park위 예는 '이름 + 전화번호'의 문자열을 '전화번호 + 이름'으로 바꾸는 예이다. sub의 바꿀 문자열 부분에 '\g<그룹 이름>'을 사용하면 정규식의 그룹 이름을 참조할 수 있게 된다.
다음과 같이 그룹 이름 대신 참조 번호를 사용해도 마찬가지 결과를 돌려준다.
>>> p = re.compile(r"(?P\w+)\s+(?P(\d+)[-]\d+[-]\d+)")
>>> print(p.sub("\g<2> \g<1>", "park 010-1234-1234"))
010-1234-1234 parksub 메서드의 매개변수로 함수 넣기
sub 메서드의 첫 번째 매개변수로 함수를 넣을 수도 있다. 다음 예를 보자.
>>> def hexrepl(match):
... value = int(match.group( ))
... return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'hexrepl 함수는 match 객체(위에서 숫자에 매치되는)를 입력으로 받아 16진수로 변환하여 돌려주는 함수이다. sub의 첫 번째 매개변수로 함수를 사용할 경우 해당 함수의 첫 번째 매개변수에서 정규식과 매치된 match 객체가 입력된다. 그리고 매치되는 문자열은 함수의 반환 값으로 바뀌게 된다.
Greedy vs Non-Greedy
정규식에서 Greed(탐욕스러운)란 어떤 의미일까? 다음 예제를 보자.
>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span( ))
(0, 32)
>>> print(re.match('<.*>', s).group( ))
<html><head><tilte>Title</title>'<.*>' 정규식의 매치 결과로 <html> 문자열을 돌려주기를 기대했을 것이다. 하지만 * 메타 문자는 매우 탐욕스러워서 매치할 수 있는 최대한의 문자열인 <html><head><title>Title</title> 문자열을 모두 소비해 버렸다. 어떻게 하면 이 탐욕스러움을 제한하고 <html> 문자열까지만 소비하도록 막을 수 있을까?
다음과 같이 non-greedy 문자인 ?를 사용하면 *의 탐욕을 제한할 수 있다.
>>> print(re.match('<.*>', s).group( ))
<html>non-greedy 문자인 ?는 *?, +?, ??, {m,n}?와 같이 사용할 수 있다. 가능한 한 가장 최소한의 반복을 수행하도록 도와주는 역할을 한다.
출처 : "점프 투 파이썬"
'Do it! 점프 투 파이썬' 카테고리의 다른 글
Chapter 07-3 강력한 정규 표현식의 세계로_1 (0) 2022.06.09 Chapter 07-2 정규 표현식 시작하기_3 (0) 2022.06.09 Chapter 07-2 정규 표현식 시작하기_2 (0) 2022.06.09 Chapter 07-2 정규 표현식 시작하기_1 (0) 2022.06.09 Chapter 07-1 정규 표현식 알아보기 (0) 2022.06.09