Programming,  Python

[Python] 정규 표현식 (Regular Expression)

특정한 조건에 부합하는 문자들을 parsing 할 때 사용하는 기법으로, 대부분의 언어가 문법이 동일하다.

1. 정규 표현식 문법

1-1. 문자 클래스

[] 사이의 문자들과 매치를 한다.

SyntaxDescription
[a-z]a부터 z 문자까지 매치
만약 [a-c]의 경우 “before”과 매칭을 하면 b만 매칭됨
[0-9]0부터 9 문자들과 매치

백슬래시(\)를 통해

SyntaxDescription
\d숫자와 매치, [0-9]와 동일한 표현
\D숫자가 아닌것과 매치, [^0-9]와 동일한 표현식
\s공백 문자, [ \t\n\r\f\v]와 동일한 표현식
\S공백문자가 아닌 것과 매치, [^ \t\n\r\f\v]와 동일한 표현식
\w문자+숫자와 매치, [a-zA-Z0-9_]와 동일한 표현식
\W문자+숫자가 아닌 문자와 매치, [^a-zA-Z0-9_]와 동일한 표현식
\\백슬래시 단어를 쓰기 위해 \\가 필요한데, python에선 \\\로 치환되기 때문에 r"\\ ......"와 같이 r을 붙여서 raw string로 인지시켜준다.
# \\ example
p = re.compile(r"\\section")
string = \
"""
\section
"""
result = p.findall(string)
print(result) # ['\\section']

1-2. Dot

줄바꿈(\n)을 제외한 모든 문자와 매치된다.

SyntaxDescription
a.ba와 b 문자 사이를 의미하는 모든 문자와 매치
예를 들면 “aab“, “a0b“의 경우 일치
“abc”는 a와 b 사이에 최소 하나의 문자가 없기 때문에 불일치
abbbbbb”의 경우 앞에서부터 매치되기 때문에 “abb”가 매치
a..ba와 b 사이 모든 문자 2개가 일치하는 경우를 비교
예를 들면 “abbbbbb”의 경우 “abbb”와 일치

1-3. 반복 (*)

앞에 특정 문자가 매칭되는 것을 확인한다.

SyntaxDescription
ca*t*의 앞 문자인 a가 0번 이상 반복 매칭되는 경우를 모두 찾음
예를 들면 “ct”는 “a”가 0번 반복되어 매치 되었기 때문에 일치한다.
p = re.compile('ab*c')
print(p.match("ac"))    # Match
print(p.match("abc"))   # Match
print(p.match("abdc"))  # None

1-4. 반복 (+)

*와의 차이점은 0번을 인정 하느냐 하지 않느냐의 차이다.

SyntaxDescription
ca+t+의 앞 문자인 a가 1번 이상 반복 매칭되는 경우를 모두 찾음
예를 들면 “ct”는 “a”가 1번 반복되어 매치가 안되었기 때문에 불일치
[a-z]+a부터 z 문자가 1회 이상 나오는 경우를 찾음
p = re.compile('[a-z]+')
print(p.match(""))      # None
print(p.match("aaa"))   # Match

1-5. 반복 ({m,n}, ?)

SyntaxDescription
a{2}b{}의 앞 문자인 a가 2번 반복 매칭되는 경우를 모두 찾음 (1번, 3번의 경우는 찾지 않음)
예를 들면 “ab”는 “a”가 1번 반복되었기 때문에 불일치
a{2,5}ba가 2 ~ 5번 반복되며 뒤에 문자가 b가 나오면 일치
예를 들면 “aaaaab”의 경우 일치
ab?c?는 {0,1}과 같은 표현으로, b가 0번 또는 1번 나올 경우 매치
p = re.compile('a{2}b')
print(p.match("ab"))    # None
print(p.match("aab"))   # Match
print(p.match("aaabc")) # None

p = re.compile('a{2,5}b')
print(p.match("aaaaab"))    # Match

