파이썬 | 정규표현식

CHOI·2022년 1월 13일
1

Python

목록 보기
33/33

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

1. 문자열

간단하게 문자열에 특정 문자열이 포함되어 있는지 판단해보자. 정규표현식은 re 모듈을 가져와서 사용하며 match 함수에 정규표현식 패턴과 문자열을 넣는다(re 는 regular experssion의 약자).

  • re.match('패턴', '문자열')

다음은 문자열에 ‘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’는 매치 객체가 반환되었지만 ‘Python’은 반환된게 없다.

사실 위와 같은 작업은 'Hello, world!'.find('Hello') 와 같이 문자열 메서드로도 충분히 할 수 있다. 이제부터는 문자열 메서드로는 못하는 작업을 해보자.

문자열 맨 앞/뒤 에 오는지 확인 ( ^ / $ )

정규표현식으로는 특정 문자열이 맨 앞에 오는지 맨 뒤에 오는지 확인할 수 있다.

문자열 앞에 ^ 를 붙이면 맨 앞에 오는지 확인하고 문자열 뒤에 $ 를 붙이면 문자열이 맨 뒤에 오는지 판단한다(특정 문자열로 끝나는지)

  • ^문자열
  • 문자열$

단, 이때는 match 대신 search 함수를 사용해야 한다. match 함수는 문자열 처음부터 매칭 되는지 확인하고 search 함수는 문자열 일부분이 매칭이 되는지 확인한다.

  • re.search('패턴', '문자열')

다음은 Hello 가 맨 앞에 오는지 판단하고 world 가 맨 마지막에 오는지 판단한다.

>>> re.search('^Hello', 'Hello, world!')     # Hello로 시작하므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 5), match='Hello'>
>>> re.search('world!$', 'Hello, world!')    # world!로 끝나므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(7, 13), match='world!'>

문자열 포함

| 는 특정 문자열에서 지정된 문자열(문자)일 하나라도 포함되는지 확인한다. 기본 개념은 OR 연산자와 같다.

  • 문자열|문자열
  • 문자열|문자열|문자열|문자열

'hello|word' 는 문자열에서 ‘hello’ 또는 ‘word’ 가 포함되는지를 판단한다.

>>> re.match('hello|world', 'hello')    # hello 또는 world가 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 5), match='hello'>

2. 범위 판단하기

이번에는 문자열이 숫자로 되어 있는지 판단해보겠다. 다음과 같이 [] 대괄호 안에 숫자 범위를 넣으면 * 또는 + 를 붙인다. 숫자 범위는 0-9 처럼 표현되며 * 는 문자(숫자)가 0 개 이상인지, + 는 1개 이상 있는지 판단한다.

  • [0-9]*
  • [0-9]+
>>> re.match('[0-9]*', '1234')    # 1234는 0부터 9까지 숫자가 0개 이상 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 4), match='1234'>
>>> re.match('[0-9]+', '1234')    # 1234는 0부터 9까지 숫자가 1개 이상 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 4), match='1234'>
>>> re.match('[0-9]+', 'abcd')    # abcd는 0부터 9까지 숫자가 1개 이상 없으므로 패턴에 매칭되지 않음

그럼 +* 는 어디에 사용될까? 다음과 같이 a+ba*b 를 확인해보면 알 수 있다.

>>> re.match('a*b', 'b')      # b에는 a가 0개 이상 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 1), match='b'>
>>> re.match('a+b', 'b')      # b에는 a가 1개 이상 없으므로 패턴에 매칭되지 않음
>>> re.match('a*b', 'aab')    # aab에는 a가 0개 이상 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 3), match='aab'>
>>> re.match('a+b', 'aab')    # aab에는 a가 1개 이상 있으므로 패턴에 매칭됨
<_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개 있는지 확인한다.

  • 문자?
  • [0-9]?
  • .
>>> re.match('abc?d', 'abd')         # abd에서 c 위치에 c가 0개 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 3), match='abd'>
>>> re.match('ab[0-9]?c', 'ab3c')    # [0-9] 위치에 숫자가 1개 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 4), match='ab3c'>
>>> re.match('ab.d', 'abxd')         # .이 있는 위치에 문자가 1개 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 4), match='abxd'>

문자 개수 판단 ( {시작개수, 끝개수} )

이번에는 특정 문자(숫자)가 정확하게 특정 몇 개 만 있는지 확인하는 것을 해보자. 이때는 문자 뒤에 {개수} 형식을 지정한다. 문자열의 경우에는 문자열을 괄호로 묶고 {개수} 형식으로 지정해준다.

  • 문자{개수}
  • (문자열){개수}

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]{개수}

다음은 휴대번호 형식인지를 확인한다.

>>> re.match('[0-9]{3}-[0-9]{4}-[0-9]{4}', '010-1000-1000')    # 숫자 3개-4개-4개 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 13), match='010-1000-1000'>
>>> re.match('[0-9]{3}-[0-9]{4}-[0-9]{4}', '010-1000-100')   # 숫자 3개-4개-4개 패턴에 매칭되지 않음

