[코딩테스트] Python 문자열 - 정규식

김희정·2024년 5월 29일
0

Coding Test

목록 보기
2/7
post-custom-banner

💎 들어가며

정규식은 모든 프로그래밍에서 어려운 영역 중에 하나로 손 꼽히는데요, 문자열 관련 문제를 풀다가 정규 표현식을 이용하여 깔끔하게 푼 문제가 있어 이번 기회에 정리해볼까 합니다.

간략하게 포스팅 내용을 정리해보자면 다음과 같습니다.

  • re 모듈 이용
  • 메타 문자: . ^ $ * + ? { } [ ] \ | ( )
  • 컴파일: re.compile("expression")
  • 검색 메소드: match, search, findall, finditer
  • pattern 객체 메소드: group, start, end, span
  • 옵션: re.DOTALL, re.IGNORECASE, re.MULTILINE, re.VERBOSE

1. 정규식

정규 표현식(regular expressions) 은 복잡한 문자열을 처리할 때 사용하는 기법으로, 파이썬만의 고유 문법이 아니라 문자열을 처리하는 모든 곳에서 사용하는 일종의 형식 언어이다.

정규 표현식은 우리가 흔히 알고 있는 정규식으로도 불립니다. 정규식은 파이썬 뿐만 아니라 많은 영역에서 사용되는데, 주된 목적은 정규표현식의 패턴을 이용하여 해당하는 문자열을 골라내고 치환하는 역할을 합니다.


1.1 메타 문자

메타 문자는 원래 그 문자가 가진 뜻이 아니라 특별한 의미를 가진 문자를 의미합니다. 정규 표현식에서 다음과 같은 메타 문자를 사용하면 특별한 의미를 갖게 됩니다.

. ^ $ * + ? { } [ ] \ | ( )

[ ] 문자 - 문자 클래스

문자 클래스로 만들어진 정규식은 '['와 ']' 사이의 문자들의 매치라는 의미를 가집니다. 예를 들어, 정규 표현식이 [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"))

.(dot) 문자 - \n을 제외한 모든 문자

.(dot) 메타 문자는 줄바꿈 문자인 \n을 제외한 모든 문자와 매치됩니다.

정규식을 작성할 때 re.DOTALL 옵션을 주면 .(dot) 문자와 \n 문자도 매치된다.

import re
p = re.compile("a.b")

해석은 "a + 모든 문자 + b"입니다. 즉, a와 b 사이에 어떤 문자가 들어가도 모두 매치된다는 의미입니다.


* 문자 - 반복 (0, -)

import re
p = re.compile("ca*t")

* 바로 앞에 있는 문자 a가 0부터 무한대까지 반복될 수 있다는 의미입니다.

  • ct - Yes
  • cat - Yes
  • caat - Yes

+ 문자 - 반복 (1, -)

반복을 나타내는 또 다른 메타 문자로 +가 있는데, 최소 1번 이상 반복될 때 사용합니다.

import re
p = re.compile("ca+t")
  • ct - No
  • cat - Yes
  • caat - 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

| 메타 문자는 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

1.2 re 모듈

파이썬에서는 정규 표현식을 지원하기 위해 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')
  1. re.compile 함수를 이용하여 정규 표현식을 컴파일합니다.
  2. re.compile의 리턴 값을 객체 p(컴파일된 패턴 객체)에 할당해 그 이후의 작업을 수행합니다.

1.3 문자열 검색

re.compile 메소드를 통해 컴파일된 패턴 객체를 사용하여 문자열을 검색할 수 있습니다. 패턴 객체는 다음과 같은 4가지 메소드를 제공합니다.

  • match() - 문자열의 처음부터 정규식과 매치되는지 조사
  • search() - 문자열 전체를 검색하여 정규식과 매치되는지 조사
  • findall() - 정규식과 매치되는 모든 문자열을 리스트로 리턴
  • finditer() - 정규식과 매치되는 모든 문자열을 반복 가능한 객체로 리턴

match()

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()

findall은 패턴과 매치되는 모든 값을 찾아 리스트로 리턴합니다.

p = re.complie('[a-z]+')
result = p.findall('life is too short')
# 출력: ['life', 'is', 'too', 'short']

finditer()

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'>

1.4 match 객체

match 객체란 앞서 살펴본 p.match, p.search, p.finditer 메소드에 의해 리턴된 매치 객체를 의미합니다.

match 객체에서 사용할 수 있는 메소드는 다음과 같습니다.

  • group(): 매치된 문자열 리턴
  • start(): 매치된 문자열의 시작 위치 리턴
  • end(): 매치된 문자열의 끝 위치 리턴
  • span(): 매치된 문자열의 위치(시작, 끝)에 해당하는 튜플 리턴
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)


1.5 컴파일 옵션

정규식을 컴파일할 때 다음 옵션을 사용할 수 있습니다.

  • DOTALL(S) - .(dot)이 줄바꿈 문자를 포함해 모든 문자와 매치될 수 있게 한다
  • IGNORECASE(I) - 대소문자에 관계없이 매치될 수 있게 한다
  • MULTILINE(M) - 여러 줄과 매치될 수 있게 한다. ^, $ 메타 문자와 관련 있는 옵션
  • VERBOSE(X) - verbose 모드를 사용할 수 있게 한다. 정규 표현식을 보기 편하게 만들 수 있고 주석 등을 사용할 수 있게 된다.

옵션을 사용할 때는 re.DOTALL 처럼 전체 옵션 이름을 써도 되고 re.S로 사용해도 됩니다.


DOTALL, 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'>

IGNORECASE, I

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'>

MULTILINE, M

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 옵션은 ^, $ 메타 문자를 문자열의 각 줄마다 적용해 주는 것입니다.

VERBOSE, X

이해하기 어려운 정규식을 주석 또는 줄 단위로 구분할 수 있다면 얼마나 보기 좋고 이해하기 쉬울까요?

이 경우에 re.VERBOSE 또는 re.X 옵션을 사용하면 됩니다.

re.compile(r"""
	(?P<name>\w+)\s+ 	# 이름
    (?P<phone>			# 전화번호
    	\d+[-]\d+[-]\d+
    )
""", re.X)

2. 문제 풀이

문제

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

💎 References

profile
Java, Spring 기반 풀스택 개발자의 개발 블로그입니다.
post-custom-banner

0개의 댓글