p = re.compile('ab?c')
print(p.match("abc"))   # Match
print(p.match("ac"))    # Match
print(p.match("abbc"))  # None

1-6. 메타문자

SyntaxDescription
|Or의 의미로 예를 들면 “apple|banana”는 apple 또는 banana에 대해서 찾음
^문자열의 맨 처음을 나타냄
$문자열의 맨 끝을 나타냄
\b공백을 나타내며, 역슬래시를 쓸 경우 r
import re
result = re.search("apple|banana", "apple is yammy")
print(result)
result = re.search("apple|banana", "this is banana")
print(result)

1-7. Grouping ()

만약 ABC가 반복되는 것을 찾고 싶을 때는 grouping을 통해 다음과 같이 작성하면 된다.

import re
p = re.compile("(ABC)+")
string = "ABCABCABC Good"
result = p.search(string)
print(result)
<_sre.SRE_Match object; span=(0, 9), match='ABCABCABC'>

Grouping한 결과를 다시 reuse 할 수 있다.

import re
p = re.compile(r"(\b\w+)\s+\1")
# p = re.compile(r"(?P<g1>\b\w+)\s+(?P=g1)")
string = "Korea is in the the winter"
result = p.search(string)
print(result)
<_sre.SRE_Match object; span=(12, 19), match='the the'>

위 코드는 앞에 공백과 단어, 그리고 space가 있고 동일한 단어 (\1)가 뒤에 동일하게 나타나는 경우를 찾는다. \1, \2는 group1, group2를 의미한다.

그리고 추가로 각 group에 대해 naming을 지정할 수 있다.
?P<Name>

import re
p = re.compile(r"(?P<g1>\w+)\s+((\d+)[-]\d+[-]\d+)")
string = "Shumin 010-1234-5678"
result = p.search(string)
print(result)
result = p.search(string).group("g1")
print(result)
<_sre.SRE_Match object; span=(0, 20), match='Shumin 010-1234-5678'>
 Shumin

1-8. 전방탐색

만약 검색할 조건과 출력하는 문자열이 다른 경우 다음과 같이 사용할 수 있다.
긍정형: (?=)
부정형: (?!)

import re
#p = re.compile(".+:")
p = re.compile(".+(?=:)")
string = "http://google.com"
result = p.search(string)
print(result)
http

위 코드는 .+을 통해 개행문자를 제외한 1개 이상 모든 문자에 대해서 :가 나올 때 까지 찾는데 출력할 경우엔 제외할 수 있다.

import re
p = re.compile(".*[.](?!txt$).*$", re.M)
string = \
"""
test.exe
test.out
test.txt
"""
result = p.findall(string)
print(result)
['test.exe', 'test.out']

위 코드는 .을 기준으로 좌우에 문자열을 찾아내는 프로그램으로 만약 txt로 끝나는 문자열은 제외하는 프로그램이다.

1-9. Greedy vs. lazy

만약 반복되는 단어로 인해 내가 찾고자 하는 범위보다 넓은 범위에서 검색을 하는 경우도 있다. 이 때 ?를 통해서 해결 가능하다.

import re
p = re.compile("<.*>", re.M)
# p = re.compile("<.*?>", re.M)
string = "<html><head><title>title</title>"
result = p.match(string)
print(result)
<_sre.SRE_Match object; span=(0, 32), match='title'>

만약 첫 단어인 <html>만 찾고자 할 경우 ?를 추가해주면 .*?로 하면 원하는 결과를 얻을 수 있다.

2. 사용법

기본적으로 python에선 re module을 통해 정규표현식 기능을 제공한다.

2-1. compile

다음과 같이 정규표현식 규칙을 지정해준다.

