ProblemSolving/String

파이썬 정규 표현식 정리 및 문자열 파싱

OSNIM 2022. 6. 8. 18:20
반응형

정규 표현식 (Regular Expression)이란?

  • 복잡한 문자열을 처리할 때 사용하는 기법
  • 파이썬 뿐만 아니라 모든 프로그래밍 언어 공통에서 쓰이는 DSL(Domain Specific Language)
  • 특정 문자열을 찾고 싶을 때, 특정 문자열을 대체하고 싶을때, 문자열을 파싱할 때 주로 사용

1. 메타 문자(Meta Characters)

원래 문자가 가진 뜻이 아닌 특별한 용도로 사용하는 문자

1) []

  • 문자 클래스
  • [] 사이의 문자들 중 하나와 매치 또는 하이픈 (-)으로 연결 가능 ([0-9], [a-zA-Z])
  • [^] 처럼 ^로 시작하는 문자클래스는 반대의 의미 (해당 문자가 아니면 매치)
  • 예시: "[pt]op" 는 "pop", "top"와 매치 
  • 자주 사용하는 문자 클래스
  • \d - 숫자와 매치, [0-9]와 동일
  • \D - 숫자가 아닌 것과 매치, [^0-9]와 동일
  • \s - whitespace 문자와 매치, [ \t\n\r\f\v]와 동일, 맨 앞의 빈 칸은 공백문자(space)를 의미한다.
  • \S - whitespace 문자가 아닌 것과 매치, [^ \t\n\r\f\v]와 동일
  • \w - 문자+숫자(alphanumeric)와 매치, [a-zA-Z0-9_]와 동일
  • \W - 문자+숫자(alphanumeric)가 아닌 문자와 매치, [^a-zA-Z0-9_]와 동일
  • 대문자로 사용된 것은 소문자의 반대임을 추측할 수 있다.

2) .(dot, 닷)

  • \n 을 제외한 모든 문자와 매치
  • 예시: "a.c" 는 "aac", "abc", ... , "a0c", "a&c"... 과 매치
  • 문자 클래스 안에 있는 . 은 ([.])은 말 그대로 '.'  을 의미  

3) * (asterisk)

  • 0회 이상 반복
  • 예시: "ct"와 ca*t는 a가 0번 반복되어 매치
  • 예시: "cat"와 ca*t는 a가 1번 반복되어 매치 
  • 예시: "caaat"와 ca*t는 a가 3번 반복되어 매치 

4) +

  • *와 달리 1회 이상 반복되어야 매치되었다고 함
  • 예시: "ct"와 ca+t는 a가 0번 반복되어 매치가 아니라서 False

5) 반복 {n} / {n, } / {, n} / {m, n} / ?

  • {n}: 반드시 n회 반복할 때만 매치
  • {n, }: n이상 반복할 때 매치
  • {, n}: 0~n회 반복할 때 매치
  • {m, n}: m ~ n 반복할 때 매치
  • ? : 0회 또는 1회 반복할 때 매치 > {0,1}과 같은 표현

6) |

  • or과 동일한 의미, "A|B" 라는 정규식이 있다면 A 또는 B를 의미
p = re.compile('Crow|Servo')
m = p.match('CrowHello')
print(m) 
#<re.Match object; span=(0, 4), match='Crow'>

7) ^ 

  • 문자열의 맨 처음과 일치함을 의미
print(re.search('^Life', 'Life is too short'))
#<re.Match object; span=(0, 4), match='Life'>
print(re.search('^Life', 'My Life'))
#None

8) $

  • 문자열의 끝
print(re.search('short$', 'Life is too short'))
#<re.Match object; span=(12, 17), match='short'>
print(re.search('short$', 'Life is too short, you need python'))
#None

9) \ 

  • 이스케이프, 메타문자를 일반 문자로 인식하게 함
  • ^ 또는 $ 문자를 메타 문자가 아닌 문자 그 자체로 매치하고 싶은 경우에는 \^, \$ 로 사용하면 된다.

10) \A

"\A"는 문자열의 처음과 매치됨을 의미. ^ 메타 문자와 동일한 의미이지만 re.MULTILINE 옵션을 사용할 경우에는 다르게 해석 

re.MULTILINE 옵션을 사용할 경우 ^은 각 줄의 문자열의 처음과 매치되지만 \A는 줄과 상관없이 전체 문자열의 처음하고만 매치.

11) \Z

\Z는 문자열의 끝과 매치됨을 의미.

