파이썬 정규표현식 뿌시기
학교 수업시간이나 크롤링할때 정규 표현식을 사용해본 적이 있지만 문법이 헷갈려서 사용할때마다 매번 구글링을 해야했다. 이번 기회에 정규표현식을 정리하면서 나를 위한 퀵가이드를 만들어보았다 :)
이번 글에서는 정규표현식의 기본 문법과 re 모듈 사용법에 대해 알아볼 예정이다. 정규표현식은 직접 써보면서 익숙해지는 것이 가장 중요하므로 백준에서 정규표현식을 사용하는 문제를 풀어보고 github에 정리두었다.
정규 표현식이란?
정규 표현식은 문자열에서 특정 문자나 패턴을 찾을때 사용하는 search tool이다. 예를 들어, 크롤링을 하거나 로그 파일에서 특정 로그를 찾을 때 정규 표현식을 사용할 수 있다. computer science에서 정규 표현식을 확장하면 Turing machine이 되고, 이 Turing machine이 곧 컴퓨터가 되기 때문에 중요한 개념이다.
기초 문법
^ : 문자열의 시작과 match
$ : 문자열의 끝과 match
. : \n을 제외한 모든 문자 1개와 match\d : 숫자(0~9)와 match
\w : 알파벳 + 숫자 + _ 와 match
\s : 공백(whitespace)과 match* : 앞의 문자가 0번 이상 반복
+ : 앞의 문자가 1번 이상 반복
? : 앞의 문자가 0번 또는 1번 반복[aeiou] : 나열된 집합에 있는 문자와 match
[^XYZ] : 나열된 집합에 없는 문자와 match (^는 여집합을 의미)
[a-z0-9] : 문자의 범위를 의미
(): 괄호 안의 문자열을 하나의 문자로 그룹화 / 특정 문자열 추출
- \d, \w, \s에서 소문자 대신 대문자로 사용하면 여집합의 의미가 된다.
re 모듈
파이썬에서 re 모듈을 import하여 정규 표현식을 사용할 수 있다. 여러 메소드들 중에서 내가 자주 사용하는 메소드순으로 정리했다.
1. re.fullmatch(pattern, string)
string 전체가 pattern과 match하면 이에 대응하는 match 객체를 반환한다. string이 patten과 match하지 않으면 None을 반환한다. 알고리즘 문제를 풀때 match 메소드보다 더 자주 쓰이는 메소드이다.
2. re.findall(pattern, string)
string에서 pattern과 match하는 부분을 문자열 리스트로 반환한다. 이때 겹치지 않게 match되고, match되는 순서대로 반환된다. 또한 ()를 사용하면 특정 문자열만 추출할 수 있다.
import re
>>> re.findall(r'aba', 'ababa')
['aba'] # 겹치지 않게 일치되므로 ['aba', 'aba']가 아님>>> re.findall(r'([a-z]+):([0-9]+)', 'abc:123')
[('abc', '123')] # :을 기준으로 소문자, 숫자로 이루어진 부분을 각각 추출
3. re.split(pattern, string)
string을 pattern과 match하는 부분을 기준으로 나눈다. pattern에 ()을 사용하면 pattern과 match하는 부분도 반환된다.
import re
>>> re.split(r'[a-z]+', '1abc2d') # 소문자 기준으로 split
['1', '2', '']
>>> re.split(r'([a-z]+)', '1abc2d')
['1', 'abc', '2', 'd', ''] # 소문자 기준으로 split + separators 포함
4. re.search(pattern, string)
string에서 pattern과 match하는 첫번째 위치를 찾고, 이에 대응하는 match 객체를 반환한다. string에서 어느 위치도 pattern과 match하지 않으면 None을 반환한다.
5. re.match(pattern, string)
string의 시작에서부터 0개 이상의 문자가 pattern과 match하면 이에 대응하는 match 객체를 반환한다. string이 pattern과 match하지 않으면 None을 반환한다.
search와 match의 차이점
match는 문자열의 시작 부분에서부터 일치하는지 확인하는 반면, search는 문자열에 pattern과 일치하는 부분이 하나라도 있는지 확인한다. search의 의미대로 match하는 부분이 있는지 ‘찾는다’라고 생각하면 될 것 같다.
import re
>>> re.search(r'[a-z]', '1abc2d')
<re.Match object; span=(1, 2), match='a'>>>> re.match(r'[a-z]', '1abc2d')
# 해당 string이 숫자로 시작하기 때문에 match하지 않고 None을 반환.
6. re.sub(patten, repl, string)
string에서 pattern과 일치하는 부분을 repl로 치환한 후 문자열을 반환한다. string이 pattern과 일치하지 않으면 string이 변경되지 않고 반환한다. 이 메소드는 문자열에서 pattern과 match하는 부분을 제거하고 싶을때 자주 사용한다.
import re
>>> re.sub('[^0-9]', '', 'ab3kd89c')
'389' # 문자열에서 숫자를 제외한 나머지 문자 제거
Greedy Matching
import re
x = 'From: Using the : character'
y = re.findall('^F.+:', x)
print(y) # ['From: Using the :']
위의 예시에서 직관적으로 생각해보면 출력값으로 ‘From:’이 더 적합해보인다. 하지만 실제 출력값은 ‘From’이 아닌 ‘From: Using the :’ 로 매치되는 것을 볼 수 있다.
그 이유는 반복을 의미하는 문자 *과 +가 최대한 largest string을(greedy하게) match하려고 하기 때문이다. 만약 ‘From:’을 얻고 싶다면, 즉 non-greedy하게 매치하고 싶다면 어떻게 해야할까? 반복을 의미하는 문자 *과 + 뒤에 ?을 붙여서 사용하면 된다.
사용 예제
알고리즘 문제에서 정규 표현식을 사용하면 쉽고 간결하게 문제를 해결할 수 있는 경우가 많다. 백준의 14405번 문제를 정규표현식을 사용한 방법과 사용하지 않은 두가지 방법으로 풀어보았다.
정규표현식을 사용하면 확실히 깔끔하게 문제를 풀 수 있다. 더 많은 정규표현식 문제들은 아래 github에 정리해두었다.
정규표현식을 뿌신다는 마음으로 하루 날잡고 공부하니 확실히 실력이 조금 늘은 것 같다. 하지만 막상 공부하다보니 내가 몰랐던 고급 기능이 엄청 많다는 것을 알게되었다. 그래서 앞으로 새로 알게된 내용이 생길때마다 계속 이 글을 업데이트 할 예정이다 :)