import re
p = re.compile("[a-z]+")
OptionsDesciption
DOTALL (S)예를 들면 “a.b”의 pattern은 줄바꿈 문자를 제외한 모든 문자를 비교할 수 있지만, DOTALL option을 사용할 경우 줄바꿈 문자도 포함해서 찾을 수 있음
IGNORECASE (I)대소문자를 모두 포함해서 검색
MULTILINE (M)여러 줄로 문자열을 입력 할 때 각 라인별로 새롭게 적용할 때 사용
VERBOSE (X)정규표현식을 한 줄로 작성하게 되면 가독성이 떨어지기 때문에, 줄바꿈을 하더라도 정상적으로 동작할 수 있도록 하는 옵션
import re                                                                                                                                                                  

p = re.compile("a.b", re.DOTALL)
string = "a\nb"
result = p.match(string)
print(result)

위 결과를 보면 개행문자가 있음에도 매치가 되는 것을 확인 할 수 있다.

import re

p = re.compile("^python\s\w+")
string = \
"""python is language
programming is hard
python is better than C
life is good
"""
result = p.match(string)
print(result)
['python is']

위 결과를 보면 오직 처음 문자인 python is만 매치가 된다, 즉 multiple line에 대해선 인지하지 못하였기 때문에, MULTILINE option re.compile("^python\s\w+", re.M)을 주면 결과가 달라진다.

['python is', 'python is']
#p = re.compile("^python\s\w+")
pattern = \
"""
^python     # Start word is python 
\s          # Space
\w          # Word
+           # At least 1 word
"""
p = re.compile(pattern, re.VERBOSE|re.MULTILINE)
string = \
"""python is language
programming is hard
python is better than C
life is good
"""
result = p.findall(string)
print(result)

위와 같은 예시처럼 줄별로 comment를 쓰면서 조건을 작성할 수 있어서 유용한 옵션이다.

2-2. match

위 처럼 지정한 pattern을 바탕으로 찾고자 하는 문자열을 입력해준다. 참고로 문자열 가장 처음부터 바로 pattern matching을 진행하는게 search와의 차이점이다.

Return type은 match object로 다양한 정보들을 담고 있다.

MethodsDescription
group()매치된 문자열을 리턴
start()매치된 문자열의 시작 위치를 리턴
end()매치된 문자열의 끝 위치를 리턴
span()매치된 문자열의 (시작, 끝)에 해당하는 tuple을 리턴
p = re.compile("[A-z]+")
string = "HelloWorld"
result = p.match(string)
print(result)           # Match
<_sre.SRE_Match object; span=(0, 10), match='HelloWorld'>

참고로 pattern에서 a와 A는 서로 다른 문자로, ASCII code값을 기준으로 결정되기 때문에 모든 영문자를 찾고 싶으면 [A-z]로 해야한다.

2-3. search

위 예제 코드에서 만약 input string이 ” Hello “와 같이 띄어쓰기가 포함될 경우 앞 부분인 ” ” 공백을 읽고 None으로 매치가 안될 것이다. 이 경우엔 search를 통해 전체 문자열에서 찾도록 할 수 있다.

p = re.compile("[A-z]+")
string = " Hello "
result = p.match(string)
print(result)           # None

result = p.search(string)
print(result)           # Match

2-4. findall

search보다 좀 더 관대한 방식의 함수로, 모든 문자열에 대해서 pattern을 찾게 되는데 기존 search의 경우 부합하는 경우를 찾게 되면 더 이상 찾지 않지만, findall의 경우는 찾고 난 뒤에도 모든 문자열을 계속 찾는다.
참고로 return은 list 형식으로 출력시킨다.

2-5. finditer

findall과 동일한 동작을 하지만 return type이 다른데, finditer의 경우 iterable object를 return한다.

p = re.compile("[A-z]+")
string = "This is Hello World"
result = p.finditer(string)
for i in result:
    print(i)           # Match
$ python test.py 
 <_sre.SRE_Match object; span=(0, 4), match='This'>
 <_sre.SRE_Match object; span=(5, 7), match='is'>
 <_sre.SRE_Match object; span=(8, 13), match='Hello'>
 <_sre.SRE_Match object; span=(14, 19), match='World'>

Reference

  1. https://wikidocs.net/4308

Leave a Reply

Your email address will not be published. Required fields are marked *