정규식은 모든 프로그래밍에서 어려운 영역 중에 하나로 손 꼽히는데요, 문자열 관련 문제를 풀다가 정규 표현식을 이용하여 깔끔하게 푼 문제가 있어 이번 기회에 정리해볼까 합니다.
간략하게 포스팅 내용을 정리해보자면 다음과 같습니다.
. ^ $ * + ? { } [ ] \ | ( )
정규 표현식(regular expressions) 은 복잡한 문자열을 처리할 때 사용하는 기법으로, 파이썬만의 고유 문법이 아니라 문자열을 처리하는 모든 곳에서 사용하는 일종의 형식 언어이다.
정규 표현식은 우리가 흔히 알고 있는 정규식으로도 불립니다. 정규식은 파이썬 뿐만 아니라 많은 영역에서 사용되는데, 주된 목적은 정규표현식의 패턴을 이용하여 해당하는 문자열을 골라내고 치환하는 역할을 합니다.
메타 문자는 원래 그 문자가 가진 뜻이 아니라 특별한 의미를 가진 문자를 의미합니다. 정규 표현식에서 다음과 같은 메타 문자를 사용하면 특별한 의미를 갖게 됩니다.
. ^ $ * + ? { } [ ] \ | ( )
문자 클래스로 만들어진 정규식은 '['와 ']' 사이의 문자들의 매치라는 의미를 가집니다. 예를 들어, 정규 표현식이 [abc]
라면, 이 표현식의 의미는 'a, b, c 중 한 개의 문자와 매치'를 뜻합니다.
[] 안의 두 문자 사이에 하이픈(-)을 사용하면 두 문자 사이의 범위를 의미합니다.
[a-zA-Z]
: 모든 알파벳[0-9]
: 모든 숫자문자 클래스 안에 어떤 문자나 메타 문자도 사용할 수 있지만, 주의해야할 문자가 1가지 있습니다. 그것은 바로 ^인데, 문자 클래스 안에 ^ 메타문자를 사용할 경우에는 반대(not)라는 의미를 갖습니다.
예를 들어, [^0-9]
라는 정규 표현식은 숫자가 아닌 문자만 매치됩니다.
[0-9]
또는[a-zA-Z]
등은 무척 자주 사용하는 정규 표현식이다. 이렇게 자주 사용하는 정규식은 별도의 표기법으로 표현할 수 있다. 다음을 기억해 두자.
\d
- 숫자와 매치된다.[0-9]
와 동일한 표현식이다.\D
- 숫자가 아닌 것과 매치된다.[^0-9]
와 동일한 표현식이다.\s
- 화이트스페이스(whitespace) 문자와 매치된다.[\t\n\r\f\v]
와 동일한 표현식이다. 맨 앞의 빈칸은 공백 문자(space)를 의미한다.\S
- 화이트스페이스 문자가 아닌 것과 매치된다.[^ \t\n\r\f\v]
와 동일한 표현식이다.\w
- 문자+숫자(alphanumeric)와 매치된다.[a-zA-Z0-9_]
와 동일한 표현식이다.\W
- 문자+숫자(alphanumeric)가 아닌 문자와 매치된다.[^a-zA-Z0-9_]
와 동일한 표현식이다.
특정 문자열이 계속해서 반복되는지 조사하는 정규식을 작성하고 싶을 때 사용할 수 있는 것이 바로 그룹핑(grouping) 입니다.
(ABC)+
그룹을 만들어 주는 메타 문자가 바로 ()
입니다.
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("(\w+)\s+(\d+[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
print(m.group(0))
# 출력: park 010-1234-1234
print(m.group(1))
# 출력: park
print(m.group(2))
# 출력: 010-1234-1234
아래 예시 처럼 그룹에 이름을 지정해 줄 수도 있습니다.
(?P<그룹명>...)
(\w+) → (?P<name>\w+)
p = re.compile(r"(?P<name>\w+)\s+(?P<phone>\d+[-]\d+[-]\d+)")
m = p.search("park 010-1234-1234")
print(m.group("name"))
print(m.group("phone"))
\n
을 제외한 모든 문자.(dot)
메타 문자는 줄바꿈 문자인 \n
을 제외한 모든 문자와 매치됩니다.
정규식을 작성할 때 re.DOTALL 옵션을 주면
.(dot)
문자와\n
문자도 매치된다.
import re
p = re.compile("a.b")
해석은 "a + 모든 문자 + b"입니다. 즉, a와 b 사이에 어떤 문자가 들어가도 모두 매치된다는 의미입니다.
import re
p = re.compile("ca*t")
*
바로 앞에 있는 문자 a가 0부터 무한대까지 반복될 수 있다는 의미입니다.
ct
- Yescat
- Yescaat
- Yes반복을 나타내는 또 다른 메타 문자로 +
가 있는데, 최소 1번 이상 반복될 때 사용합니다.
import re
p = re.compile("ca+t")
ct
- Nocat
- Yescaat
- Yes{}
메타 문자를 사용하면 반복 횟수를 지정할 수 있습니다.
import re
p = re.compile("ca{2}t")
이 정규 표현식의 의미는 'c + a를 반드시 2번 반복 + t' 으로 caat가 매치됩니다.
범위를 지정하고 싶을 때는 아래와 같이 사용할 수 있습니다.
import re
# 2-5회 반복
p = re.compile("ca{2, 5}t")
# 3회 이하 반복
p = re.compile("ca{,3}t")
# 3회 이상 반복
p = re.compile("ca{3,}t")
반복은 아니지만 그와 유사한 기능을 하는 ?
메타 문자도 있습니다. ?
메타 문자가 의미하는 것은 {0, 1}
입니다.
import re
p = re.compile("ab?c")
a와 c 사이에 b가 있어도 되고 없어도 되는 경우입니다.
|
메타 문자는 or과 동일한 의미로 사용됩니다. A|B
라는 정규식이 있다면 A또는 B라는 의미가 됩니다.
import re
p = re.compile('Crow|Servo')
m = p.match('CrowHello')
print(m)
# 결과
<re.Match object; span=(0, 4), match='Crow'>
^
메타 문자는 문자열로 시작하는 것을 의미합니다. str.startsWith 메소드
와 유사합니다.
>>> re.search('^Life', 'Life is too short')
<re.Match object; span=(0, 4), match='Life'>
>>> re.search('^Life', 'My Life')
None
$
메타 문자는 ^
메타 문자와 반대로 문자열로 끝나는 것을 의미합니다. str.endsWith 메소드
와 유사합니다.
>>> re.search('short$', 'Life is too short')
<re.Match object; span=(12, 17), match='short'>
>>> re.search('short$', 'Life is too short, you need python')
None
파이썬에서는 정규 표현식을 지원하기 위해 re(regular expression) 모듈을 제공합니다. re 모듈은 파이썬을 설치할 때 자동으로 설치되는 표준 라이브러리입니다.
다음과 같이 사용할 수 있습니다.
import re
p = re.compile('[a-z]+')
m = p.match('python')
if m:
print('Match found: ', m.group())
else:
print('No match')
re.compile
메소드를 통해 컴파일된 패턴 객체를 사용하여 문자열을 검색할 수 있습니다. 패턴 객체는 다음과 같은 4가지 메소드를 제공합니다.
match() 메소드는 문자열의 처음부터 정규식과 매치되는지 조사합니다.
p = re.compile("[a-z]+")
m = p.match("python")
# 출력: <re.Match object; span=(0, 6), match='python'>
m = p.match("3 python")
# 출력: None
python
문자열은 [a-z]+
정규식에 부합하므로 match 객체가 리턴되지만 3 python
문자열은 처음에 나오는 문자 3이 정규식 [a-z]+
에 부합하지 않으므로 None이 리턴됩니다.
search() 메소드는 match() 메소드와 동일하지만 전체 문자열을 검색합니다.
p = re.compile("[a-z]+")
m = p.search("python")
# 출력: <re.Match object; span=(0, 6), match='python'>
m = p.search("3 python")
# 출력: <re.Match object; span=(2, 8), match='python'>
이렇듯 match 메서드와 search 메서드는 문자열의 처음부터 검색할지의 여부에 따라 다르게 사용해야 합니다.
findall은 패턴과 매치되는 모든 값을 찾아 리스트로 리턴합니다.
p = re.complie('[a-z]+')
result = p.findall('life is too short')
# 출력: ['life', 'is', 'too', 'short']
finditer 메소드는 findall과 동일하지만, 그 결과를 반복 가능한 객체로 리턴합니다. 반복 가능한 객체는 match 객체입니다.
>>> 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'>
match 객체란 앞서 살펴본 p.match
, p.search
, p.finditer
메소드에 의해 리턴된 매치 객체를 의미합니다.
match 객체에서 사용할 수 있는 메소드는 다음과 같습니다.
import re
p = re.compile('[a-z]+')
m = p.match('python')
>>> print(m)
<re.Match object; span=(0, 6), match='python'>
>>> print(m.group())
'python'
>>> print(m.start())
0
>>> print(m.end())
6
>>> print(m.span())
(0, 6)
정규식을 컴파일할 때 다음 옵션을 사용할 수 있습니다.
.
(dot)이 줄바꿈 문자를 포함해 모든 문자와 매치될 수 있게 한다^
, $
메타 문자와 관련 있는 옵션옵션을 사용할 때는 re.DOTALL
처럼 전체 옵션 이름을 써도 되고 re.S
로 사용해도 됩니다.
.
메타 문자는 줄바꿈 문자(\n
)을 제외한 모든 문자와 매치되는 규칙이 있습니다. 하지만 re.DOTALL
또는 re.S
옵션을 사용해 정규식을 컴파일하면 모두 매치할 수 있습니다.
import re
p = re.comile('a.b', re.DOTALL)
m = p.match('a\nb')
>>> print(m)
<re.Match object; span=(0, 3), match='a\nb'>
re.IGNORECASE
또는 re.I
옵션은 대소문자 구별 없이 매치를 수행할 때 사용하는 옵션입니다.
import re
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'>
re.MULTILINE
또는 re.M
옵션은 문자열 전체의 처음이 아니라 각 라인의 처음으로 인식시킬 때 사용합니다.
import re
data = """python one
life is too short
python two
you need python
python three"""
>>> p = re.compile("^python\s\w+")
>>> p.findall(data)
['python one']
>>> p = re.compile("^python\s\w+", re.MULTILINE)
>>> p.findall(data)
['python one', 'python two', 'python three']
즉, re.MULTILINE
옵션은 ^, $ 메타 문자를 문자열의 각 줄마다 적용해 주는 것입니다.
이해하기 어려운 정규식을 주석 또는 줄 단위로 구분할 수 있다면 얼마나 보기 좋고 이해하기 쉬울까요?
이 경우에 re.VERBOSE
또는 re.X
옵션을 사용하면 됩니다.
re.compile(r"""
(?P<name>\w+)\s+ # 이름
(?P<phone> # 전화번호
\d+[-]\d+[-]\d+
)
""", re.X)
https://school.programmers.co.kr/learn/courses/30/lessons/133499
머쓱이는 태어난 지 11개월 된 조카를 돌보고 있습니다. 조카는 아직 "aya", "ye", "woo", "ma" 네 가지 발음과 네 가지 발음을 조합해서 만들 수 있는 발음밖에 하지 못하고 연속해서 같은 발음을 하는 것을 어려워합니다. 문자열 배열 babbling이 매개변수로 주어질 때, 머쓱이의 조카가 발음할 수 있는 단어의 개수를 return하도록 solution 함수를 완성해주세요.
import re
def solution(babbling):
count = 0
words = ["aya", "ye", "woo", "ma"]
pattern = re.compile('^(aya|ye|woo|ma)+$')
for word in babbling:
if pattern.match(word) and not any(word.count(w*2) for w in words):
count += 1
return count