정규표현식
정규표현식은 처음에 점프 투 파이썬을 볼때 이해가 안되서 넘겼던 내용인데,
어쩌다 다시 만나게 되어 공부하는 차에 정리를 했다.
정규표현식은 특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어로, 복잡한 문자열을 다루기 위해 사용한다. 메타 문자(meta characters)를 이용하여 규칙을 표현하며, 주된 메타 문자는 아래와 같다.
[ ] (문자 클래스)
문자 클래스로 만들어진 정규식은 '[ ] 사이의 문자들과 매치'라는 의미를 갖는다.
예를 들어, [abc]라는 정규식은 'a, b, c 중 한 개의 문자와 매치'를 의미한다.
리스트가 아니다.
[abc]
'a' #a가 있으므로 매치
'boy' #b가 있으므로 매치
'dorm' #a, b, c가 모두 없으므로 매치 X
[ ] 안의 하이픈(-)은 문자열의 범위를 나타내며, ^는 부정의 의미를 갖는다.
[a-zA-Z] # 알파벳 모두
[^0-9] # 숫자가 아님
Dot(.)
dot 메타문자는 모든 문자와 매치됨을 의미한다.
a.b #a, b 사이에 줄바꿈을 제외한 어떤 문자가 들어가도 모두 매치
'aab'
'a0b'
'abc' # a, b 사이에 어떤 문자라도 하나는 있어야 하는 정규식과 일치하지 않으므로 매치X
* / +
* 와 +는 모두 바로 앞 문자의 반복을 의미한다.
다만 * 는 반복 횟수가 0회부터, +는 반복 횟수가 1회부터 매치된다.
ca*t #아래 문자열은 모두 매치
'ct'
'cat'
'caaaaaaaaaaaaaaaaaat'
ca+t
'ct' #a가 1회 이상 반복되어야 하는데 0개이므로 매치X
'cat'
'caaaaaaaaaaaaaaaaaaat'
그 외 반복회수를 {m:n}으로 지정해줄 수도 있고,
? 을 사용하면 0 ~ 1회 반복되는 케이스만 찾는다는데 자주 쓰이는지는 잘 모르겠다.
네, 정규 표현식에서 ? 메타 문자는 자주 사용됩니다. 주로 다음과 같은 목적으로 사용됩니다:
- Optional 매칭: ?는 앞에 오는 패턴이 없거나 한 번만 등장할 수 있음을 나타냅니다. 예를 들어, colou?r는 'color'와 'colour' 둘 다에 매치됩니다. u?nique는 'nique'와 'unique' 둘 다에 매치됩니다.
- Non-Greedy 매칭: 기본적으로 정규 표현식은 greedy 매칭을 수행합니다. 즉, 가장 긴 문자열에 대해 매칭을 시도합니다. 하지만 ?를 사용하면 non-greedy 매칭을 할 수 있습니다. 예를 들어, a+?는 'a'가 한 번 이상 등장하지만 가능한 가장 짧은 매치를 찾습니다.
- Lookahead 및 Lookbehind: ?=는 positive lookahead, ?!는 negative lookahead, ?<=는 positive lookbehind, ?<!는 negative lookbehind에 사용됩니다. 이들은 패턴을 검색하지만 실제로는 매치되는 텍스트를 반환하지 않고 다음 또는 이전에 일치하는지 확인합니다.
?는 다양한 상황에서 유용하게 사용될 수 있으며, 선택적인 매칭이나 매치 방식을 조절하는 데 자주 쓰입니다.
ChatGPT는 그렇다고는 한다.
re 모듈
re 모듈은 정규표현식을 지원하기 위한 모듈로, 아래의 메서드를 사용할 수 있다.
match() : 문자열의 처음부터 정규식과 매치되는지 조사한다.
match시 match 객체를 리턴하고, 매치되지 않을 시 None을 리턴한다.
import re
p = re.compile('[a-z]+') # 알파벳 문자열만으로 구성되었는지 확인
m =p.match('python')
print(m)
n = p.match('3.7 python')
print(n)
>> <re.Match object; span=(0, 6), match='python'>
>> None
search() : 문자열 전체를 검색하여 정규식과 매치되는지 조사한다.
match() 메서드와 동일하지만, 일부가 매치되어도 match 객체를 리턴한다.
import re
p = re.compile('[a-z]+')
m =p.search('python')
print(m)
n = p.search('3.7 python')
print(n)
>> <re.Match object; span=(0, 6), match='python'>
>> <re.Match object; span=(4, 10), match='python'>
이렇게 두 개 메서드는 boolean 조건과 결합하여 쓰인다.
예를 들어, match값이 있으면 다음 작업을 수행하고, None이면 수행하지 않는다 등
findall() : 정규식과 매치되는 모든 문자열을 리스트로 돌려준다.
import re
p = re.compile('[a-z]+')
m = p.findall('life!is,too~~shortㅠㅠ')
print(m)
>> ['life', 'is', 'too', 'short']
문자열이 단순히 'life is too short'였다면 split() 메서드를 사용해도 되었겠지만,
이렇게 여러 가지가 섞인 경우 정규표현식을 사용하면 훨씬 효율적으로 작업할 수 있다.
finditer() : 정규식과 매치되는 모든 문자열을 반복 가능한 객체로 돌려준다.
sub() : 정규식의 매칭되는 부분을 다른 문자열로 치환하는 메서드
re.sub(검색할 정규 표현식, 치환 문자열, 대상문자열) 형태로 작성한다.
import re
text = "Contact us at support@example.com or info@example.com"
pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' # 이메일 주소를 나타내는 정규 표현식
masked_text = re.sub(pattern, '[EMAIL]', text)
print(masked_text)
>> Contact us at [EMAIL] or [EMAIL]
ChatGPT가 예제용이 아닌 진짜 정규표현식을 써줬다. 해석하기도 어렵다..
이 정규 표현식은 이메일 주소 패턴을 검색하는 데 사용됩니다.
여기서 \b는 단어 경계를 나타내며, 패턴의 시작이나 끝을 나타냅니다.
이메일 주소 패턴의 구성 요소는 다음과 같습니다:
[A-Za-z0-9._%+-]+: 이 부분은 이메일 주소의 로컬 파트(local part)를 나타냅니다.
여기서는 영문 대소문자, 숫자, 그리고 특수 문자들(._%+-)이 하나 이상 등장해야 합니다.
@: 이 부분은 이메일 주소의 로컬 파트와 도메인 파트(domain part)를 나누는 '@' 기호입니다.
[A-Za-z0-9.-]+: 이 부분은 이메일 주소의 도메인 이름을 나타냅니다.
여기서는 영문 대소문자, 숫자, 그리고 하이픈('-')이 하나 이상 등장해야 합니다.
\. : 이 부분은 도메인 이름과 최상위 도메인(TLD, Top-Level Domain)을 나타내는 점(.)입니다.
정규 표현식에서는 점을 특수 문자로 다루기 위해 이스케이프() 처리되었습니다.
[A-Z|a-z]{2,} : 이 부분은 최상위 도메인(TLD)을 나타냅니다.
여기서는 영문 대소문자로 이루어진 최소 2글자 이상의 문자열이어야 합니다.
\b: 다시 단어 경계를 나타내며, 패턴의 시작이나 끝을 나타냅니다.
이걸 왜 공부하게 되었냐면 아래 문제 때문이다.
Most Common Word - LeetCode
Can you solve this real interview question? Most Common Word - Given a string paragraph and a string array of the banned words banned, return the most frequent word that is not banned. It is guaranteed there is at least one word that is not banned, and tha
leetcode.com
문제는 어려울 게 없어서 다음과 같은 코드를 짰더니 답이 안나온다.
paragraph = 'Bob hit a ball, the hit BALL flew far away after it was hit.'
banned = ['hit']
from collections import Counter
paragraph = paragraph.lower().split()
word_list = [word for word in paragraph if word.isalnum() and word not in banned]
word_count = Counter(word_list)
print(word_count.most_common[1])
이유는 ball, 때문인데, isalnum() 메서드를 쓰면서 저 부분이 날아가버렸다.
그 전에 풀었던 팰린드롬 문제는 글자 단위로 잘라서 문제가 없었지만,
이 문제는 단어 단위로 보기 때문에 답이 틀렸다.
.isalnum() 메서드를 정규표현식으로 바꿔주고, 그에 맞게 코드를 수정한다.
from collections import Counter
import re
paragraph = 'Bob hit a ball, the hit BALL flew far away after it was hit.'
banned = ['hit']
word_list = [word for word in re.findall('[0-9a-zA-Z]+', paragraph.lower()) if word not in banned]
count = Counter(word_list)
answer = count.most_common(1)[0][0]
print(answer)
1) re.findall() 메서드는 매치되는 모든 문자열을 리스트로 돌려주므로, paragraph를 미리 split할 필요가 없다.
2) most_common() 메서드는 그대로 쓰면 빈도순으로 나열된 튜플 리스트를 리턴하고,
n 값을 지정하면 n에 해당하는 튜플을 반환한다.
이 경우 most_common(1)을 돌리면 [(hit, 3)]이 반환되므로
해당 리스트의 첫번째 원소의 첫번째 원소를 선택하면 된다 → most_common(1)[0][0]
간단한 문제도 생각보다 정확히 맞추기는 너무 어렵다...