이 기능은 문자(숫자)의 개수 범위도 지정해줄 수 있다. {시작개수, 끝개수} 형식으로 시작 개수와 끝 개수를 지정해주면 된다.

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

다음은 일반번호 형식인지를 확인한다.

>>> re.match('[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}', '02-100-1000')    # 2~3개-3~4개-4개 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 11), match='02-100-1000'>
>>> re.match('[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}', '02-10-1000')  # 2~3개-3~4개-4개 패턴에 매칭되지 않음

숫자/영문 문자 조합해서 확인

지금까지는 숫자 범위만 사용했는데 숫자 범위와 영문 범위를 조합해서 확인해보자 영문 문자 범위는 a-z, A-Z 와 같이 표현한다.

>>> re.match('[a-zA-Z0-9]+', 'Hello1234')    # a부터 z, A부터 Z, 0부터 9까지 1개 이상 있으므로
<_sre.SRE_Match object; span=(0, 9), match='Hello1234'>                        # 패턴에 매칭됨
>>> re.match('[A-Z0-9]+', 'hello')    # 대문자, 숫자는 없고 소문자만 있으므로 패턴에 매칭되지 않음

이처럼 숫자, 영문 문자 범위는 a-zA-Z0-9 또는 A-Z0-9와 같이 붙여 쓰면 된다.

% 한글 범위

  • 가-힣
>>> re.match('[가-힣]+', '홍길동')    # 가부터 힣까지 1개 이상 있으므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 3), match='홍길동'>

특정 문자 범위를 제외

지금까지는 특정 문자 범위에 포함되는지 확인했는데 이번에는 반대로 특정 문자 범위에 포함되지 않는지를 판단해보자.

다음와 같이 문자(숫자)범위 앞에 ^ 를 붙이면 된다.

  • [^범위]*
  • [^범위]+

즉, [^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'>

특수 문자 확인

그런데 정규표현식에 사용하는 특수 문자 *, +, ?, ., ^, $, (, ) [, ], - 등을 판단하려면 어떻게 해야 할까? 특수 문자를 판단할 때에는 특수 문자 앞에 \ 를 붙이면 된다. 단, [] 안에서는 \ 를 안 붙여도 되지만 오류가 발생하는 경우에는 \ 를 붙인다.

  • \특수문자
>>> 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)'>

지금까지 범위를 지정할때 0-9a-zA-Z 와 같이 했다 그런데 이러한 방식으로 범위를 지정하면 정규표현식의 길이가 길어지고 복잡해진다. 단순히 숫자인지 문자인지를 판단할 때는 \d, \D, \w, \W 를 사용하면 편하다.

  • \d: [0-9]와 같음. 모든 숫자
  • \D: [^0-9]와 같음. 숫자를 제외한 모든 문자
  • \w: [a-zA-Z0-9_]와 같음. 영문 대소문자, 숫자, 밑줄 문자
  • \W: [^a-zA-Z0-9_]와 같음. 영문 대소문자, 숫자, 밑줄 문자를 제외한 모든 문자
>>> re.match('\d+', '1234')          # 모든 숫자이므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 4), match='1234'>
>>> re.match('\D+', 'Hello')         # 숫자를 제외한 모든 문자이므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 5), match='Hello'>
>>> re.match('\w+', 'Hello_1234')    # 영문 대소문자, 숫자, 밑줄 문자이므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 10), match='Hello_1234'>
>>> re.match('\W+', '(:)')    # 영문 대소문자, 숫자, 밑줄문자를 제외한 모든 문자이므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 3), match='(:)'>

공백 처리

이번에는 공백을 처리해보자. 공백은 ‘ ‘와 같이 공백 문자를 넣어도 되고 \s 또는 \S 로 표현할 수 있다.

  • \s: [ \t\n\r\f\v]와 같음. 공백(스페이스), \t(탭) \n(새 줄, 라인 피드), \r(캐리지 리턴), \f(폼피드), \v(수직 탭)을 포함
  • \S: [^ \t\n\r\f\v]와 같음. 공백을 제외하고 \t, \n, \r, \f, \v만 포함

% compile

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

객체 = re.compile('패턴')

객체.match('문자열')

객체.search('문자열')

>>> p = re.compile('[0-9]+')    # 정규표현식 패턴을 객체로 만듦
>>> p.match('1234')             # 정규표현식 패턴 객체에서 match 메서드 사용
<_sre.SRE_Match object; span=(0, 4), match='1234'>
>>> p.search('hello')           # 정규표현식 패턴 객체에서 search 메서드 사용

3. 그룹

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

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

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

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

  • 매치객체.group(그룹숫자)
>>> m = re.match('([0-9]+) ([0-9]+)', '10 295')
>>> m.group(1)    # 첫 번째 그룹(그룹 1)에 매칭된 문자열을 반환
'10'
>>> m.group(2)    # 두 번째 그룹(그룹 2)에 매칭된 문자열을 반환
'295'
>>> m.group()     # 매칭된 문자열을 한꺼번에 반환
'10 295'
>>> m.group(0)    # 매칭된 문자열을 한꺼번에 반환
'10 295'