re.MULTILINE 옵션을 사용할 경우 $ 메타 문자와는 달리 전체 문자열의 끝과 매치.

12) \b

\b는 단어 구분자(Word boundary). 보통 단어는 whitespace에 의해 구분 됨

p = re.compile(r'\bclass\b')
print(p.search('no class at all'))  
#<re.Match object; span=(3, 8), match='class'>

\bclass\b 정규식은 앞뒤가 whitespace로 구분된 class라는 단어와 매치됨을 의미

따라서 no class at all의 class라는 단어와 매치

print(p.search('the declassified algorithm'))
#None

위 예의 the declassified algorithm 문자열 안에도 class 문자열이 포함되어 있긴 하지만 whitespace로 구분된 단어가 아니므로 매치되지 않음

print(p.search('one subclass is'))
None

subclass 문자열 역시 class 앞에 sub 문자열이 더해져 있으므로 매치되지 않음

 

\b 메타 문자를 사용할 때 주의해야 할 점

\b는 파이썬 리터럴 규칙에 의하면 백스페이스(BackSpace)를 의미하므로 백스페이스가 아닌 단어 구분자임을 알려 주기 위해 r'\bclass\b'처럼 Raw string임을 알려주는 기호 r을 반드시 붙여야 함

13) \B

\B 메타 문자는 \b 메타 문자와 반대, 즉 whitespace로 구분된 단어가 아닌 경우에만 매치

p = re.compile(r'\Bclass\B')
print(p.search('no class at all'))  
#None
print(p.search('the declassified algorithm'))
#<re.Match object; span=(6, 11), match='class'>
print(p.search('one subclass is'))
#None

class 단어의 앞뒤에 whitespace가 하나라도 있는 경우에는 매치가 안 됨

2. 파이썬에서 정규표현식을 지원하는 re 모듈

먼저 re 모듈을 불러온 뒤, 

  • 컴파일된 패턴 객체 사용
    p = re.compile("ABC")  # 패턴 객체를 반환
    x = p.search("ABCDABD") # 패턴 객체의 검색 메서드로 search 수행
  • 축약된 형태 
    x = re.search("ABC", "ABCDABD")
  • "패턴"이란 정규식을 컴파일 한 결과

일회성 작업에서는 축약형이 간단하나, 반복 수행이 필요한 경우 컴파일로 패턴 객체를 생성해 여러 번 재사용 가능

3. 정규식을 이용한 문자열 검색

컴파일 된 패턴 객체는 4가지 메서드 사용 가능

 

Method 목적
match() 문자열의 처음부터 정규식과 매치되는지 조사한다.
search() 문자열 전체를 검색하여 정규식과 매치되는지 조사한다.
findall() 정규식과 매치되는 모든 문자열(substring)을 리스트로 돌려준다.
finditer() 정규식과 매치되는 모든 문자열(substring)을 반복 가능한 객체로 돌려준다.

 

match 객체: 정규식의 검색 결과로 돌려주는 객체

match, search는 정규식과 매치될 때는 match 객체를 반환하고, 매치되지 않을 때는 None을 반환

 

1) match

문자열의 처음부터 정규식과 매치되는지 조사

m = p.match("python")
print(m)
# <re.Match object; span=(0, 6), match='python'>

"python" 문자열은 [a-z]+ 정규식에 부합되므로 match 객체 반환

m = p.match("3 python")
print(m)
# None

"3 python" 문자열은 처음에 나오는 문자 3이 정규식 [a-z]+에 부합되지 않으므로 None 반환

2) search

m = p.search("3 python")
print(m)
# <re.Match object; span=(2, 8), match='python'>

문자열 내부에 정규식과 매치되는지 조사

3) findall

result = p.findall("life is too short")
print(result)
# ['life', 'is', 'too', 'short']

"life is too short" 문자열의 'life', 'is', 'too', 'short' 단어를 각각 [a-z]+ 정규식과 매치해서 리스트로 반환

4) finditer

result = p.finditer("life is too short")
print(result)
#<callable_iterator object at 0x01F5E390>
for r in result: print(r)

#<re.Match object; span=(0, 4), match='life'>
#<re.Match object; span=(5, 7), match='is'>
#<re.Match object; span=(8, 11), match='too'>
#<re.Match object; span=(12, 17), match='short'>

findall과 동일하지만 반복 가능한 객체(iterator object)를 반환

반복 가능한 객체가 포함하는 각각의 요소는 match 객체

4. match 객체의 메서드

 

