31. 정규표현식(Regular Expression)
1. 키워드
- 정규표현식(Regular Expression)
2. 정규표현식 사용하기
- 정규표현식은 일정한 규칙(패턴)을 가진 문자열을 표현하는 방법이다.
- 복잡한 문자열 속에서 특정한 규칙으로 된 문자열을 검색한 뒤 추출하거나 바꿀 때 사용한다.
- 또는, 문자열이 정해진 규칙에 맞는지 판단할 때도 사용한다.
3. 문자열 판단하기
- 간단하게 문자열에 특정 문자열이 포함되어 있는지 판단해 보자.
- 정규표현식은
re
모듈을 가져와서 사용하며match
함수에 정규표현식 패턴과 판단할 문자열을 넣는다.
- 다음은
"Hello, world!"
문자열에"Hello"
와"Python"
이 있는지 판단한다.
>>> import re
# 문자열이 있으므로 정규표현식 매치 객체가 반환됨
>>> re.match("Hello", "Hello, world!")
<_sre.SRE_Match object; span=(0, 5), match='Hello'>
# 문자열이 없으므로 아무것도 반환되지 않음
>>> re.match("Python", "Hello, world!")
- 문자열이 있으면 매치(
SRE_Match
) 객체가 반환되고 없으면 아무것도 반환되지 않는다. - 여기서는
"Hello"
가 있으므로match='Hello'
와 같이 패턴에 매칭된 문자열이 표시된다. - 사실 이런 판단은
"Hello, world!".find("Hello")
처럼 문자열 메서드로도 충분히 가능하다.
1) 문자열이 맨 앞에 오는지 맨 뒤에 오는지 판단하기
- 정규표현식은 특정 문자열이 맨 앞에 오는지 맨 뒤에 오는지 판단할 수 있다.
- 문자열 앞에
^
를 붙이면 문자열이 맨 앞에 오는지 판단하고, 문자열 뒤에$
를 붙이면 문자열이 맨 뒤에 오는지 판단한다.
- 단, 이때는
match
대신search
함수를 사용해야 한다. match
함수는 문자열 처음부터 매칭되는지 판단하지만,search
는 문자열 일부분이 매칭되는지 판단한다.
"^Hello"
는"Hello, world!"
가"Hello"
로 시작하는지 판단하고,"world!$"
는"Hello, world!"
가"world!"
로 끝나는지 판단한다.
# Hello로 시작하므로 패턴에 매칭됨
>>> re.search("^Hello", "Hello, world!")
<_sre.SRE_Match object; span=(0, 5), match='Hello'>
# world!로 끝나므로 패턴에 매칭됨
>>> re.search("world!$", "Hello, world!")
<_sre.SRE_Match object; span=(7, 13), match='world!'>
2) 지정된 문자열이 하나라도 포함되는지 판단하기
|
는 특정 문자열에서 지정된 문자열(문자)이 하나라도 포함되는지 판단하는데, 기본 개념은 OR 연산자와 같다.
"hello|world"
는 문자열에서"hello"
또는"world"
가 포함되는지 판단한다.
# hello 또는 world가 있으므로 패턴에 매칭됨
>>> re.match("hello|world", "hello")
<_sre.SRE_Match object; span=(0, 5), match='hello'>
4. 범위 판단하기
- 이번에는 문자열이 숫자로 되어있는지 판단해 보자.
- 다음과 같이
[]
(대괄호) 안에 숫자 범위를 넣으며*
또는+
를 붙인다. - 숫자 범위는
0-9
처럼 표현하며*
는 문자(숫자)가 0개 이상 있는지,+
는 1개 이상 있는지 판단한다.
# 1234는 0부터 9까지 숫자가 0개 이상 있으므로 패턴에 매칭됨
>>> re.match("[0-9]*", "1234")
<_sre.SRE_Match object; span=(0, 4), match='1234'>
# 1234는 0부터 9까지 숫자가 1개 이상 있으므로 패턴에 매칭됨
>>> re.match("[0-9]+", "1234")
<_sre.SRE_Match object; span=(0, 4), match='1234'>
# abcd는 0부터 9까지 숫자가 1개 이상 없으므로 패턴에 매칭되지 않음
>>> re.match("[0-9]+", "abcd")
*
와+
를 다음과 같이 활용할 수 있다.
# b에는 a가 0개 이상 있으므로 패턴에 매칭됨
>>> re.match("a*b", "b")
<_sre.SRE_Match object; span=(0, 1), match='b'>
# b에는 a가 1개 이상 없으므로 패턴에 매칭되지 않음
>>> re.match("a+b", "b")
# aab에는 a가 0개 이상 있으므로 패턴에 매칭됨
>>> re.match("a*b", "aab")
<_sre.SRE_Match object; span=(0, 3), match='aab'>
# aab에는 a가 1개 이상 있으므로 패턴에 매칭됨
>>> re.match("a+b", "aab")
<_sre.SRE_Match object; span=(0, 3), match='aab'>
a*b
,a+b
에서b
는 무조건 있어야 하는 문자고,a*
는a
가 0개 이상 있어야 하므로b
는 매칭이 된다.- 하지만
a+
는a
가 1개 이상 있어야 하므로b
는 매칭되지 않는다. - 그리고
"ab"
,"aab"
,"aaab"
처럼a
가 0개 이상 또는 1개 이상 있을 때는a*b
와a+b
를 모두 만족한다.
1) 문자가 한 개만 있는지 판단하기
- 문자가 여러 개 있는지 판단할 때는
*
과+
를 사용했지만, 문자가 한 개만 있는지 판단할 때는?
와.
을 사용한다. ?
는?
앞에 문자(범위)가 0개 또는 1개인지 판단하고,.
은.
이 있는 위치에 아무 문자(숫자)가 1개 있는지 판단한다.
# abd에서 c 위치에 c가 0개 있으므로 패턴에 매칭됨
>>> re.match("abc?d", "abd")
<_sre.SRE_Match object; span=(0, 3), match='abd'>
# [0-9] 위치에 숫자가 1개 있으므로 패턴에 매칭됨
>>> re.match("ab[0-9]?c", "ab3c")
<_sre.SRE_Match object; span=(0, 4), match='ab3c'>
# .이 있는 위치에 문자가 1개 있으므로 패턴에 매칭됨
>>> re.match("ab.d", "abxd")
<_sre.SRE_Match object; span=(0, 4), match='abxd'>
2) 문자 개수 판단하기
- 문자(숫자)가 정확히 몇 개 있는지 판단할 때는 문자 뒤에
{개수}
형식을 지정한다. - 문자열의 경우 문자열을 괄호로 묶고 뒤에
{개수}
형식을 지정한다.
h{3}
은h
가 3개 있는지 판단하고,(hello){3}
은hello
가 3개 있는지 판단한다.
>>> re.match("h{3}", "hhhello")
<_sre.SRE_Match object; span=(0, 3), match='hhh'>
>>> re.match("(hello){3}", "hellohellohelloworld")
<_sre.SRE_Match object; span=(0, 15), match='hellohellohello'>
- 특정 범위의 문자(숫자)가 몇 개 있는지 판단할 수도 있는데, 이때는 범위
[]
뒤에{개수}
형식을 지정한다.
- 다음은 휴대전화의 번호 형식에 맞는지 판단한다.
# 숫자 3개-4개-4개 패턴에 매칭됨
>>> re.match("[0-9]{3}-[0-9]{4}-[0-9]{4}", "010-1000-1000")
<_sre.SRE_Match object; span=(0, 13), match='010-1000-1000'>
# 숫자가 3개-4개-4개 패턴에 매칭되지 않음
>>> re.match("[0-9]{3}-[0-9]{4}-[0-9]{4}", "010-1000-1000")
- 이 기능은 문자(숫자)의 개수 범위도 지정할 수 있다.
{시작개수,끝개수}
형식으로 시작 개수와 끝 개수를 지정해 주면 특정 개수 사이에 들어가는지 판단한다.
- 다음은 일반전화의 번호 형식에 맞는지 판단한다.
# 2~3개-3~4개-4개 패턴에 매칭됨
>>> re.match("[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}", "02-100-1000")
<_sre.SRE_Match object; span=(0, 11), match='02-100-1000'>
# 2~3개-3~4개-4개 패턴에 매칭되지 않음
>>> re.match("[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}", "02-10-1000")
3) 숫자와 영문 문자를 조합해서 판단하기
- 지금까지 숫자 범위만 판단해 보았으니 이제 숫자와 영문 문자를 조합해서 판단해 보자.
- 영문 문자 범위는
a-z
,A-Z
와 같이 표현한다.
# a부터 z, A부터 Z, 0부터 9까지 1개 이상 있으므로 패턴에 매칭됨
>>> re.match("[a-zA-Z0-9]+", "Hello1234")
<_sre.SRE_Match object; span=(0, 9), match='Hello1234'>
# 대문자, 숫자는 없고 소문자만 있으므로 패턴에 매칭되지 않음
>>> re.match("[A-Z0-9]+", "hello")
- 이처럼 숫자, 영문 문자 범위는
a-zA-Z0-9
또는A-Z0-9
와 같이 붙여 쓰면 된다.
- 한글의 경우
가-힣
처럼 나올 수 있는 한글 조합을 정해 주면 된다.
# 가부터 힣까지 1개 이상 있으므로 패턴에 매칭됨
>>> re.match("[가-힣]+", "홍길동")
<_sre.SRE_Match object; span=(0, 3), match='홍길동'>
4) 특정 문자 범위에 포함되지 않는지 판단하기
- 지금까지 정규표현식으로 특정 문자 범위에 포함되는지 살펴보았다.
- 특정 문자 범위에 포함되지 않는지 판단하려면, 다음과 같이 문자(숫자) 범위 앞에
^
를 붙이면 해당 범위를 제외한다.
- 즉,
"[^A-Z]+"
는 대문자를 제외한 모든 문자(숫자)가 1개 이상 있는지 판단한다.
# 대문자를 제외. 대문자가 있으므로 패턴에 매칭되지 않음
>>> re.match("[^A-Z]+", "Hello")
# 대문자를 제외. 대문자가 없으므로 패턴에 매칭됨
>>> re.match("[^A-Z]+", "hello")
<_sre.SRE_Match object; span=(0, 5), match='hello'>
- 앞에서 특정 문자열로 시작하는지 판단할 때도
^
를 사용했었는데 문법이 비슷해서 이 부분은 헷갈리기 쉽다. - 범위를 제외할 때는
"[^A-Z]+"
와 같이[]
안에 넣어주고, 특정 문자 범위로 시작할 때는"^[A-Z]+"
와 같이[]
앞에 붙여준다.
- 그래서 다음과 같이
"^[A-Z]+"
는 영문 대문자로 시작하는지 판단한다.
# 대문자로 시작하므로 패턴에 매칭됨
>>> re.search("^[A-Z]+", "Hello")
<_sre.SRE_Match object; span=(0, 1), match='H'>
- 물론 특정 문자(숫자) 범위로 끝나는지 확인할 때는 정규표현식 뒤에
$
를 붙이면 된다.
# 숫자로 끝나므로 패턴에 매칭됨
>>> re.search("[0-9]+$", "Hello1234")
<_sre.SRE_Match object; span=(5, 9), match='1234'>
5) 특수 문자 판단하기
- 문자열을 판단할 때
"Hello1234"
처럼 평범한 문자열만 판단했다.
- 정규표현식에 사용하는 특수 문자
*
,+
,?
,.
,^
,$
,(
,)
,-
등을 판단하려면 특수 문자 앞에\
를 붙이면 된다. - 단,
[]
안에서는\
를 붙이지 않아도 되지만 에러가 발생하는 경우에는\
를 붙인다.
# *이 들어있는지 판단
>>> re.search("\*+", "1 ** 2")
<_sre.SRE_Match object; span=(2, 4), match='**'>
# $, (, )와 문자, 숫자가 들어있는지 판단
>>> re.match("[$()a-zA-Z0-9]+", "$(document)")
<_sre.SRE_Match object; span=(0, 11), match='$(document)'>
- 지금까지 범위를 지정하면서
a-zA-Z0-9
처럼 대소문자와 숫자를 모두 나열했다. - 이런 방식으로 범위를 정하면 정규표현식이 길어지고 복잡해진다.
- 단순히 숫자인지 문자인지 판단할 때는
\d
,\D
,\w
,\W
를 사용하면 편리하다.
1] \d
[0-9]
와 같음- 모든 숫자
2] \D
[^0-9]
와 같음- 숫자를 제외한 모든 문자
3] \w
[a-zA-Z0-9_]
와 같음- 영문 대소문자, 숫자, 밑줄 문자
4] \W
[^a-zA-Z0-9_]
와 같음- 영문 대소문자, 숫자, 밑줄 문자를 제외한 모든 문자
6) 공백 처리하기
- 이번에는 공백을 처리해 보자.
- 공백은
' '
처럼 공백 문자를 넣어도 되고,\s
또는\S
로 표현할 수도 있다.
1] \s
[ \t\n\r\f\v]
와 같음`(공백, 스페이스),
\t(탭),
\n(새 줄, 라인 피드),
\r(캐리지 리턴),
\f(폼피드),
\v`(수직 탭)을 포함
2] \S
[^ \t\n\r\f\v]
와 같음`(공백)을 제외하고
\t,
\n,
\r,
\f,
\v`만 포함
# ' '로 공백 표현
>>> re.match("[a-zA-Z0-9 ]+", "Hello 1234")
<_sre.SRE_Match object; span=(0, 10), match='Hello 1234'>
# \s로 공백 표현
>>> re.match("[a-zA-Z0-9\s]+", "Hello 1234")
<_sre.SRE_Match object; span=(0, 10), match='Hello 1234'>
같은 정규표현식 패턴을 자주 사용할 때
- 매번
match
나search
함수에 정규표현식 패턴을 지정하는 방법은 비효율적이다. - 같은 패턴을 자주 사용할 때는
compile
함수를 사용하여 정규표현식 패턴을 객체로 만든 뒤match
또는search
메서드를 호출하면 된다.
5. 그룹 사용하기
- 지금까지 정규표현식 하나로만 매칭 여부를 판단했다.
- 이번에는 정규표현식을 그룹으로 묶는 방법을 알아보자.
- 정규표현식 그룹은 해당 그룹과 일치하는 문자열을 얻어올 때 사용한다.
- 패턴 안에서 정규표현식을
()
(괄호)로 묶으면 그룹이 된다.
- 다음은 공백으로 구분된 숫자를 두 그룹으로 나누어서 찾은 뒤 각 그룹에 해당하는 문자열(숫자)을 가져온다.
>>> m = re.match("([0-9]+) ([0-9]+)", "10 295")
# 첫 번째 그룹(그룹 1)에 매칭된 문자열을 반환
>>> m.group(1)
'10'
# 두 번째 그룹(그룹 2)에 매칭된 문자열을 반환
>>> m.group(2)
'295'
# 매칭된 문자열을 한꺼번에 반환
>>> m.group()
'10 295'
# 매칭된 문자열을 한꺼번에 반환
>>> m.group(0)
'10 295'
- 매치 객체의
group
메서드에 숫자를 지정하면 해당 그룹에 매칭된 문자열을 반환한다. - 숫자를 지정하지 않거나
0
을 지정하면 매칭된 문자열을 한꺼번에 반환한다.
- 그리고
groups
메서드는 각 그룹에 해당하는 문자열을 튜플로 반환한다.
- 그룹 개수가 많아지면 숫자로 그룹을 구분하기가 힘들어진다.
- 이때는 그룹에 이름을 지으면 편리한데, 그룹의 이름은 다음과 같이
()
(괄호) 안에?P<이름>
형식으로 지정한다.
- 다음은 함수를 호출하는 코드
print(1234)
에서 함수의 이름print
와 인수1234
를 추출한다.
>>> m = re.match("(?P<func>[a-zA-Z_][a-zA-Z0-9_]+)\((?P<arg>\w+)\)", "print(1234)")
# 그룹 이름으로 매칭된 문자열 출력
>>> m.group("func")
'print'
# 그룹 이름으로 매칭된 문자열 출력
>>> m.group("arg")
'1234'
(?P<func>)
와(?P<arg>)
처럼 각 그룹에 이름을 짓고m.group("func")
,m.group("arg")
로 매칭된 문자열을 출력했다.- 참고로 함수 이름은 문자로만 시작해야 하므로 첫글자는
[a-zA-Z_]
로 판단해 준다. - 또한
print
뒤에 붙은(
,)
는 정규표현식에 사용하는 특수 문자이므로 앞에\
를 붙여준다.
1) 패턴에 매칭되는 모든 문자열 가져오기
- 지금까지 그룹에 해당하는 문자열을 가져왔다.
- 그룹 지정 없이 패턴에 매칭되는 모든 문자열을 가져오려면,
findall
함수를 사용하며 매칭된 문자열을 리스트로 반환한다.
- 다음은 문자열에서 숫자만 가져온다.
*
, +
와 그룹 활용하기
- 정규표현식에서
+
와*
을 조합하여 사용할 때는 그룹으로 묶어서 사용한다. (.[a-z]+)*
는 점과 영문 소문자가 1개 이상 있는지 판단하고, 이것 자체가 0개 이상인지 판단한다.- 즉, 규칙은 반드시 지켜야 하지만 있어도 되고 없어도 되는 상황에 사용한다.
# .world는 문자열이므로 패턴에 매칭됨
>>> re.match("[a-z]+(.[a-z]+)*$", "hello.world")
<_sre.SRE_Match object; span=(0, 11), match='hello.world'>
# .1234는 숫자이므로 패턴에 매칭되지 않음
>>> re.match("[a-z]+(.[a-z]+)*$", "hello.1234")
# .뒤에 문자열이 없어도 패턴에 매칭됨
>>> re.match("[a-z]+(.[a-z]+)*$", "hello")
<_sre.SRE_Match object; span=(0, 5), match='hello'>
6. 문자열 바꾸기
- 이번에는 정규표현식으로 특정 문자열을 찾은 뒤 다른 문자열로 바꾸는 방법을 알아보자.
- 문자열을 바꿀 때는
sub
함수를 사용하며 패턴, 바꿀 문자열, 문자열, 바꿀 횟수를 넣어준다. - 여기서 바꿀 횟수를 넣으면 지정된 횟수만큼 바꾸며 바꿀 횟수를 생략하면 찾은 문자열을 모두 바꾼다.
- 다음은 문자열에서
"apple"
또는"orange"
를 찾아서"fruit"
로 바꾼다.
# apple 또는 orange를 fruit로 바꿈
>>> re.sub("apple|orange", "fruit", "apple box orange tree")
'fruit box fruit tree'
- 다음은 숫자만 찾아서
"n"
으로 바꾼다.
sub
함수는 바꿀 문자열 대신 교체 함수를 지정할 수도 있다.- 교체 함수는 매개변수로 마치 객체를 받으며 바꿀 결과를 문자열로 반환하면 된다.
- 다음은 문자열에서 숫자를 찾은 뒤 숫자를 10배로 만든다.
>>> def multiple10(m): # 매개변수로 매치 객체를 받음
... n = int(m.group()) # 매칭된 문자열을 가져와서 정수로 변환
... return str(n * 10) # 숫자에 10을 곱한 뒤 문자열로 변환해서 반환
...
>>> re.sub("[0-9]+", multiple10, "1 2 Fizz 4 Buzz Fizz 7 8")
'10 20 Fizz 40 Buzz Fizz 70 80'
multiple10
함수에서group
메서드로 매칭된 문자열을 가져와서 정수로 바꾼다.- 그리고 숫자에
10
을 곱한 뒤 문자열로 변환해서 반환했다.
- 교체 함수의 내용이 간단하다면 다음과 같이 람다 표현식을 만들어서 넣어도 된다.
>>> re.sub("[0-9]+", lambda m: str(int(m.group()) * 10), "1 2 Fizz 4 Buzz Fizz 7 8")
'10 20 Fizz 40 Buzz Fizz 70 80'
1) 찾은 문자열을 결과에 다시 사용하기
- 이번에는 정규표현식으로 찾은 문자열을 가져와서 결과에 다시 사용해 보자.
- 먼저 정규표현식을 그룹으로 묶고 나면 바꿀 문자열에서
\\숫자
형식으로 매칭된 문자열을 가져와서 사용할 수 있다.
- 다음은
"hello 1234"
에서hello
는 그룹 1,1234
는 그룹 2로 찾은 뒤 그룹 2, 1, 2, 1 순으로 문자열의 순서를 바꿔서 출력한다.
# 그룹 2, 1, 2, 1 순으로 바꿈
>>> re.sub("([a-z]+) ([0-9]+)", "\\2 \\1 \\2 \\1", "hello 1234")
'1234 hello 1234 hello'
- 다음은
'{"name": "james"}'
를"<name>james</name>"
형식으로 바꾼다.
>>> re.sub('({\s*)"(\w+)":\s*"(\w+)"(\s*})', "<\\2>\\3</\\2>", '{ "name": "james" }')
'<name>james</name>'
- 이 정규표현식에 그룹은 4개이다.
- 맨 처음
({\s*)
은{
와`(공백)을 찾으므로
"{ "`을 찾는다. - 그리고 마지막
(\s*})
은`(공백)과
}를 찾으므로
" }"`를 찾는다. - 중간에 있는
"(\w+)":\s*"(\w+)"
은:
을 기준으로 양 옆의name
과james
를 찾는다. - 바꿀 문자열은
"<\\2>"
와 같이 그룹 2name
과 그룹 3james
만 사용하고 그룹 1"{ "
, 그룹 4"} "
는 버린다.
- 만약 그룹에 이름을 지었다면
\\g<이름>
형식으로 매칭된 문자열을 가져올 수 있다.
>>> re.sub('({\s*)"(?P<key>\w+)":\s*"(?P<value>\w+)"(\s*})', "<\\g<key>>\\g<value></\\g<key>>", '{ "name": "james" }')
'<name>james</name>'
raw 문자열 사용하기
- 정규표현식의 특수 문자를 판단하려면
\
를 붙여야 한다. - 여기서 문자열 앞에
r
을 붙여주면 원시(raw) 문자열이 되어\
를 붙이지 않아도 특수 문자를 그대로 판단할 수 있다. - 따라서 raw 문자열에서는
\\숫자
,\\g<이름>
,\\g<숫자>
는\숫자
,\g<이름>
,\g<숫자>
형식처럼\
를 하나만 붙여서 사용할 수 있다.