매치 객체의 group 메서드에 숫자를 지정하면 해당 그룹에 매칭된 문자열을 반환한다. 만약 숫자를 지정하지 않거나 0을 지정하면 매칭된 그룹 전체를 반환한다.

그리고 groups 메서드는 각 그룹에 해당하는 문자열을 튜플로 반환한다.

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

그룹에 숫자가 많아지면 그룹을 숫자로 구분하기 힘들어진다. 이때는 그룹에 이름을 지정하면 된다.그룹 이름은 () 괄호 안에 ?P<이름> 형식으로 지정한다.

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

다음 소스코드는 print(1234) 에서 print1234 를 추출한다.

>>> 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] 로 하였고 () 는 정규표현식에서 사용하는 특수문자이기 때문에 \ 를 앞에 붙여주었다.

패턴에 매칭되는 문자열

이번에는 그룹지정 없이 패턴에 매칭되는 문자열을 가지고 와보자. 이럴때에는 finall 함수를 사용하여 매칭된 문자열을 리스트로 반환한다.

  • re.findall('패턴', '문자열')

다음은 문자열에서 숫자만 가지고 온다.

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

% 그룹 사용하기 with + *

정규 표현식에서 +* 를 조합하여 사용할때는 그룹으로 묶어서 사용한다. (.[a-z]+)* 는 점과 영문 소문자가 1개 이상인지를 판단하고 이것 자체가 0개 이상인지를 판단한다. 즉, 규칙을 반드시 지켜야 하지만 있어도 되고 없어도 상관없을 때 사용한다.

>>> re.match('[a-z]+(.[a-z]+)*$', 'hello.world')    # .world는 문자열이므로 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 11), match='hello.world'>
>>> re.match('[a-z]+(.[a-z]+)*$', 'hello.1234')     # .1234는 숫자이므로 패턴에 매칭되지 않음
>>> re.match('[a-z]+(.[a-z]+)*$', 'hello')          # .뒤에 문자열이 없어도 패턴에 매칭됨
<_sre.SRE_Match object; span=(0, 5), match='hello'>

4. 문자열 바꾸기

이번에는 정규표현식에서 특정 문자열을 찾은 뒤에 다른 문자열로 바꾸는 방법에 대해서 알아보자.

이때는 sub 함수를 사용한다.

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

바꿀횟수를 생략하면 찾은 문자열을 모두 바꾼다.

다음 문자열은 ‘apple’ 과 ‘orange’를 ‘fruit’로 바꾼다.

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

또는 문자열에서 숫자를 찾아서 다른 문자로 바꿀 수 있다.

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

sub 함수는 바꿀 문자열 대신 교체 함수를 지정할 수 있다. 교체 함수는 매개변수로 매치 객체를 받으며 바꿀 결과를 문자열로 반환하면 된다. 다음은 문자열에서 숫자를 찾은 뒤 숫자를 10배로 만든다.

  • 교체함수(매치객체)
  • re.sub('패턴', 교체함수, '문자열', 바꿀횟수)
>>> 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'

교체함수의 내용이 간단하다면 람다 표현식을 만들어서 넣어도 된다.

찾은 문자열 결과 다시 사용하기

이번에는 정규표현식을 통해 찾은 문자열을 다시 가져와서 사용해보자. 먼저 정규 표현식을 그룹으로 묶어준다. 그러고 나면 바꿀 문자열에서 \\숫자 형식으로 매칭되는 문자열을 가져올 수 있다.

다음은 ‘hello 1234’ 에서 ‘hello’ 는 1 그룹, ‘1234’ 는 2 그룹으로 찾은 뒤에 2, 1, 2, 1 순서대로 문자열의 순서를 바꿔서 출력한다.

>>> re.sub('([a-z]+) ([0-9]+)', '\\2 \\1 \\2 \\1', 'hello 1234')    # 그룹 2, 1, 2, 1 순으로 바꿈
'1234 hello 1234 hello'

이번에는 좀 더 응용해보자 다음은 '{ "name": "james" }''<name>james</name>' 형식으로 바꾼다.

>>> re.sub('({\s*)"(\w+)":\s*"(\w+)"(\s*})', '<\\2>\\3</\\2>', '{ "name": "james" }')
'<name>james</name>'

보기에는 외계어 처럼 보이지만 부분부분 나눠서 보면 그리 어렵지 않다.

만약 그룹을 그룹 이름으로 지었다면 \\g<이름> 형식으로 매칭된 문자열을 가져올 수 있다(\\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>'
💡 지금까지 정규 표현식에 대해서 배웠다 워낙 특수문자가 많고 복잡해 보여서 많은 사람들이 어려워하는 부분이다. 지금 당장 모든것을 외울 필요가 없다 필요할 때 다시 와서 보면 된다.
profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

0개의 댓글