method 목적
group() 매치된 문자열을 돌려준다.
start() 매치된 문자열의 시작 위치를 돌려준다.
end() 매치된 문자열의 끝 위치를 돌려준다.
span() 매치된 문자열의 (시작, 끝)에 해당하는 튜플을 돌려준다.

 

match 메서드 사용시

 m = p.match("python")
m.group()
# 'python'
m.start()
# 0
m.end()
# 6
m.span()
# (0, 6)

search 메서드 사용시

m = p.search("3 python")
m.group()
#'python'
m.start()
#2
m.end()
#8
m.span()
#(2, 8)

5. 컴파일 옵션

  • DOTALL(S): . 이 줄바꿈 문자를 포함하여 모든 문자와 매치할 수 있게 해줌.
  • IGNORECASE(I): 대소문자에 관계없이 매치를 가능하게 함.
  • MULTILINE(M): 여러줄과 매치할 수 있게 해줌. (^, $ 메타문자의 사용과 관계가 있는 옵션이다)
  • VERBOSE(X): verbose 모드를 사용할 수 있도록 함. (정규식을 보기 편하게 만들수 있고 주석등을 사용할 수 있게된다.)

옵션을 사용할 때는 re.DOTALL처럼 전체 옵션 이름을 써도 되고 re.S처럼 약어를 써도 됨.

 

1) DOTALL, S

. 메타 문자는 줄바꿈 문자(\n)를 제외한 모든 문자와 매치

만약 \n 문자도 포함하여 매치하고 싶다면 re.DOTALL 또는 re.S 옵션을 사용해 정규식을 컴파일해야함

p = re.compile('a.b', re.DOTALL)
m = p.match('a\nb')
print(m)
# <re.Match object; span=(0, 3), match='a\nb'>

보통 re.DOTALL 옵션은 여러 줄로 이루어진 문자열에서 \n에 상관없이 검색할 때 많이 사용

 

2) IGNORECASE, I

re.IGNORECASE 또는 re.I 옵션은 대소문자 구별 없이 매치를 수행할 때 사용하는 옵션

p = re.compile('[a-z]+', re.I)
p.match('python')
# <re.Match object; span=(0, 6), match='python'>
p.match('Python')
# <re.Match object; span=(0, 6), match='Python'>
p.match('PYTHON')
# <re.Match object; span=(0, 6), match='PYTHON'>

[a-z]+ 정규식은 소문자만을 의미하지만 re.I 옵션으로 대소문자 구별 없이 매치가 가능

 

3) MULTILINE, M

import re
p = re.compile("^python\s\w+", re.MULTILINE)

data = """python one
life is too short
python two
you need python
python three"""

print(p.findall(data))
# ['python one', 'python two', 'python three']

re.MULTILINE 옵션으로 인해 ^ 메타 문자가 문자열 전체가 아닌 각 줄의 처음이라는 의미를 가짐

^, $ 메타 문자를 문자열의 각 줄마다 적용

 

4) VERBOSE, X

re.VERBOSE 옵션을 사용하면 문자열에 사용된 whitespace는 컴파일할 때 제거 (단 [ ] 안에 사용한 whitespace는 제외)

줄 단위로 #기호를 사용하여 주석문을 작성 가능

charref = re.compile(r'&[#](0[0-7]+|[0-9]+|x[0-9a-fA-F]+);')
charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

첫 번째와 두 번째 예를 비교해 보면 컴파일된 패턴 객체인 charref는 모두 동일한 역할을 함

하지만 정규식이 복잡할 경우 두 번째처럼 주석을 적고 여러 줄로 표현하는 것이 훨씬 가독성이 좋음

6. 백슬래시 문제

예를 들어 어떤 파일 안에 있는 "\section" 문자열을 찾기 위한 정규식을 만든다고 가정

\section

이 정규식은 \s 문자가 whitespace로 해석되어 의도한 대로 매치가 이루어지지 않음

따라서 다음과 같이 위 정규식을 컴파일하려면 다음과 같이 작성

 p = re.compile('\\section')

하지만 위처럼 정규식을 만들어서 컴파일하면 실제 파이썬 정규식 엔진에는 파이썬 문자열 리터럴 규칙에 따라 \\ \로 변경되어 \section이 전달됨

(이 문제는 위와 같은 정규식을 파이썬에서 사용할 때만 발생 (파이썬의 리터럴 규칙). 유닉스의 grep, vi 등에서는 이러한 문제 없음)

 p = re.compile('\\\\section')

결국 정규식 엔진에 \\ 문자를 전달하려면 파이썬은 \\\\처럼 백슬래시를 4개나 사용해야 함

 

