[Python] 정규 표현식 (Regular Expression)
특정한 조건에 부합하는 문자들을 parsing 할 때 사용하는 기법으로, 대부분의 언어가 문법이 동일하다.
1. 정규 표현식 문법
1-1. 문자 클래스
[] 사이의 문자들과 매치를 한다.| Syntax | Description | 
|---|---|
| [a-z] | a부터 z 문자까지 매치 만약 [a-c]의 경우 “before”과 매칭을 하면 b만 매칭됨 | 
| [0-9] | 0부터 9 문자들과 매치 | 
백슬래시(\)를 통해
| Syntax | Description | 
|---|---|
| \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)을 제외한 모든 문자와 매치된다.
| Syntax | Description | 
|---|---|
| a.b | a와 b 문자 사이를 의미하는 모든 문자와 매치 예를 들면 “aab“, “a0b“의 경우 일치 “abc”는 a와 b 사이에 최소 하나의 문자가 없기 때문에 불일치 “abbbbbb”의 경우 앞에서부터 매치되기 때문에 “abb”가 매치 | 
| a..b | a와 b 사이 모든 문자 2개가 일치하는 경우를 비교 예를 들면 “abbbbbb”의 경우 “abbb”와 일치 | 
1-3. 반복 (*)
앞에 특정 문자가 매칭되는 것을 확인한다.
| Syntax | Description | 
|---|---|
| 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번을 인정 하느냐 하지 않느냐의 차이다.
| Syntax | Description | 
|---|---|
| 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}, ?)
| Syntax | Description | 
|---|---|
| a{2}b | {}의 앞 문자인 a가 2번 반복 매칭되는 경우를 모두 찾음 (1번, 3번의 경우는 찾지 않음) 예를 들면 “ab”는 “a”가 1번 반복되었기 때문에 불일치 | 
| a{2,5}b | a가 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. 메타문자
| Syntax | Description | 
|---|---|
| | | 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]+")
| Options | Desciption | 
|---|---|
| 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로 다양한 정보들을 담고 있다.
| Methods | Description | 
|---|---|
| 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
- https://wikidocs.net/4308
