Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-16 04:51
관리 메뉴

nomad-programmer

[Programming/Python] 정규표현식 (regular expression) 본문

Programming/Python

[Programming/Python] 정규표현식 (regular expression)

scii 2023. 2. 1. 01:54

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

문자열 판단하기

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

  • re.match('패턴', '문자열')
import re
re.match('hello', 'hello, world') # 문자열이 있으므로 정규표현식 매치 객체 반환
<re.Match object; span=(0, 5), match='hello'>

re.match('python', 'hello, world') # 문자열이 없으므로 아무것도 반환되지 않음

문자열이 맨 앞에 오는지 맨 뒤에 오는지 판단하기

정규표현식은 특정 문자열이 맨 앞에 오는지 맨 뒤에 오는지 판단할 수 있다. 문자열 앞에 ^을 붙이면 문자열이 맨 앞에 오는지 판단하고 문자열 뒤에 $를 붙이면 문자열이 맨 뒤에 오는지 판단한다(특정 문자열로 끝나는지).

  • ^문자열
  • 문자열$

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

  • re.search('패턴', '문자열')
re.search('^hello', 'hello, world')
re.search('world$', 'hello, world')

지정된 문자열이 하나라도 포함되는지 판단하기

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

  • 문자열|문자열
  • 문자열|문자열|문자열
re.match('hello|world', 'hello') # hello 또는 world가 있으므로 패턴 매칭 성공

범위 판단하기

[](대괄호) 안에 숫자 범위를 넣으며 * 또는 + 를 붙인다. 숫자 범위는 0-9처럼 표현하며 *는 문자(숫자)가 0개 이상 있는지, +는 1개 이상 있는지 판단한다.

  • [0-9]*
  • [0-9]+
re.match('[0-9]*', '1234') # 1234는 0부터 9까지 숫자가 0개 이상 있으므로 패턴 매칭 성공
re.match('[0-9]+', '1234') # 1234는 0부터 9까지 숫자가 1개 이상 있으므로 패턴 매칭 성공

그럼 *와 +는 어디에 활용할까? 다음과 같이 a*b와 a+b를 확인해보면 쉽게 알 수 있다.

re.match('a*b', 'b') # b에는 a가 0개 이상 있으므로 패턴 매칭 성공
re.match('a+b', 'b') # b에는 a가 1개 이상 없으므로 패턴 매칭 실패
re.match('a*b', 'aab') # aab에는 a가 0개 이상 있으므로 패턴 매칭 성공
re.match('a+b', 'aab') # aab에는 a개 1개 이상 있으므로 패틴 매칭 성공

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

문자가 여러 개 있는 판단할 때는 *와 +를 사용했지만, 문자가 한 개만 있는지 판단할 때는 ?와 .을 사용한다. ?는 문자 0개 또는 1개인지 판단하고 .는 문자 1개인지 판단한다.

  • 문자?
  • 문자.
re.match('H?', 'H') # H 뒤에 문자가 0개 또는 1개이므로 패턴 매칭 성공
re.match('H?', 'Hi') # H 뒤에 문자가 0개 또는 1개이므로 패턴 매칭 성공
re.match('H.', 'Hi') # H 뒤에 문자가 1개 있으므로 패턴 매칭 성공

문자 개수 판단하기

문자(숫자)가 정확히 몇 개 있는지 판단하고 싶을 때가 있다. 이때는 문자 뒤에 {개수} 형식을 지정한다. 문자열의 경우에는 문자열을 괄호로 묶고 뒤에 {개수} 형식을 지정한다.

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

h{3}은 h가 3개 있는지판단하고, (hello){3} 은 hello가 3개 있는지 판단한다.

re.match('h{3}', 'hhhello')
re.match('(hello){3}', 'hellohellohelloworld')

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

  • [0-9]{개수}

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

re.match('[0-9]{3}-[0-9]{4}-[0-9]{4}', '010-1000-1000') # 숫자 3개-4개-4개, 패턴 매칭 성공
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개, 패턴 매칭 성공
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와 같이 표현한다.

  • a-z
  • A-Z
re.match('[a-zA-Z0-9]+', 'Hello1234') # 패턴 매칭 성공
re.match('[A-Z0-9]+', 'hello') # 패턴 매칭 실패

그럼 한글은 어떻게 사용할까? 영문 문자와 방법이 같다. 가-힣처럼 나올 수 있는 한글 조합을 정해주면 된다.

  • 가-힣
re.match('[가-힣]+', '홍길동') # 패턴 매칭 성공

특정 문자 범위에 포함되지 않는지 판단하기

특정 문자 범위에 포함되지 않는지 판단하려면 다음과 같이 문자(숫자) 범위 앞에 ^을 붙이면 해당 범위를 제외한다.

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

즉, '[^A-Z]+' 는 대문자를 제외한 모든 문자(숫자)가 1개 이상 있는지 판단한다.

re.match('[^A-Z]+', 'Hello') # 대문자가 있으므로 패턴 매칭 실패
re.match('[^A-Z]+', 'hello') # 대문자가 없으므로 패턴 매칭 성공

특정 문자열로 시작하는지 판단할 때도 ^을 사용했었는데 문법이 비슷해서 이 부분은 헷갈리기 쉽다. 범위를 제외할 때는 '[^A-Z]+' 와 같이 [ ] 안에 넣어주고, 특정 문자 범위로 시작할 때는 '^[A-Z]+' 와 같이 [ ] 앞에 붙여준다. 그래서 다음과 같이 '^[A-Z]+' 는 영문 대문자로 시작하는지 판단한다.

  • ^[범위]*
  • ^[범위]+