위 문제를 해결하기 위해 파이썬 정규식에는 Raw String 규칙이 생김

컴파일해야 하는 정규식이 Raw String임을 알려 줄 수 있도록 파이썬 문법

>>> p = re.compile(r'\\section')

위와 같이 정규식 문자열 앞에 r 문자를 삽입하면 이 정규식은 Raw String 규칙에 의하여 백슬래시 2개 대신 1개만 써도 2개를 쓴 것과 동일한 의미를 가짐

(만약 백슬래시를 사용하지 않는 정규식이라면 r의 유무에 상관없이 동일한 정규식이 됨)

2 (), 그룹핑

ABC 문자열이 계속해서 반복되는지 조사하는 정규식을 작성하고 싶을때 지금까지 공부한 내용으로는 위 정규식을 작성할 수 없음. 이 경우 필요한 것이 그루핑(Grouping)

보통 반복되는 문자열을 찾을 때 그룹을 사용하는데, 그룹을 사용하는 보다 큰 이유는 매치된 문자열 중에서 특정 부분의 문자열만 뽑아내기 위해서 자주 사용

p = re.compile('(ABC)+')
m = p.search('ABCABCABC OK?')
print(m)
#<re.Match object; span=(0, 9), match='ABCABCABC'>
print(m.group())
#ABCABCABC

예시 

p = re.compile(r"\w+\s+\d+[-]\d+[-]\d+")
m = p.search("park 010-1234-1234")

\w+\s+\d+[-]\d+[-]\d+ 이름 + " " + 전화번호 형태의 문자열을 찾는 정규식

매치된 문자열 중에서 이름만 뽑는 방법

p = re.compile(r"(\w+)\s+\d+[-]\d+[-]\d+")
m = p.search("park 010-1234-1234")
print(m.group(1))
#park

이름에 해당하는 \w+ 부분을 그룹 (\w+)으로 만들면 match 객체의 group(인덱스) 메서드를 사용하여 그루핑된 부분의 문자열만 뽑아냄

group(인덱스) 설명
group(0) 매치된 전체 문자열
group(1) 첫 번째 그룹에 해당되는 문자열
group(2) 두 번째 그룹에 해당되는 문자열
group(n) n 번째 그룹에 해당되는 문자열

전화번호만 뽑아내는 방법

p = re.compile(r"(\w+)\s+(\d+[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
print(m.group(2))
#010-1234-1234

국번만 뽑아내는 방법

p = re.compile(r"(\w+)\s+((\d+)[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
print(m.group(3))
#010

그룹을 중첩되게 사용하여 안쪽의 그룹만 뽑아 낼 수 있음

그룹이 중첩되어 있는 경우는 바깥쪽부터 시작하여 안쪽으로 들어갈수록 인덱스가 증가

 

1) 루핑된 문자열 재참조하기 (메타 문자 \1, \2, ...)

p = re.compile(r'(\b\w+)\s+\1')
p.search('Paris in the the spring').group()
#'the the'

정규식 (\b\w+)\s+\1은 (그룹) + " " + 그룹과 동일한 단어와 매치

2개의 동일한 단어를 연속적으로 사용해야만 매치

이것을 가능하게 해주는 것이 재참조 메타 문자인 \1 

\1은 정규식의 그룹 중 첫 번째 그룹을 가리킴 (두 번째 그룹을 참조하려면 \2를 사용)

 

2) 그루핑된 문자열에 이름 붙이기

정규식 안에 그룹이 무척 많아진다고 가정해 보자. 예를 들어 정규식 안에 그룹이 10개 이상만 되어도 매우 혼란스러울 것이다. 거기에 더해 정규식이 수정되면서 그룹이 추가, 삭제되면 그 그룹을 인덱스로 참조한 프로그램도 모두 변경해 주어야 하는 위험도 갖게 된다.

만약 그룹을 인덱스가 아닌 이름(Named Groups)으로 참조할 수 있다면 어떨까? 그렇다면 이런 문제에서 해방되지 않을까?

이러한 이유로 정규식은 그룹을 만들 때 그룹 이름을 지정할 수 있게 했다. 

