[Study] 정규 표현식 - 1편

정규 표현식 기초

안녕하세요, 이번 포스팅에서는 정규 표현식 기초에 대해 공부한 내용을 정리하려고 합니다.
전반적인 내용은 점프 투 파이썬의 정규표현식을 따르고 있고, 제가 공부하면서 간단히 정리한 정도임을 미리 말씀 드립니다. 해당 출처는 이 곳에 있습니다. 아무래도 정규 표현식에 대한 공부를 하는 포스팅이라 상당히 내용이 많고 길 수 있으니, 목차를 참고하셔서 필요한 부분만 골라서 보시면 좋을 것 같습니다 :)

정규 표현식의 정의

정규 표현식 (Regular Expressions)은 복잡한 문자열을 처리할 때 사용하는 기법으로, 파이썬만의 고유 문법이 아니라 문자열을 처리하는 모든 곳에서 사용합니다. 줄여서 간단히 정규식 이라고도 합니다.

정규 표현식은 왜 필요한가?

정규 표현식은 복잡한 문자열도 아주 간단하게 다룰 수 있기 때문에 잘 사용한다면 큰 효율을 얻을 수 있는데요, 점프 투 파이썬에서는 이와 관련한 예시를 아주 잘 들어서 설명 했습니다.

주민등록번호를 포함하고 있는 텍스트가 있다. 이 텍스트에 포함된 모든 주민등록번호의 뒷자리를 * 문자로 변경해 보자.

우리가 정규식을 모르고 문제를 푼다면, 다음과 같은 순서로 프로그램을 작성해야 합니다.

  1. 전체 텍스트를 공백 문자로 나눈다 (split)
  2. 나뉜 단어가 주민등록번호 형식인지 조사한다.
  3. 단어가 주민등록번호 형식이라면 뒷자리를 *로 변환한다.
  4. 나뉜 단어를 다시 조립한다.

이를 구현하면 아래와 같이 할 수 있겠죠.

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*tct, 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?cac도 되고 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 메서드를 수행해 봅시다.

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