7 분 소요


학습목표


정규표현식(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’ 이 반환되게 되는 것이다.


백슬래시 (\)

  1. 다른 문자와 함께 사용되어 특수한 의미를 지닌다.
    • \d : [0-9] 와 동일 (숫자)
    • \D : 숫자가 아닌 문자를 의미하며, [ ^0-9 ]와 동일
    • \s : 공백 문자(띄어쓰기, 엔터, 탭 등…)
    • \S : 공백이 아닌 문자
    • \w : 알파벳 대소문자, 숫자 [a-zA-Z0-9]와 동일
    • \W : non-alpha-numeric 문자 [ ^a-zA-Z0-9 ]와 동일
  2. 메타 캐릭터가 캐릭터 자체를 표현하도록 할 경우 사용한다.
    • \. , \\


예)

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) : 인덱스 0match의 값과 동일한 값을 가지고 있다.


{}

  • *,+,?을 사용하여 반복적인 패턴을 찾는 것이 가능하지만, 반복의 횟수 제한불가
  • 패턴 뒤에 위치하는 중괄호{}에 숫자를 명시하면 해당 숫자 만큼의 반복인 경우에만 매칭
  • {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
  • 결과문자열로 다시 반환
  • 두 번째 인자특정 문자열이 될 수도 있고, 함수가 될 수도 있음
  • count0인 경우 전체를 의미하고, 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 식의 의미는 위와 같은 기능을 하는 대신 count1이기 때문에 치환 과정한 번만 진행하여 뒤의

’ 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에 정규표현식을 저장하고 searchfindall 등의 메소드를 사용할 수 있게된다.

맺으며

정규표현식은 특정한 규칙을 가진 문자열패턴을 다루는데 용이한 만큼 웹페이지에서 전화번호이메일 주소등을 발췌할 때 굉장히 좋다. 또한 로그 파일에서 특정 에러메시지가 들어간 라인과 같은 것들도 이를 통해 찾아 낼 수 있어 re모듈은 익혀두면 상당히 좋은 모듈인 것 같다.

댓글남기기