re.search('^[A-Z]+', 'Hello') # 대문자로 시작하므로 패턴 매칭 성공

물론 특정 문자(숫자) 범위로 끝나는지 확인할 때9는 정규표현식 뒤에 $를 붙이면 된다.

  • [범위]*$
  • [범위]+$
re.search('[0-9]+$', 'Hello1234') # 숫자로 끝나므로 패턴 매칭 성공

특수 문자 판단하기

특수 문자를 판단할 때는 특수 문자 앞에 \를 붙이면 된다. 단, [ ] 안에서는 \를 붙이지 않아도 되지만 에러가 발생하는 경우에는 \를 붙인다.

re.search('\*+', '1 ** 2') # *가 들어있는지 판단
re.match('[$()a-zA-Z0-9]+', '$(document)') # $, (, )와 문자, 숫자가 들어있는지 판단

지금까지 범위를 지정하면서 a-zA-Z0-9 처럼 대소문자와 숫자를 모두 나열했다. 이런 방식으로 범위를 정하면 정규표현식이 길어지고 복잡해진다. 단순히 숫자인지 문자인지 판단할 때는 \d, \D, \w, \W를 사용하면 편리하다.

  • \d: [0-9]와 같음. 모든 숫자
  • \D: [^0-9]와 같음. 숫자를 제외한 모든 문자
  • \w: [a-zA-Z0-9_]와 같음. 영문 대소문자. 숫자, 밑줄 문자
  • \W: [^a-zA-Z0-9_]와 같음. 영문 대소문자, 숫자, 밑줄 문자를 제외한 모든 문자
re.match('\d+', '1234') # 모든 숫자이므로 패턴 매칭 성공
re.match('\D+', 'Hello') # 숫자를 제외한 모든 문자이므로 패턴 매칭 성공
re.match('\w+', 'Hello_1234') # 영문 대소문자, 숫자, 밑줄 문자이므로 패턴 매칭 성공
re.match('\W+', '(:)') # 영문 대소문자, 숫자, 밑줄 문자를 제외한 모든 문자이므로 패턴 매칭 성공

공백 처리하기

공백은 ' ' 처럼 공백 문자를 넣어도 되고, \s 또는 \S로 표현할 수도 있다.

  • \s: [\t\n\r\f\v]와 같음.
  • \S: [^\t\n\r\f\v]와 같음.
re.match('[a-zA-Z0-9 ]+', 'hello 1234') # ' ' 로 공백 표현
re.match('[a-zA-Z0-9\s]+', 'hello 1234') # \s로 공백 표현

같은 정규표현식 패턴을 자주 사용할 때 효율적인 방법

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

  • 객체 = re.compile('패턴')
  • 객체.match('문자열')
  • 객체.search('문자열')
p = re.compile('[0-9]+') # 정규표현식 패턴을 객체로 만듦
p.match('1234')
p.search('hello')

그룹 사용하는 방법

정규표현식 그룹은 해당 그룹과 일치하는 문자열을 얻어올 때 사용한다. 패턴 안에서 정규표현식을 ( )로 묶으면 그룹이 된다.

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

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

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

매치 객체의 group 메서드에 숫자를 지정하면 해당 그룹에 매칭된 문자열을 반환한다. 숫자를 지정하지 않거나 0을 지정하면 매칭된 문자열을 한꺼번에 반환한다. 그리고 groups 메서드는 각 그룹에 해당하는 문자열을 튜플로 반환한다.

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

그룹 개수가 많아지면 숫자로 그룹을 구분하기가 힘들어진다. 이때는 그룹에 이름을 지으면 편리하다. 그룹의 이름은 ( )안에 ?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 뒤에 붙은 (, )는 정규표현식에 사용하는 특수 문자이므로 앞에 \를 붙여준다.

패턴에 매칭되는 모든 문자열 가져오기

그룹 지정 없이 패턴에 매칭되는 모든 문자열을 가져오려면 findall 함수를 사용하여 매칭된 문자열을 리스트로 받을 수 있다.

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

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

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

*, +와 그룹 활용하기

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

re.match('[a-z]+(.[a-z]+)*$', 'hello.world') # .world는 문자열이므로 패턴 매칭 성공
re.match('[a-z]+(.[a-z]+)*$', 'hello.1234') # .1234는 숫자이므로 패턴 매칭 실패
re.match('[a-z]+(.[a-z]+)*$', 'hello') # . 뒤에 문자열이 없어도 패턴 매칭 성공

문자열 바꾸기

정규표현식으로 특정 문자열을 찾은 뒤 다른 문자열로 바꾸는 방법에 대해 알아보자. 문자열을 바꿀 때는 sub 함수를 사용하며 패턴, 바꿀 문자열, 문자열, 바꿀 횟수를 넣어준다. 바꿀 횟수를 넣으면 지정된 횟수만큼 바꾸며 바꿀 횟수를 생략하면 찾은 문자열을 모두 바꾼다.

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

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

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

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

  • 교체함수(매치객체)
  • re.sub('패턴', 교체함수, '문자열', 바꿀횟수)
def multiple10(m): # 매개변수로 매치 객체를 받음
    a = 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'

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

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

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

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

  • \\숫자

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

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