[Study] 정규 표현식 - 1편
정규 표현식 기초
안녕하세요, 이번 포스팅에서는 정규 표현식 기초에 대해 공부한 내용을 정리하려고 합니다.
전반적인 내용은 점프 투 파이썬의 정규표현식을 따르고 있고, 제가 공부하면서 간단히 정리한 정도임을 미리 말씀 드립니다. 해당 출처는 이 곳에 있습니다.
아무래도 정규 표현식에 대한 공부를 하는 포스팅이라 상당히 내용이 많고 길 수 있으니, 목차를 참고하셔서 필요한 부분만 골라서 보시면 좋을 것 같습니다 :)
정규 표현식의 정의
정규 표현식 (Regular Expressions)은 복잡한 문자열을 처리할 때 사용하는 기법으로, 파이썬만의 고유 문법이 아니라 문자열을 처리하는 모든 곳에서 사용합니다. 줄여서 간단히 정규식 이라고도 합니다.
정규 표현식은 왜 필요한가?
정규 표현식은 복잡한 문자열도 아주 간단하게 다룰 수 있기 때문에 잘 사용한다면 큰 효율을 얻을 수 있는데요, 점프 투 파이썬에서는 이와 관련한 예시를 아주 잘 들어서 설명 했습니다.
주민등록번호를 포함하고 있는 텍스트가 있다. 이 텍스트에 포함된 모든 주민등록번호의 뒷자리를 * 문자로 변경해 보자.
우리가 정규식을 모르고 문제를 푼다면, 다음과 같은 순서로 프로그램을 작성해야 합니다.
- 전체 텍스트를 공백 문자로 나눈다 (split)
- 나뉜 단어가 주민등록번호 형식인지 조사한다.
- 단어가 주민등록번호 형식이라면 뒷자리를 *로 변환한다.
- 나뉜 단어를 다시 조립한다.
이를 구현하면 아래와 같이 할 수 있겠죠.
data = """
park 800905-1049118
kim 700905-1059119
"""
print('[변환이전]')
print(data)
result = []
for line in data.split("\n"):
word_result = []
for word in line.split(" "):
if len(word) == 14 and word[:6].isdigit() and word[7:].isdigit():
word = word[:6] + "-" + "*******"
word_result.append(word)
result.append(" ".join(word_result))
print('[변환 이후]')
print("\n".join(result))
[변환이전]
park 800905-1049118
kim 700905-1059119
[변환 이후]
park 800905-*******
kim 700905-*******
반면에, 정규식을 사용한다면 훨씬 더 간편하고 직관적으로 코드를 작성할 수 있습니다!
아직 정규식을 배우지 않았으니 눈으로만 살펴봅시다.
import re
data = """
park 800905-1049118
kim 700905-1059119
"""
pat = re.compile("(\d{6})[-]\d{7}")
print(pat.sub("\g<1>-*******", data))
park 800905-*******
kim 700905-*******
단 몇줄만으로도 깔끔하게 문자열을 변환할 수 있는 것을 알 수 있습니다. 참 신기하죠?
그럼 지금부터 정규 표현식에 대해 본격적으로 배워 보겠습니다 !
정규 표현식의 기초, 메타 문자
정규 표현식에서는 메타 문자(meta characters) 라는 것을 사용하는데요, 메타 문자란 원래 그 문자가 가진 뜻이 아닌 특별한 용도로 사용하는 문자를 말합니다. 메타문자는 아래와 같은 친구들이 있습니다.
. ^ $ * + ? { } [ ] \ | ( )
정규 표현식에서 이 메타 문자들은 특별한 의미를 갖게 됩니다.
가장 간단한 정규 표현식부터 시작해서 각 메타 문자의 의미와 사용 방법을 알아 봅시다.
문자 클래스 [ ]
먼저, 문자 클래스 Character class)에 대해 알아 봅시다. 문자클래스로 만들어진 정규식은 "[ ] 사이의 문자들과 매치"
라는 의미를 가집니다.
즉, 정규 표현식이 [abc]라면 이 표현식의 의미는 “a, b, c 중 한 개의 문자와 매치” 라는 뜻입니다. 이해를 돕기 위해 각 문자열 “a”, “before”, “dude”가 정규식 [abc]와 어떻게 매치되는지 살펴 봅시다.
- “a”는 정규식과 일치하는 문자인 “a”가 있으므로 매치
- “before”는 정규식과 일치하는 문자인 “b”가 있으므로 매치
- “dude”는 정규식과 일치하는 문자인 a,b,c 중 어느 하나도 포함하지 않으므로 매치되지 않음.
[ ] 안의 두 문자 사이에 하이픈(-)을 사용하면 두 문자 사이의 범위 (From-To)를 의미합니다. 예를 들어 [a-c]라는 정규 표현식은 [abc]와 동일하고 [0-5]는 [012345]와 동일합니다.
다음은 하이픈(-)을 사용한 문자 클래스의 사용 예시입니다.
- [a-zA-Z] : 알파벳 모두
- [0-9] : 숫자
문자 클래스 안에는 어떤 문자나 메타문자도 사용할 수 있지만, 한 가지 주의해야할 메타 문자가 있습니다. ^
는 문자 클래스 안에서 사용될 때 반대(not)라는 의미를 갖습니다. 예를들어 [^0-9]
라는 정규 표현식은 숫자가 아닌 문자만 매치됩니다..
[자주 사용하는 문자 클래스]
\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_]와 동일한 표현식.
*Whitespace란?
- \b : 뒤로 한 칸 이동 (Backspace) (아스키코드 8)
- \t : 수평탭 간격 띄우기 (아스키코드 9)
- \n : 줄바꿈 (Linefeed) (아스키코드 10)
- \v : 수직탭 간격 띄우기 (아스키코드 11)
- \f : 프린트 출력 용지를 한 페이지 넘김 (Form feed) (아스키코드 12)
- \r : 동일한 줄의 맨 앞으로 커서 이동 (Carriage Return) (아스키코드 13)
대문자로 사용된 것은 소문자의 반대임을 알 수 있습니다.
Dot(.)
정규 표현식의 Dot(.) 메타 문자는 줄바꿈 문자인 \n을 제외한 모든 문자와 매치됨을 의미합니다.
다음 정규식을 봅시다.
a.b
위 정규식의 의미는,
a + 모든 문자 + b"
즉, a와 b라는 문자 사이에 어떤 문자가 들어가도 모두 매치된다는 것을 의미합니다.
이해를 돕기 위해 문자열 “aab”, “a0b”, “abc”가 정규식 a.b
와 어떻게 매치되는지 살펴 봅시다.
- “aab” : 가운데 문자 “a”가 모든 문자를 의미하는
.
과 일치하므로 매치 - “a0b” : 가운데 문자 “0가 모든 문자를 의미하는
.
과 일치하므로 매치 - “abc” : “a”문자와 “b” 문자 사이에 아무 문자도 없으므로 매치되지 않음
다른 정규식을 봅시다.
a[.]b
이 정규식의 의미는 다음과 같습니다.
"a + Dot(.)문자 + b"
따라서 정규식 a[.]b
는 “a.b”문자열과 매치되고, “a0b” 문자열과는 매치되지 않습니다. 즉, []
안에 Dot(.)문자가 들어가면 “모든 문자”가 아닌 문자.
그 자체를 의미합니다.
반복 (*)
정규 표현식의 * 메타 문자는 * 바로 앞에 있는 문자가 0부터 무한대로 반복될 수 있다는 의미입니다.
예를 들어, ca*t
는 ct
, cat
, caaaat
모두 가능합니다.
반복 (+)
정규 표현식의 + 메타문자는 *와 거의 비슷하지만, 바로 앞의 문자가 최소 1번 이상 반복되는 경우를 뜻합니다.
즉, ca+t
에서는 ct
가 유효하지 않습니다.
반복 ({m,n}, ?)
그렇다면, 이 반복의 횟수를 3회만, 혹은 1회부터 3회까지만으로 제한하고 싶을때는 어떻게 하면 될까요?
{ }
메타 문자를 사용하여 {m, n}
으로 사용하면 반복 횟수를 m부터 n회까지 매치할 수 있습니다.
또한, m과 n을 생략하여 {m,}
은 m회 이상, {,n}
은 n회 이하의 반복으로 사용할 수도 있습니다.
마지막으로, {m}
으로 표기하면 정확히 m회만 반복되는 것을 의미합니다.
?
메타 문자는 반복은 아니지만 {0,1}
과 동일한 의미를 가지고 있습니다.
아래 정규식을 봅시다.
ab?c
위 정규식의 의미는 아래와 같습니다.
"a + b(있어도 되고 없어도 됨) + c"
즉, ab?c
는 ac
도 되고 abc
도 됩니다.
여기까지가 아주 기초적인 정규 표현식입니다. 알아야 할 것이 아직 많이 남았지만, 파이썬으로 정규 표현식을 어떻게 사용하는지 알아보도록 합시다 !
Python에서 정규 표현식을 지원하는 re 모듈
파이썬은 정규 표현식을 지원하기 위해 re(regular expression의 약어) 모듈을 제공합니다. re 모듈은 파이썬을 설치할 때 자동으로 설치되는 기본 라이브러리로, 사용 방법은 아래와 같습니다.
import re
p = re.compile('ab*')
re.compile을 이용하여 정규 표현식을 컴파일 합니다. re.compile의 결과로 돌려주는 객체 p(컴파일된 패턴 객체)를 사용하여 그 이후의 작업을 수행할 수 있습니다.
- 정규식을 컴파일할 때 특정 옵션을 주는 것도 가능한데, 이에 대해서는 뒤에서 자세히 살펴보겠습니다.
- 패턴이란 정규식을 컴파일한 결과를 뜻합니다.
정규식을 이용한 문자열 검색
패턴 객체를 이용하여 문자열 검색을 수행할 수 있습니다. 컴파일된 패턴 객체는 다음과 같은 4가지 메서드를 제공합니다.
Method | 목적 |
---|---|
match() | 문자열의 처음부터 정규식과 매치되는지 조사 |
search() | 문자열 전체를 검색하여 정규식과 매치되는지 조사 |
findall() | 정규식과 매치되는 모든 문자열(substring)을 리스트로 반환 |
finditer() | 정규식과 매치되는 모든 문자열(substring)을 반복 가능한 객체로 돌려줌 |
match, search는 정규식과 매치될때는 match 객체를 반환하고, 매치되지 않을때는 None을 반환합니다. 이들 메서드에 대한 간단한 예시를 살펴 봅시다.
match()
match 메서드는 문자열의 처음부터 정규식과 매치되는지 조사합니다. 위 패턴에 match 메서드를 수행해 봅시다.
import re
p = re.compile('[a-z]+') #알파벳으로만 이루어진 문자열
m = p.match("python") #조건에 부합
m2 = p.match("3 python") #조건에 부합하지 않음 (숫자 포함)
print(m)
print(m2)
<re.Match object; span=(0, 6), match='python'>
None
이렇게 조건에 따라 반환이 되는 것을 확인할 수 있습니다.
match의 결과에 따라 문자열을 다루려면 보통 다음과 같은 흐름으로 코드를 작성합니다.
p = re.compile('[a-z]+')
m = p.match("python")
if m:
print('Match found: ', m.group())
else:
print('No match')
Match found: python
search()
이번에는 search 메서드를 수행해 봅시다.
m = p.search("python")
m2 = p.search("3 python")
print(m)
print(m2)
<re.Match object; span=(0, 6), match='python'>
<re.Match object; span=(2, 8), match='python'>
search 메서드는 문자열의 처음부터 검색하는 것이 아니라 전체 문자열을 검색하기 때문에, 3 이후의 “python” 문자열과 매치되는 것을 알 수 있습니다.
findall()
findall 메서드는 문자열에서 각 단어를 정규식과 매치해서 리스트로 돌려줍니다.
result = p.findall("life is too short")
print(result)
['life', 'is', 'too', 'short']
finditer
finditer 메서드는 findall과 동일하지만 그 결과로 반복 가능한 객체(iterator object)를 반환합니다. 반복 가능한 객체가 포함하는 각각의 요소는 match 객체입니다.
result = p.finditer("life is too short")
print(result)
for r in result: print(r)
<callable_iterator object at 0x0000021FF9A6A190>
<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'>
match 객체의 메서드
지금까지 각 함수들의 역할은 잘 이해했지만, 마지막에 반환되는 객체에 대해서는 궁금증이 남아 있을 수 있습니다.
- 어떤 문자열이 매치되었는지?
- 매치된 문자열의 인덱스는 어디부터 어디까지인지?
match 객체의 메서드를 사용하면 이 궁금증들을 해결할 수 있습니다.
method | 목적 |
---|---|
group() | 매치된 문자열을 돌려준다 |
start() | 매치된 문자열의 시작 위치를 돌려준다 |
end() | 매치된 문자열의 끝 위치를 돌려준다 |
span() | 매치된 문자열의 (시작,끝)에 해당하는 튜플을 돌려준다 |
다음 예로 확인해 봅시다.
m = p.match("python")
print(m.group())
print(m.start())
print(m.end())
print(m.span())
python
0
6
(0, 6)
모듈 단위로 수행하기
지금까지 우리는 re.compile
을 사용하여 컴파일된 패턴 객체로 그 이후의 작업을 수행했지만, re 모듈은 이것을 더 축약한 형태로 사용할 수 있는 방법을 제공한다. 다음 예를 보자.
p = re.compile('[a-z]+')
m = p.match("python")
이것을 축약해서 나타내면 아래와 같다.
m = re.match('[a-z]+', "python")
컴파일 옵션
정규식을 컴파일할 때 아래와 같은 옵션들을 사용할 수 있습니다.
- DOTALL(S) :
.
이 줄바꿈 문자를 포함하여 모든 문자와 매치할 수 있도록 한다. - IGNORECASE(I) : 대소문자에 관계없이 매치할 수 있도록 한다.
- MULTILINE(M) : 여러줄과 매치할 수 있도록 한다. (
^
,$
메타문자의 사용과 관계가 있는 옵션이다) - VERBOSE(X) : verbose 모드를 사용할 수 있도록 한다. (정규식을 보기 편하게 만들 수 있고 주석 등을 사용할 수 있게 된다)
옵션을 사용할 때는 re.DOTALL
처럼 전체 옵션 이름을 써도 되고, re.S
처럼 약어를 써도 된다.
VERBOSE, X
이 중, 특히 verbose 기능은 예시가 꼭 필요할 것 같아서 적어 보겠습니다.
아래와 같은 복잡한 정규식이 있습니다.
charref = re.compile(r'%[#](0[0-7]+|[0-9]+|x[0-9a-fA-F]+);')
쉽게 이해되지 않는 정규식인데요, 이해를 돕기 위해 verbose 기능을 이용해 주석을 달아 정리해보겠습니다.
charref = re.compile(r"""
&[#] # Start of numeric entity reference
(
0[0-7]+ # Octal form
| [0-9]+ # Decimal form
I x[0-9a-fA-F]+ # Hexadacimal form
)
; # Trailing semicolon
""", re.VERBOSE)
백슬래쉬
정규식에서 백슬래쉬는 기능적인 부분과 중복이 될 수 있습니다. 예를 들어, \section
이라는 문자열을 찾고 싶어서 정규식에 입력을 하더라도, 이미 \s
라는 기능은 white space를 의미합니다. 백슬래쉬를 그 자체를 표현하기 위해서는 \\section
와 같이 백슬래쉬를 두 번 입력해주면 됩니다.
또 다른 방법으로 row string의 약자인 r
문자를 제일 앞에 붙여도 됩니다. r'\section'
자, 이렇게 해서 정규 표현식의 기본적인 내용들을 다루어 봤습니다. 한번에 모든 내용을 다 하려고 했는데, 양이 너무 많아져서, 나머지 부분은 다음 포스팅에서 이어서 정리하도록 하겠습니다.
그럼, 다음 포스팅에서 뵈어요 :)
Comments