본문 바로가기

Computer/Python

정규표현식을 이용해 문자열에서 숫자 찾기

반응형

최근에 급히 여러 줄로 이루어진 텍스트로부터 각 줄마다 숫자를 찾아야 할 일이 있었습니다. 텍스트 파일 형식은 xml과 유사한 처음 보는 파일 형식으로 원래는 관련 파이썬 라이브러리 (BeautifulSoup, xml 등등..)를 이용해 해결하고자 했으나 잘 안되더군요. 마음이 급한 와중에 생각한 것이 텍스트 파일의 규격이 일정하기에 정규표현식 (regular expression)을 이용하면 어떨까 생각해서 급히 stack overflow를 뒤져보던 중 다음과 같은 깔끔한 코드 스니펫을 발견했습니다.

import re

# Format is [(<string>, <expected output>), ...]
ss = [("apple-12.34 ba33na fanc-14.23e-2yapple+45e5+67.56E+3",
       ['-12.34', '33', '-14.23e-2', '+45e5', '+67.56E+3']),
      ('hello X42 I\'m a Y-32.35 string Z30',
       ['42', '-32.35', '30']),
      ('he33llo 42 I\'m a 32 string -30', 
       ['33', '42', '32', '-30']),
      ('h3110 23 cat 444.4 rabbit 11 2 dog', 
       ['3110', '23', '444.4', '11', '2']),
      ('hello 12 hi 89', 
       ['12', '89']),
      ('4', 
       ['4']),
      ('I like 74,600 commas not,500', 
       ['74,600', '500']),
      ('I like bad math 1+2=.001', 
       ['1', '+2', '.001'])]

for s, r in ss:
    rr = re.findall("[-+]?[.]?[\d]+(?:,\d\d\d)*[\.]?\d*(?:[eE][-+]?\d+)?", s)
    if rr == r:
        print('GOOD')
    else:
        print('WRONG', rr, 'should be', r)

 

일단 위 코드를 사용하여 급한 불은 껐는데, 정확히 원리를 모르고 사용하는 것이 찜찜하여 파이썬 정규표현식 복습 차 이번 포스트를 작성하게 되었습니다. 정규표현식의 기초적인 내용은 점프투파이썬을 참고하기로 하고 이제 다음 구문을 해석해봅시다.

"[-+]?[.]?[\d]+(?:,\d\d\d)*[\.]?\d*(?:[eE][-+]?\d)?"

먼저 "[]"은 정규표현식의 문자 클래스로 [] 안에 들어가는 문자들과의 매치를 나타냅니다. "*"와 "+"은 반복인데, "*"은 0번을 포함한 제한없는 반복, "+"은 1회 이상 반복을 나타냅니다. 또한 "\d"는 [0-9] 사이의 정수를 나타내죠. 이걸 기반으로 봤을 때 "[-+]?[.]?[\d]+"는 1) "[-+]?": "-+"가 있던 없던 , 2) "[.]": "."이 있던 없던, 3) "[\d]+" [0-9] 사이의 정수가 반복되는 문자열 찾는다고 해석할 수 있습니다. 실제로 "[-+]?[.]?[\d]+"을 이용해 re.findall 함수를 돌리면 다음과 같은 결과가 나옵니다.

apple-12.34 ba33na fanc-14.23e-2yapple+45e5+67.56E+3
WRONG ['-12', '.34', '33', '-14', '.23', '-2', '+45', '5', '+67', '.56', '+3'] should be ['-12.34', '33', '-14.23e-2', '+45e5', '+67.56E+3']
hello X42 I'm a Y-32.35 string Z30
WRONG ['42', '-32', '.35', '30'] should be ['42', '-32.35', '30']
he33llo 42 I'm a 32 string -30
GOOD
h3110 23 cat 444.4 rabbit 11 2 dog
WRONG ['3110', '23', '444', '.4', '11', '2'] should be ['3110', '23', '444.4', '11', '2']
hello 12 hi 89
GOOD
4
GOOD
I like 74,600 commas not,500
WRONG ['74', '600', '500'] should be ['74,600', '500']
I like bad math 1+2=.001
GOOD

