Skip to content

31. 정규표현식(Regular Expression)


1. 키워드

  • 정규표현식(Regular Expression)


2. 정규표현식 사용하기

  • 정규표현식은 일정한 규칙(패턴)을 가진 문자열을 표현하는 방법이다.
  • 복잡한 문자열 속에서 특정한 규칙으로 된 문자열을 검색한 뒤 추출하거나 바꿀 때 사용한다.
  • 또는, 문자열이 정해진 규칙에 맞는지 판단할 때도 사용한다.


3. 문자열 판단하기

  • 간단하게 문자열에 특정 문자열이 포함되어 있는지 판단해 보자.


  • 정규표현식은 re 모듈을 가져와서 사용하며 match 함수에 정규표현식 패턴과 판단할 문자열을 넣는다.


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는 문자열 일부분이 매칭되는지 판단한다.


re.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개 이상 있는지 판단한다.


[0-9]*
[0-9]+
# 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*ba+b를 모두 만족한다.


1) 문자가 한 개만 있는지 판단하기

  • 문자가 여러 개 있는지 판단할 때는 *+를 사용했지만, 문자가 한 개만 있는지 판단할 때는 ?.을 사용한다.
  • ?? 앞에 문자(범위)가 0개 또는 1개인지 판단하고, ..이 있는 위치에 아무 문자(숫자)가 1개 있는지 판단한다.


문자?
[0-9]?
.
# 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'>


  • 특정 범위의 문자(숫자)가 몇 개 있는지 판단할 수도 있는데, 이때는 범위 [] 뒤에 {개수} 형식을 지정한다.


[0-9]{개수}


  • 다음은 휴대전화의 번호 형식에 맞는지 판단한다.


# 숫자 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")


  • 이 기능은 문자(숫자)의 개수 범위도 지정할 수 있다.
  • {시작개수,끝개수} 형식으로 시작 개수와 끝 개수를 지정해 주면 특정 개수 사이에 들어가는지 판단한다.


(문자){시작개수,끝개수}
(문자열){시작개수,끝개수}
[0-9]{시작개수,끝개수}


  • 다음은 일반전화의 번호 형식에 맞는지 판단한다.


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


같은 정규표현식 패턴을 자주 사용할 때

  • 매번 matchsearch 함수에 정규표현식 패턴을 지정하는 방법은 비효율적이다.
  • 같은 패턴을 자주 사용할 때는 compile 함수를 사용하여 정규표현식 패턴을 객체로 만든 뒤 match 또는 search 메서드를 호출하면 된다.


객체 = re.compile("패턴")
객체.match("문자열")
객체.search("문자열")
# 정규표현식 패턴을 객체로 만듦

>>> p = re.compile("[0-9]+")

# 정규표현식 패턴 객체에서 match 메서드 사용

>>> p.match("1234")
<_sre.SRE_Match object; span=(0, 4), match='1234'>

# 정규표현식 패턴 객체에서 search 메서드 사용

>>> p.search("hello")


5. 그룹 사용하기

  • 지금까지 정규표현식 하나로만 매칭 여부를 판단했다.
  • 이번에는 정규표현식을 그룹으로 묶는 방법을 알아보자.
  • 정규표현식 그룹은 해당 그룹과 일치하는 문자열을 얻어올 때 사용한다.


  • 패턴 안에서 정규표현식을 ()(괄호)로 묶으면 그룹이 된다.


(정규표현식) (정규표현식)


  • 다음은 공백으로 구분된 숫자를 두 그룹으로 나누어서 찾은 뒤 각 그룹에 해당하는 문자열(숫자)을 가져온다.


매치객체.group(그룹숫자)
>>> 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 메서드는 각 그룹에 해당하는 문자열을 튜플로 반환한다.


매치객체.groups()
# 각 그룹에 해당하는 문자열을 튜플 형태로 반환
>>> m.groups()
('10', '295')


  • 그룹 개수가 많아지면 숫자로 그룹을 구분하기가 힘들어진다.


  • 이때는 그룹에 이름을 지으면 편리한데, 그룹의 이름은 다음과 같이 ()(괄호) 안에 ?P<이름> 형식으로 지정한다.


(?P<이름>정규표현식)


  • 다음은 함수를 호출하는 코드 print(1234)에서 함수의 이름 print와 인수 1234를 추출한다.


매치객체.group("그룹이름")
>>> 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 함수를 사용하며 매칭된 문자열을 리스트로 반환한다.


re.findall("패턴", "문자열")


  • 다음은 문자열에서 숫자만 가져온다.


>>> re.findall("[0-9]+", "1 2 Fizz 4 Buzz Fizz 7 8")
['1', '2', '4', '7', '8']


*, +와 그룹 활용하기

  • 정규표현식에서 +*을 조합하여 사용할 때는 그룹으로 묶어서 사용한다.
  • (.[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 함수를 사용하며 패턴, 바꿀 문자열, 문자열, 바꿀 횟수를 넣어준다.
  • 여기서 바꿀 횟수를 넣으면 지정된 횟수만큼 바꾸며 바꿀 횟수를 생략하면 찾은 문자열을 모두 바꾼다.


re.sub("패턴", "바꿀문자열", "문자열", 바꿀횟수)


  • 다음은 문자열에서 "apple" 또는 "orange"를 찾아서 "fruit"로 바꾼다.


# apple 또는 orange를 fruit로 바꿈
>>> re.sub("apple|orange", "fruit", "apple box orange tree")
'fruit box fruit tree'


  • 다음은 숫자만 찾아서 "n"으로 바꾼다.


# 숫자만 찾아서 n으로 바꿈
>>> re.sub("[0-9]+", "n", "1 2 Fizz 4 Buzz Fizz 7 8")
'n n Fizz n Buzz Fizz n n'


  • sub 함수는 바꿀 문자열 대신 교체 함수를 지정할 수도 있다.
  • 교체 함수는 매개변수로 마치 객체를 받으며 바꿀 결과를 문자열로 반환하면 된다.


교체함수(매치객체)
re.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+)":을 기준으로 양 옆의 namejames를 찾는다.
  • 바꿀 문자열은 "<\\2>"와 같이 그룹 2 name과 그룹 3 james만 사용하고 그룹 1 "{ ", 그룹 4 "} "는 버린다.


001


  • 만약 그룹에 이름을 지었다면 \\g<이름> 형식으로 매칭된 문자열을 가져올 수 있다.


\\g<이름>
\\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<숫자> 형식처럼 \를 하나만 붙여서 사용할 수 있다.


r"\숫자 \g<이름> \g<숫자>"
>>> re.sub('({\s*)"(\w+)":\s*"(\w+)"(\s*})', r"<\2>\3</\2>", '{ "name": "james" }')
'<name>james</name>'

References