[Python] 정규표현식과 re모듈 사용법
학습목표
정규표현식(re)에 대한 이해 및 숙지
- 정규표현식이란?
- regular expression
- 특정 패턴에 상응하는 문자열을 ‘검색’, ‘치환’, ‘제거’ 하는 기능을 가짐
- 정규표현식의 도움없이도 패턴을 찾는 작업(Rule 기반)은 불완전하거나, 작업의 cost가 높음
- ex) 이메일 형식 판별, 전화번호 형식 판별, 숫자로만 이루어진 문자열 등
- raw string
- 문자열 앞에 ‘r‘이 붙으면 해당 문자열이 구성된 그대로 문자열로 반환한다.
a = 'abcdef\n' #escape 문자열
print(a)
'''
abcdef
'''
b = r'abcdef\n'
print(b)
'''
abcdef\n
'''
escape string이란 escape sequence를 따르는 문자들로서 다음 문자가 특수문자임을 알리는 백슬래시(‘\’)를 사용한다.
위의 a는 \n이 escape문자열로 작용하여 abcdef를 출력 후 new line으로 작용하는 역할을 하는 반면, b와 같이 문자열 정의 후 앞에 문자 ‘r’을 붙이게 되면 ‘\n’ 또한 순수 문자로 인식하여 raw string, 즉 모든 문자를 있는 그대로 표현한 모습이다.
기본패턴
- a,T,4 등 과 같은 하나 하나의 character들은 정확히 해당 문자와 일치
- ex) 패턴 test는 test 문자열과 일치
- 대소문자의 경우 기본적으로 구별가능하나, 구별하지 않도록 설정 가능
- 몇몇 문자들은 예외로서 특별한 의미를 가짐
- . ^ $ * + ? {} [] \ () 등등
- .(온점) : 하나의 character와 일치 (단, new line(엔터)은 제외한다.)
- \w - 문자 character와 일치 [a-zA-Z0-9_]
- \s - 공백 문자와 일치
- \t, \n, \r - 각각 tab, new line, return을 의미
- \d - 숫자 character와 일치 [0-9]
- ^ 는 시작, $는 끝을 의미
- \가 붙으면 스페셜한 의미는 사라짐.
- 예를 들어, ‘\. ‘는 ‘ . ‘자체를 의미하고 ‘\\’는 ‘\’를 의미한다.
- 참고 : 링크 (파이썬 문서 - re 모듈)
search method(검색 함수)
- 첫번째로 패턴을 찾으면 match 객체를 반환
- 패턴을 찾지 못하면 None 객체 반환
import re
c = re.search(r'abc', 'abcdef')
print(c.start())
print(c.end())
print(c.group())
'''
0
3
abc
'''
먼저 re 모듈을 사용하기 위해 import를 한다.
- re.search(r’찾고자 하는 문자열’, ‘주어지는 문자열’)
- 주어지는 문자열 속에서 찾고자 하는 문자열을 찾아낸다.
-
search 객체를 변수 c 에 저장시키고 이 객체의 start, end, group 메소드를 각각 출력한 결과
각각 0, 3, abc가 나왔다.
- 이를 통해 start()는 첫번째 인덱스를 나타내고 end()는 마지막 인덱스를 나타내는데 해당 인덱스를 포함하지 않기 때문에 3이 출력되었다. 마지막으로 group()은 검색된 객체 자체를 가져올 수 있는 함수이다.
c = re.search(r'abc', 'ab')
print(c)
# None
이처럼 주어진 문자열에 찾고자 하는 문자열이 존재하지 않는다면 None 객체를 반환한다.
c = re.search(r'\d\d', '5zxd789rrr856')
print(c)
# <re.Match object; span=(4, 6), match='78'>
이번엔 \d\d를 통해 찾고자 하는 문자열을 두 숫자가 연속 된 것으로 범위를 이 전보다 넓게 잡았고 그 결과 주어진 문자열에서 가장 먼저 두 숫자가 연속되는 걸 만족하는 것은 ‘78’ 이기 때문에 re 객체의 인덱스는 4에서 6, match에는 78 이라는 값을 얻어낼 수 있다.
c = re.search(r'..\w\w', '!@#$ABCDwxyz')
print(c)
# <re.Match object; span=(2, 6), match='#$AB'>
위의 .(온점)이 의미하는 것은 new line(엔터 키)을 제외한 모든 character 중 하나의 문자를 뜻하는 것이므로 찾고자 하는 문자열은 아무 character나 두 개 연속으로 온 뒤 문자 character가 두 개 연속으로 온 경우를 뜻하는 것이다. 따라서 ’#$AB’ 의 결과가 나온 모습이다.
metacharacters(메타 캐릭터)
-
[] 내부의 metacharacter는 캐릭터 그 자체를 표현한다.
- 예를 들어,
- [abcd] : a or b or c or d
- [abc.^] : a or b or c or . or ^
- [a-f] : 문자 사이에 들어간 ’ - (대시) ‘ 는 두 문자 사이에 속하는 문자 중 하나를 의미
- [0-9] : 모든 숫자
- [a-z] : 모든 소문자
- [A-Z] : 모든 대문자
- [a-zA-Z0-9] : 모든 알파벳 문자 및 숫자
- [^0-9] : ^가 맨 앞에 사용되는 경우에는 해당 문자 패턴이 아닌 것과 매칭 (not(부정)의 의미!)
c = re.search(r'[cmb]at', 'bat')
print(c)
# <re.Match object; span=(0, 3), match='bat'>
찾고자 하는 문자열은 3글자 이며 첫번째 자리에는 c,m, b 셋 중에 어느 것이 와도 상관 없으며 주어진 문자열은 ‘bat’ 이므로 match의 값은 ‘bat’ 이다.
c = re.search(r'[^xyz]arrot', 'zarrot')
print(c)
#None
c = re.search(r'[^xyz]arrot', 'carrot')
print(c)
# <re.Match object; span=(0, 6), match='carrot'>
이번엔 ^를 활용하여 not 기능을 사용해 본 것이다. arrot 앞에 오는 문자는 [ ^xyz ] 즉 x,y,z 를 제외한 어떠한 character 하나를 의미하므로 ‘zarrot’을 주었을 땐 ‘z’가 검출되어 None 객체가 반환되고
‘carrot’을 주었을 땐 ‘c’가 x,y,z 셋 중 어느 것에도 포함되지 않기 때문에 match에는 ‘carrot’ 이 반환되게 되는 것이다.
백슬래시 (\)
- 다른 문자와 함께 사용되어 특수한 의미를 지닌다.
- \d : [0-9] 와 동일 (숫자)
- \D : 숫자가 아닌 문자를 의미하며, [ ^0-9 ]와 동일
- \s : 공백 문자(띄어쓰기, 엔터, 탭 등…)
- \S : 공백이 아닌 문자
- \w : 알파벳 대소문자, 숫자 [a-zA-Z0-9]와 동일
- \W : non-alpha-numeric 문자 [ ^a-zA-Z0-9 ]와 동일
- 메타 캐릭터가 캐릭터 자체를 표현하도록 할 경우 사용한다.
- \. , \\
예)
c = re.search(r'\Dython', 'python')
print(c)
# <re.Match object; span=(0, 6), match='python'>
c = re.search(r'\Dython', '3ython')
print(c)
# None
\D는 숫자가 아닌 character를 의미하므로 주어진 문자열이 python 일 때는 match에 ‘python’ 이라는 값이 저장되지만, ‘3ython’일 때는 맨 앞자리가 숫자이기 때문에 None 객체를 반환하는 모습니다.
반복 패턴
-
패턴 뒤에 위치하는 *, +, ?는 해당 패턴이 반복적으로 존재하는 지 검사
- ’+’ -> 1번 이상의 패턴이 발생
- ‘*‘ -> 0번 이상의 패턴이 발생
- ’?’ -> 0 혹은 1번의 패턴이 발생
-
패턴이 발생하는 경우 반복을 greedy search 를 이용하여 검색
-
즉,가능한 많은 부분이 매칭 되도록 한다.
ex)
-
c = re.search(r'a[bcd]*b', 'abcbdccb')
print(c)
# <re.Match object; span=(0, 8), match='abcbdccb'>
위의 경우처럼 ‘ab’, ‘abcb’, ‘abcbdccb’ 전부 가능하지만 최대한 많은 부분이 매칭된 ‘abcbccb’ 가 검출되는 식이다.
- ’+’와 ‘*‘의 차이점 ?
#case : '+'
c = re.search(r'pi+g', 'pg')
print(c)
#None
#case: '*'
c = re.search(r'pi+g', 'pg')
print(c)
#pg
+의 경우 p가 나오고 i의 패턴이 1번 이상 발생해야 하기 때문에 ‘pg’ 에서는 이를 검출에 실패하는 반면,
***의 경우 p가 나오고 i의 **패턴이 0번 이상 발생해야 하기 때문에 0도 포함하므로 i가 나오지 않아도 된다. 따라서 ‘pg’가 주어졌을 경우에도 ‘pg’를 검출해 낼 수 있는 것이다.
^*, *$
- ^ 문자열의 맨 앞부터 일치하는 경우를 검색
- $ 문자열의 맨 뒤부터 일치하는 경우를 검색
c = re.search(r'b\w+a', 'wabana')
print(c)
# 'bana'
c = re.search(r'^b\w+a', 'wabana')
print(c)
# None
’^’를 붙여주게 되면 반드시 맨 앞부터 시작하여 ‘b\w+a’가 검출되어야 하는 것이기 때문에 ‘wabana’는 ‘w’로시작하여 검출에 실패하는 것이다.
Grouping
- ()을 사용하여 grouping
- 매칭결과를 각 그룹별로 분리하여 관리 가능
- 패턴을 명시 할 때, 각 그룹을 괄호() 안에 넣어 분리하여 사용
c = re.search(r'(\w+)@(.+)', 'asdf@email.com')
print(c.group(1))
# asdf
print(c.group(2))
# email.com
괄호를 통해 그룹핑을 진행할 그룹을 선정하고 group()에 인덱스를 넣어 원하는 group을 가져올 수 있다.
- group(0) : 인덱스 0은 match의 값과 동일한 값을 가지고 있다.
{}
- *,+,?을 사용하여 반복적인 패턴을 찾는 것이 가능하지만, 반복의 횟수 제한은 불가
- 패턴 뒤에 위치하는 중괄호{}에 숫자를 명시하면 해당 숫자 만큼의 반복인 경우에만 매칭
- {4} - 4번 반복
- {3,4} - 3 ~ 4번 반복
c = re.search('pi{3}g','piiig')
print(c)
# 'piiig'
미니멈 매칭(non-greedy way)
- 기본적으로 *,+,?를 사용하면 greedy(맥시멈 매칭)하게 동작함
- *?, +?을 이용하여 해당 기능을 구현
c = re.search(r'<.+>', '<html>haha</html>')
print(c)
# '<html>haha</html>'
c = re.search(r'<.+?>','<html>haha</html>')
print(c)
# '<html>'
greedy 방식이 맥시멈 매칭이라면 non-greedy 방식은 미니멈 매칭이므로 ’<.+?>’ 이 뜻하는 것은 <>안에 문자가 담긴 여러 형식 중 최소 그리고 최초의 경우만 선택하여 값이 검출된다.
{}?
- {m,n}의 경우 m번에서 n번 반복하나 greedy하게 동작
- {m,n}?로 사용하면 non-greedy하게 동작. 즉, 최소 m번만 매칭하면 만족
c = re.search(r'a{3,5}', 'aaaaa')
print(c)
# 'aaaaa'
c = re.search(r'a{3,5}?', 'aaaaa')
print(c)
# 'aaa'
{}뒤에 ?가 붙는 경우 non-greedy하게 동작하기 때문에 3번만 만족해도 동작하므로 최소값인 ‘aaa’ 를 검출해 내는 것이다.
match
- search와 유사하나, 주어진 문자열의 시작부터 비교하여 패턴이 있는지 확인
- 시작부터 해당 패턴이 존재하지 않다면 None 반환
c1 = re.match(r'\d\d\d', 'the number: 246')
print(c1)
#None
c2 = re.match(r'\d\d\d', '246 : the number')
print(c2)
#'246'
c1은 match로 숫자가 세 개 연속되어 시작하는 문자열을 찾는데, 주어진 문자열은 알파벳으로 시작하기 때문에 조건에 충족하지가 않아 None 객체를 반환하고
c2도 위와 마찬가지이지만 주어진 문자열이 숫자 세 개로 시작하기 때문에 조건에 딱 맞아 해당하는 ‘246’ 값이 검출된다.
findall
- search가 최초로 매칭되는 패턴만 반환한다면, findall은 매칭되는 전체의 패턴을 반환
- 매칭되는 모든 결과를 리스트(list)의 형태로 반환
c = re.findall(r'[\w-]+@[\w.]+', 'apple@email.com blahblah banana@email.com goodjob')
print(c)
#['applpe@email.com', 'banana@email.com']
이메일 형식을 찾기 위해서 정규표현식을 ’[\w-]+@[\w.]+’와 같이 표현하였고 그 결과 알맞게 리스트의 형식으로 검출된 것을 확인할 수 있다.
sub
- 주어진 문자열에서 일치하는 모든 패턴을 replace
- 그 결과를 문자열로 다시 반환함
- 두 번째 인자는 특정 문자열이 될 수도 있고, 함수가 될 수도 있음
- count가 0인 경우 전체를 의미하고, 1이상인 경우 해당 숫자로 치환됨
c1 = re.sub(r'[\w-]+@[\w.]+', 'great', 'apple@email.com blahblah banana@email.com grape strawberry')
print(c1)
#great blahblah great grape strawberry
c2 = re.sub(r'[\w-]+@[\w.]+', 'great', 'apple@email.com blahblah banana@email.com grape strawberry', count = 1)
print(c2)
#great blahblah banana@email.com grape strawberry
c1 식의 의미는 이메일 형식의 모든(count가 default로 0이기 때문) 문자열을 ‘great’로 치환하겠다는 의미이고 그 결과를 보여준다.
c2 식의 의미는 위와 같은 기능을 하는 대신 count가 1이기 때문에 치환 과정을 한 번만 진행하여 뒤의
’ banana @ email.com’은 치환이 되지 않은 모습이다.
compile
- 동일한 정규표현식을 매번 일일히 다시 쓰기 번거로움 문제점을 해결
- compile로 해당 표현식을 re.RegexObject 객체로 저장하여 사용 가능
email_reg = re.compile(r'[\w-]+@[\w.]+')
print(email_reg.search('apple@email.com banana grape'))
#'apple@email.com'
compile을 통해 email_reg에 정규표현식을 저장하고 search나 findall 등의 메소드를 사용할 수 있게된다.
맺으며
정규표현식은 특정한 규칙을 가진 문자열의 패턴을 다루는데 용이한 만큼 웹페이지에서 전화번호나 이메일 주소등을 발췌할 때 굉장히 좋다. 또한 로그 파일에서 특정 에러메시지가 들어간 라인과 같은 것들도 이를 통해 찾아 낼 수 있어 re모듈은 익혀두면 상당히 좋은 모듈인 것 같다.
댓글남기기