(?P<그룹명>...)
p = re.compile(r"(?P<name>\w+)\s+((\d+)[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
print(m.group("name"))
#park

name이라는 그룹 이름으로 참조 가능

p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
p.search('Paris in the the spring').group()
#'the the'

재참조할 때에는 (?P=그룹이름)이라는 확장 구문을 사용해야 함

3. 전방탐색

p = re.compile(".+:")
m = p.search("http://google.com")
print(m.group())
#http:

.+:과 일치하는 문자열로 http:를 반환

만약 http:라는 검색 결과에서 :을 제외하고 출력하려면 어떻게 해야 할까? 위 예는 그나마 간단하지만 훨씬 복잡한 정규식이어서 그루핑은 추가로 할 수 없다는 조건까지 더해진다면 어떻게 해야 할까?

이럴 때 사용할 수 있는 것이 바로 전방 탐색이다.

 

1) 긍정(Positive)형 전방 탐색

((?=...)) - ... 에 해당되는 정규식과 매치되어야 하며 조건이 통과되어도 문자열이 소비되지 않음

p = re.compile(".+(?=:)")
m = p.search("http://google.com")
print(m.group())
# http

정규식 중 :에 해당하는 부분에 긍정형 전방 탐색 기법을 적용하여 (?=:)으로 변경

기존 정규식과 검색에서는 동일한 효과를 발휘하지만 : 에 해당하는 문자열이 정규식 엔진에 의해 소비되지 않아(검색에는 포함되지만 검색 결과에는 제외됨) 검색 결과에서는 :이 제거된 후 돌려줌

 

2) 부정(Negative)형 전방 탐색 

((?!...)) - ...에 해당되는 정규식과 매치되지 않아야 하며 조건이 통과되어도 문자열이 소비되지 않음

 

foo.bar, autoexec.bat, sendmail.cf 같은 형식의 파일들을 와 매치되는 정규식

.*[.].*$

이 정규식은 "파일 이름" + "." + "확장자"를 나타내는 정규식

이 정규식은 foo.bar, autoexec.bat, sendmail.cf 같은 형식의 파일과 매치

 

만약  "bat인 파일은 제외" 이라는 조건을 추가하면 >> 부정형 전방 탐색 사용

.*[.](?!bat$).*$

확장자가 bat가 아닌 경우에만 통과된다는 의미

bat 문자열이 있는지 조사하는 과정에서 문자열이 소비되지 않으므로 bat가 아니라고 판단되면 그 이후 정규식 매치가 진행

exe 역시 제외하라는 조건이 추가되더라도 다음과 같이 간단히 표현할 수 있음

.*[.](?!bat$|exe$).*$

 

4. 문자열 바꾸기

sub 메서드를 사용하면 정규식과 매치되는 부분을 다른 문자로 변경 가능

p = re.compile('(blue|white|red)')
p.sub('color', 'blue socks and red shoes')
# 'color socks and color shoes'

딱 1회만 바꾸고 싶은 경우

p.sub('color', 'blue socks and red shoes', count=1)
# 'color socks and red shoes'

1) sub 메서드와 유사한 subn 메서드

subn 역시 sub와 동일한 기능을 하지만 결과를 튜플로 반환 

돌려준 튜플의 첫 번째 요소는 변경된 문자열이고, 두 번째 요소는 바꾸기가 발생한 횟수

p = re.compile('(blue|white|red)')
p.subn( 'color', 'blue socks and red shoes')
# ('color socks and color shoes', 2)

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<name>\w+)\s+(?P<phone>(\d+)[-]\d+[-]\d+)")
print(p.sub("\g<2> \g<1>", "park 010-1234-1234"))
# 010-1234-1234 park

그룹 이름 대신 참조 번호를 사용해도 결과는 같음

 

3) sub 메서드의 매개변수로 함수 넣기

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 객체가 입력되고 매치되는 문자열은 함수의 반환 값으로 바뀜 

5. Greedy vs Non-Greedy

s = '<html><head><title>Title</title>'
len(s)
#32
print(re.match('<.*>', s).span())
#(0, 32)
print(re.match('<.*>', s).group())
#<html><head><title>Title</title>

정규식에서 greedy 란 * 메타 문자 처럼 매치할 수 있는 최대한의 문자열을 매치해서 모두 소비하는 메타 문자들을 말함 

<.*> 정규식의 매치 결과로 우리는 <html> 문자열만 돌려주기를 원했지만 * 메타 문자는 최대한의 문자열인  <html><head><title>Title</title> 문자열을 모두를 소비

따라서 non-greedy 문자인 ?를 사용하면 *의 탐욕을 제한

 

>>> print(re.match('<.*?>', s).group())
<html>

non-greedy 문자인 ?는 *?, +?, ??, {m,n}?와 같이 사용 가능

가능한 가장 최소한의 반복을 수행하도록 도와주는 역할

반응형