1. Prologue
"100-200*300-500+20"
프로그래머스 67257번 문제이자, 2020 카카오 인턴십 2번 문제다.
위와 같은 문자열 수식이 주어진다고 할 때, 저 수식을 활용하여 정답을 도출하는 문제였다.
문제는 저 입력이 '문자열'이라는 것이다.
어떻게 저 연산 기호와 숫자를 잘 분리하느냐가 핵심이었다.
string = "100-200*300-500+20"
nums = string.replace("*", "_").replace("+", "_").replace("-", "_").split("_")
for num in set(nums): string = string.replace(num, "_")
operator = string.split("_")[1:-1]
# ['100', '200', '300', '500', '20']
print(nums)
# ['-', '*', '-', '+']
print(operator)
해당 문제에서 숫자는 0에서 999까지의 숫자 범위라고 명시해놓았다.
하지만 숫자를 기준으로 나누기보다는, 3개뿐인 연산 기호를 기준으로 나누는 것이 빠르다고 생각했다.
그래서 우선 연산 기호 3종류를 전부 '_' 문자로 바꿔주었다.
└ 익숙하면서도 무시하라는 의미가 담겨있도록 underscore를 사용하였다.
그런 다음 split() 내장 함수를 사용하여 분리하면 nums에 숫자들만 담긴다.
그런 다음 nums를 set 자료 구조로 바꾼다.
기존 string 문자열에서 해당 숫자 문자를 전부 '_'로 바꾸고, 다시 split()으로 분리하면, 연산 기호만 뽑을 수 있다.
이때 set을 사용하는 이유는, 숫자가 중복되는 경우가 있을 수 있기 때문이다.
예를 들어 nums에 ["10", "20", "20", "10"]이 담겼다고 해보자.
replace(bef, aft) 함수를 사용하면 해당 문자열 내에 있는 모든 bef를 aft로 바꿔준다.
즉 처음에 있는 "10"을 이미 바꿔주었기 때문에, 뒤에 있는 "10"은 바꿀 이유가 없다.
시간을 줄이기 위해 set을 사용하였다.
import re
string = "100-200*300-500+20"
expression = re.split('([*+-])', string)
# ['100', '-', '200', '*', '300', '-', '500', '+', '20']
print(expression)
나름 깔끔하게 작성해서 괜찮다고 생각하던 찰나에, 정규 표현식을 접했다.
단 한 줄의 코드로 내가 위에서 작성한 과정을 보기 좋게 줄여버렸다.
이에 정규 표현식이라는 '문자열 처리'에 대한 필요성을 느꼈다.
과장을 조금 보태자면, 정규 표현식을 사용하면 어떤 문자열이든 단 한 줄 코딩으로 처리가 가능하다.
2. Regular Expression
2.1. 개념
정규 표현식(Regular Expression)은 텍스트에서 문자열 패턴을 찾기 위한 형식이다.
특정 규칙을 가진 문자열을 표현하기 위해 사용하며, 검색ㆍ대체 ㆍ추출 같은 작업에 용이하다.
정규 표현식을 사용할 때 '메타 문자'라는 것을 사용한다.
2.2. 메타 문자
메타 문자 | |||
종류 | 설명 | 예시 | |
. | 어떤 문자 하나와 일치하는지 확인 (줄바꿈 제외) | a.b는 acb, aab, a3b와 일치, ab, a\nb와 불일치 | |
^ | 문자열의 시작과 일치하는지 확인 | ^a는 abc, apk, a1a와 일치, ba, meta와 불일치 | |
$ | 문자열의 끝과 일치하는지 확인 | a$는 aaa, sea와 일치, apple, naan과 불일치 | |
* | 바로 앞의 문자가 0번 이상 반복하는지 확인 | bo*는 b, bo, booo와 일치, bq, base와 불일치 | |
+ | 바로 앞의 문자가 1번 이상 반복하는지 확인 | b+c는 bc, bbc, bbbbc와 일치, bb, c, bac와 불일치 | |
{ } | 정확한 반복 횟수나 범위를 지정 | a{2}는 aa와 일치, a{2, 3}은 aa, aaa와 일치 | |
[ ] | 대괄호 안에 문자 중 하나와 일치하는지 확인 | [abc]는 a, b, c와 일치, ab, bc, ac와 불일치 | |
\ | 다음에 오는 메타 문자를 문자 그대로 취급 | \^는 ^ 문자와 일치(메타 문자 취급을 하지 않음) | |
| | 두 조건 중 하나와 일치하는지 확인 (OR) | a|b는 a, b와 일치, ab, ba, aa, bb와 불일치 | |
() | 정규식을 그룹으로 묶어 여러 문자를 조합하여 사용 | b(an)+a는 banana와 일치, ba, ban, bnaa과 불일치 | |
? | 바로 앞의 문자가 0번 혹은 1번 있는지 확인 | ab?c는 ac와 일치, abc와 일치, abbc와 불일치 |
메타 문자란, 원래 문자가 가진 뜻이 아닌 다른 의미로 사용하는 문자들을 말한다.
예를 들어 마침표에 해당하는 온점(.)은 '문장의 맺음'을 표현하거나, 객체 메소드를 호출하는 용도(~의)로 사용한다.
하지만 메타 문자로서 온점은 '정확하게 이 문자와 대응하는 문자'라는 의미로서 사용한다.
이런 메타 문자의 종류와 설명, 예시를 표로 정리하였다.
*, +, ?는 각각 {0, }, {1, }, {0, 1}로 표현하는 것이 가능하다.
하지만 가독성 때문에 메타문자로 적는 것을 권장한다고 한다.
적재적소에 필요에 따라 사용하면 된다.
2.3. 자주 사용하는 표현 표기법
자주 사용하는 정규식은 별도의 표기법으로 표현할 수 있다 | |
문자 클래스 | 설명 |
\d | 숫자와 매치된다. [0-9]와 동일한 표현식이다. |
\D | 숫자가 아닌 것과 매치된다. [^0-9]와 동일한 표현식이다. |
\s | 공백(whitespace) 문자와 매치된다. |
\S | 공백 문자가 아닌 것과 매치된다. |
\w | 문자+숫자(alphanumeric)와 매치된다. [a-zA-Z0-9_]와 동일한 표현식이다. |
\W | 문자+숫자(alphanumeric)가 아닌 문자와 매치된다. [^a-zA-Z0-9_]와 동일한 표현식이다. |
대괄호[ ]를 사용한 메타 문자는 다양하게 활용할 수 있다.
[a-z]는 영소문자, [A-Z]는 영대문자, [a-zA-Z]는 모든 영문자, [0-9]는 모든 숫자처럼 말이다.
그래서 이런 정규 표현식은 약어로 존재한다. 그것을 정리해 둔 표이다.
소문자와 대문자의 관계가 서로 상반됨을 알 수 있다.
2.4. Regular Expression Method
Method | 목적 |
match() | 문자열의 맨 시작부터 정규식 패턴과 일치하는 내용을 찾는다. |
예시 | re.match('hi', 'hello hi') >>> None re.match('hi', 'hi greeting') >>> <re.Match object; span=(0, 2), match='hi'> |
search() | 문자열 전체에서 정규식 패턴과 일치하는 첫 번째 내용을 찾는다. |
예시 | re.search('hi', 'hello hi') >>> <re.Match object; span=(6, 8), match='hi'> re.match('hi', 'hi, hi?') >>> <re.Match object; span=(0, 2), match='hi'> |
findall() | 문자열 전체에서 정규식 패턴과 일치하는 모든 내용을 찾는다. |
예시 | re.findall('hi', 'hi hi hello') >>> ['hi', 'hi'] re.findall('a+b', 'aab, aba, aab, b') >>> ['aab', 'ab, 'aab'] |
finditer() | 문자열 전체에서 정규식 패턴과 일치하는 모든 내용을 찾아 iterator로 반환한다. |
예시 | re.finditer('ab', 'abracadabra') >>> <callable_iterator object at 0x00001CB7AE06E3> |
fullmatch() | 문자열 전체가 정규식 패턴과 정확히 일치하는 경우에만 객체를 반환한다. |
예시 | re.fullmatch('Pomeranian', 'Pomegranate') >>> None re.fullmatch('b(an)*a', 'banana') >>> <re.Match object; span=(0, 6), match='banana'> |
split() | 정규식 패턴을 구분자로 사용하여 문자열을 분할한다. 이때 ()를 사용하면 구분자를 포함한다. |
예시 | re.split('-', 'What-The-Hell') >>> ['What', 'The', 'Hell'] re.split('(-)', "What-The-Hell') >>> ['What', '-', 'The', '-', 'Hell'] |
sub() | 문자열 전체에서 정규식 패턴과 일치하는 부분을 다른 문자열로 대체한다. |
예시 | re.sub(' ', '!OH!', 'friends like me') >>> friends!OH!like!OH!me re.sub('o|g', '-', 'algorithmprogramming') >>> al--rithmpr--ammin- |
subn() | sub() 결과에 대체한 횟수도 함께 반환한다. |
예시 | re.subn(' ', '!OH!', 'friends like me') >>> (friends!OH!like!OH!me, 2) re.subn('o|g', '-', 'algorithmprogramming') >>> (al--rithmpr--ammin-, 5) |
위에서 설명한 메타 문자를 이용하여 원하는 문자열을 추출할 차례이다.
기본적으로 re(regular expression) 라이브러리를 호출해야 사용이 가능하다.
예를 들어서 'ab*'라는 정규식 패턴과 일치하는지 찾고 싶다고 해보자.
우선 pattern = re.compile('ab*')로 컴파일 객체 만들어야 한다.
그 다음 result = pattern.match('abbb')의 형태로 코드를 작성하면 result에 결과가 담기게 된다.
이 과정을 축약하여 result = re.match('ab*', 'abbb') 형태로 작성하는 것도 지원한다.
위의 표에서는 가독성을 위하여 축약 형태로 예시를 설명했음을 알린다.
2.5. Match Method
Match 객체의 method |
||
Method | 용도 | 예시 |
group | 문자열 반환 | re.search('isThere', 'dvDNJisThereKMdcOnklm').group() >>> isThere |
start | 문자열의 시작 위치를 반환 | re.search('isThere', 'dvDNJisThereKMdcOnklm').start() >>> 5 |
end | 문자열의 끝 위치를 반환 | re.search('isThere', 'dvDNJisThereKMdcOnklm').end() >>> 12 |
span | 문자열의 (시작, 끝)을 tuple로 반환 | re.search('isThere', 'dvDNJisThereKMdcOnklm').span() >>> (5, 12) |
match, search, finditer 등의 메소드로 객체를 탐색할 경우, Match Object 형태로 결과를 반환한다.
하지만 일치하는 문자가 있는지? 시작은 어디인지? 끝은 어디인지?가 궁금한 것이다.
위의 4가지 메소드를 사용하면 match object 결과에서 원하는 부분만 return 할 수 있다.
2.6. Compile Option
Compile Option | ||
종류 | 설명 | 예시 |
IGNORECASE 또는 I | 대소문자를 구분하지 않음 | re.search('abc', 'ABC', re.IGNORECASE) >>> <re.Match object; span=(0, 3), match='ABC'> |
DOTALL 또는 S | 줄바꿈 문자를 포함한 모든 문자와 매칭 | re.search('a.b', 'a\nb', re.DOTALL) >>> <re.Match object; span=(0, 3), match='a\nb'> |
MULTILINE 또는 M | 메타 문자 ^, $를 각 줄에 대해서 적용 | re.search('^a', 'ba\na', re.MULTILINE) >>> <re.Match object; span=(3, 4), match='a'> re.search('k$', 'asd\n'apk', re/MULTILINE) >>> <re.Match object; span=(5, 6), match='k'> |
VERBOSE 또는 X | 문자열에 사용한 공백과 주석 무시 | re.compile(r''' \d+ # 숫자 \. # 소수점 \d+ # 소수 부분 ''', '3.141592', re.VERBOSE) >>> <re.Match object; span=(0, 8), match='3.141592'> |
컴파일 옵션(혹은 플래그)라고 부르는 동작 방식이 있다.
re 라이브러리에서 제공하는 패턴 객체 생성 시의 컴파일 옵션이다.
마지막 VERBISE 옵션에서 사용한 r'' 문법은 raw string 문법이다.
메타 문자를 활용하여 [\d+]라고 사용하지 않고 단 한 글자만 필요하여 \d라고 쓰고 싶다고 해보자.
이러면 역슬래시(\)를 메타 문자로 인식해버린다. 따라서 \d가 아닌 \\d라고 작성해주어야 한다.
이것을 r'\d'라고 작성할 수 있다.
예시에서 r''' '''로 3개씩 사용한 이유는 여러 줄에 걸쳐있는 경우이기 때문이다.
고민을 시작한 문제 출처 : https://school.programmers.co.kr/learn/courses/30/lessons/67257
참고한 문서 자료 : https://wikidocs.net/4308
참고한 문서 자료 : https://wikidocs.net/21703
참고한 웹 사이트 : https://nachwon.github.io/regular-expressions/
참고한 웹 사이트 : https://spidyweb.tistory.com/373
'Computer Science > 파이썬(Python)' 카테고리의 다른 글
Python Decorator (0) | 2023.11.12 |
---|---|
Python Iterator (0) | 2023.11.11 |
Python list.pop(0) vs deque.popleft() (0) | 2023.10.03 |
Python float OverflowError (0) | 2023.09.19 |
Python Namespace(Scope) (0) | 2023.08.18 |