위 결과를 보면 뭔가 연속된 숫자를 찾았지만 소수점과 76,000 같이 컴마로 구분된 숫자는 찾지 못했습니다. 먼저 컴마로 구분된 숫자를 매치시키기 위해서는 "(?:,\d\d\d)*" 구문을 추가하면 됩니다. 정규표현식에서 "()"은 그룹으로 묶는 것을 말하며, "(?:" 뒷부분의 정규표현식 (여기서는 ",\d\d\d")과 매치되는 것을 찾습니다. 실은 이 구문은 "[,\d\d\d]"로 바꾸어도 동작하는데, "?:"의 의미는 그룹을 사용하여 정규식의 일부를 나타내고 싶지만 그룹의 내용을 꺼내는 데에는 관심이 없을 때 사용합니다. 즉, 지금처럼 findall 함수를 이용해 매치되는 구간을 조회할 때에는 사용되지만 match, search 등 매치된 모든 문자열을 반환할 때는 해당 그룹의 내용은 반환되지 않는 거죠. (자세하게는 비포착그룹이라고 하는데, 이 글을 참조하시길 바랍니다) 그럼 "[-+]?[.]?[\d]+[,\d\d\d]*" 표현을 이용하여 숫자를 찾아보겠습니다.

apple-12.34 ba33na fanc-14.23e-2yapple+45e5+67.56E+3
WRONG ['-12', '.34', '33', '-14', '.23', '-2', '+45', '5', '+67', '.56', '+3'] should be ['-12.34', '33', '-14.23e-2', '+45e5', '+67.56E+3']
hello X42 I'm a Y-32.35 string Z30
WRONG ['42', '-32', '.35', '30'] should be ['42', '-32.35', '30']
he33llo 42 I'm a 32 string -30
GOOD
h3110 23 cat 444.4 rabbit 11 2 dog
WRONG ['3110', '23', '444', '.4', '11', '2'] should be ['3110', '23', '444.4', '11', '2']
hello 12 hi 89
GOOD
4
GOOD
I like 74,600 commas not,500
GOOD
I like bad math 1+2=.001
GOOD

이제 컴마로 이루어진 숫자는 해결했고 소수를 해결할 차례입니다. 소수는 점 "." 으로 시작하니 "[.]?" 으로 표현하여 점 (".")이 있거나 없거나를 나타내주고 이후에는 \d를 반복해서 붙이면 됩니다. 그러면 "[.]?[\d]*" 를 붙여 최종적으로는 "[-+]?[.]?[\d]+(?:,\d\d\d)*[\.]?\d*" 형태가 되겠죠.

apple-12.34 ba33na fanc-14.23e-2yapple+45e5+67.56E+3
WRONG ['-12.34', '33', '-14.23', '-2', '+45', '5', '+67.56', '+3'] should be ['-12.34', '33', '-14.23e-2', '+45e5', '+67.56E+3']
hello X42 I'm a Y-32.35 string Z30
GOOD
he33llo 42 I'm a 32 string -30
GOOD
h3110 23 cat 444.4 rabbit 11 2 dog
GOOD
hello 12 hi 89
GOOD
4
GOOD
I like 74,600 commas not,500
GOOD
I like bad math 1+2=.001
GOOD

거의 다 왔습니다. 나머지는 매우 큰 수를 e나 E로 표현하는 부동소수점입니다. 이것도 그룹핑을 이용하여 "()" 안에 구문을 묶고 1) "[eE]": e나 E가 있고, 2) "[-+]?": "-"나 "+"가 있거나 없거나, 3) "\d+": 정수가 1회 이상 반복되는 것을 나타내면 "(?:[eE][-+]?\d+)"로 나타낼 수 있습니다. 따라서 아까 소개드린 다음과 같은 형태가 되는 것이죠.

"[-+]?[.]?[\d]+(?:,\d\d\d)*[\.]?\d*(?:[eE][-+]?\d+)?"

 

참조

반응형

'Computer > Python' 카테고리의 다른 글

최소 지식의 원칙과 클래스 메소드  (0) 2022.07.17
파이썬 dataclasses 표준 라이브러리  (3) 2022.03.31
Property와 descriptor (디스크립터)  (0) 2021.08.27
ModuleNotFoundError 와 ImportError  (0) 2021.08.26
Vectorization  (0) 2021.08.14