코드 보기
import re
text = "It is true that string cleaning is a topic in this chapter. string editing is another."
re.findall("string \w+\s", text)['string cleaning ', 'string editing ']
이 장에서는 정규 표현식을 사용하여 텍스트를 처리하는 방법을 다룹니다.
정규 표현식(Regex)은 텍스트를 검색하고 변경하는 방법을 제공합니다. 장점은 간결하고 실행 속도가 매우 빠르며, 여러 언어에 이식 가능하다는 점입니다(절대 파이썬만의 전유물이 아닙니다!). 또한 매우 강력합니다. 단점은 혼란스럽고 익숙해지는 데 시간이 걸린다는 점입니다!

정규 표현식 코드를 실시간으로 테스트해 볼 수 있는 곳이 몇 군데 있습니다. 첫 번째는 Visual Studio Code 자체입니다. 왼쪽 패널 옵션에서 돋보기 아이콘을 클릭하여 이를 수행할 수 있습니다. 검색창이 나타나면 검색어를 입력할 수 있습니다. 텍스트 입력란 오른쪽에 세 개의 버튼이 있는데, 그중 하나가 마침표(.) 뒤에 별표(*)가 붙은 모양입니다. 이 옵션을 선택하면 Visual Studio 텍스트 검색 기능에서 정규 표현식을 사용할 수 있게 됩니다. 이는 현재 Visual Studio 워크스페이스의 모든 텍스트에 정규 표현식을 적용합니다.
또 다른 방법은 https://regex101.com/ 또는 https://regexr.com/에 접속하여 정규 표현식을 직접 입력해 보는 것입니다(regexr의 치트 시트와 참조 패턴도 확인해 볼 가치가 있습니다). 정규 표현식을 적용할 대상 텍스트를 상자에 추가해야 합니다.
위의 방법 중 하나로 string \w+\s라는 정규 표현식을 시도해 보세요. 이 식은 ’string’이라는 단어 뒤에 또 다른 단어가 오고 그 뒤에 공백이 오는 모든 경우를 매칭합니다. 예로 ’string cleaning ’이라는 텍스트가 이 정규 표현식에 의해 매칭 결과로 선택될 것입니다.
파이썬 내에서는 re 라이브러리가 정규 표현식을 지원합니다. 한번 시도해 보겠습니다:
import re
text = "It is true that string cleaning is a topic in this chapter. string editing is another."
re.findall("string \w+\s", text)['string cleaning ', 'string editing ']
re.findall()은 모든 일치 항목을 반환합니다. re에는 re.function(regex, text)와 유사한 구문을 가진 유용한 검색 기능들이 여러 개 있습니다. 아래 표는 그 기능들이 무슨 일을 하는지 보여줍니다.
| 함수 | 기능 | 사용 예시 | 주어진 text 값에 대한 출력 |
|---|---|---|---|
re.match() |
문자열의 시작 부분에서 일치 항목이 있는지 확인합니다. | re.match("string \w+\s" , text) is True |
None |
re.search() |
문자열의 어느 곳에든 일치 항목이 있는지 확인합니다. | re.search("string \w+\s" , text) is True |
True |
re.findall() |
모든 일치 항목을 반환합니다. | re.findall("string \w+\s" , text) |
['string cleaning ', 'string editing '] |
re.split() |
일치 항목이 발생할 때마다 텍스트를 분할합니다. | re.split("string \w+\s" , text) |
['It is true that ', 'is a topic in this chapter. ', 'is another.'] |
또 다른 유용한 정규 표현식 함수는 re.sub()입니다. 이 함수는 일치 항목을 찾으면 한 텍스트 조각을 다른 것으로 대체합니다. 예제입니다:
new_text = "new text here! "
re.sub("string \w+\s", new_text, text)'It is true that new text here! is a topic in this chapter. new text here! is another.'
지금까지 우리는 평범한 단어인 string, 다른 단어를 뜻하는 코드 \w+, 그리고 공백을 뜻하는 코드 \s가 포함된 아주 간단한 정규 표현식 적용 사례만 보았습니다. 이제 정규 표현식 특수 문자들을 더 자세히 살펴보겠습니다:
| 문자 | 설명 | 예제 텍스트 | 예제 정규 표현식 | 예제 매칭 텍스트 |
|---|---|---|---|---|
| 모든 스크립트에서의 유니코드 숫자 하나 | “file_93 is open” | file_\d\d |
“file_93” | |
| “단어 문자”: 유니코드 문자, 숫자 또는 언더스코어 | “blah hello-word blah” | \w-\w |
“hello-world” | |
| “공백 문자”: 모든 유니코드 구분자 | “these are some words with spaces” | words\swith\sspaces |
“words with spaces” | |
| 숫자가 아닌 문자 (반대) | “ABC 10323982328” | \D\D\D |
“ABC” | |
| 단어 문자가 아닌 문자 (반대) | “Once upon a time *” | \W |
“*” | |
| 공백 문자가 아닌 문자 (반대) | “y” | \S |
“y” | |
| 문자열의 끝 | “End of a string” | \w+\Z |
“string”” | |
| . | 줄바꿈을 제외한 모든 문자와 일치 | “ab=def” | ab.def |
“ab=def” |
공백 문자에는 줄바꿈 \n과 탭 \t이 포함된다는 점에 유의하세요.
이러한 특수 문자 외에도, 한 문자가 한 번 이상 나타나도록 요청하는 수량자가 있습니다. 예를 들어 위에서 \w\w는 두 개의 단어 문자를 요청했고, \d\d는 두 개의 숫자를 요청했습니다. 다음 표는 모든 수량자를 보여줍니다.
| 수량자 | 역할 | 예제 텍스트 | 예제 정규 표현식 | 예제 매칭 |
|---|---|---|---|---|
| {m} | 정확히 m번 반복 | “936 and 42 are the codes” | \d{3} |
“936” |
| {m,n} | m번(기본값 0)부터 n번(기본값 무한대)까지 | “Words up to four letters” | \b\w{1,4}\b |
“up”, “to”, “four” |
| * | 0번 이상. {,}와 동일 | “42 is the code” | \d*\s |
“42” |
| + | 1번 이상. {1,}과 동일 | “4 323 hello” | \d+ |
“4”, “323” |
| ? | 선택 사항, 즉 0번 또는 1번. {,1}과 동일 | “4 323 hello” | \d?\s |
“4” |
{.callout-note} 연습 문제 "Inflation in year 3 was 2 percent"와 "Interest rates were as high as 12 percent" 모두에서 퍼센트 숫자만 골라내는 단일 정규 표현식을 찾아보세요.
이제 특수 문자와 수량자 외에도 메타 문자 매칭이 있습니다. 이것들은 문자 그 자체라기보다는 단어(\w+)의 시작, 끝, 그리고 다른 부분들을 나타냅니다. 예를 들어 \b는 단어 경계에서의 문자열과 일치하므로, “Three letter words only are captured”라는 텍스트에 \b\w\w\w\b를 적용하면 “are”를 반환할 것입니다. \B는 단어 경계가 아닌 곳에서의 문자열과 일치하므로 “Bricks”라는 텍스트에 \B\w\w\B를 적용하면 “ri”를 산출할 것입니다. 다음 표에는 몇 가지 유용한 메타 문자들이 포함되어 있습니다.
| 메타 문자 시퀀스 | 의미 | 예제 정규 표현식 | 예제 매칭 |
|---|---|---|---|
| ^ | 문자열이나 줄의 시작 | ^abc |
“abc” (문자열이나 줄의 시작 부분에 나타날 때) |
| $ | 문자열이나 줄의 끝 | xyz$ |
“xyz” (문자열이나 줄의 끝 부분에 나타날 때) |
| 단어(+) 경계에서의 문자열 일치 | ing\b |
“matching” (단어 끝에 ing가 올 때 일치) | |
| 단어(+) 경계가 아닌 곳에서의 문자열 일치 | \Bing\B |
“stinger” (단어의 시작이나 끝이 아닌 곳에 ing가 올 때 일치) |
정규 표현식에서 많은 문자가 특별한 의미를 갖기 때문에, 예를 들어 달러 기호나 마침표를 찾고 싶다면 백슬래시를 사용하여 해당 문자를 먼저 이스케이프(escape)해야 합니다. 따라서 \${1}\d+는 단일 달러 기호 뒤에 숫자가 오는 경우를 찾으며 ‘she made $50 dollars’에서’$50’을 찾아낼 것입니다.
{.callout-note} 연습 문제 다음 문장에서 'money'라는 단어의 첫 번째 사례와 'money' 다음에 오는 단어만 골라내는 정규 표현식을 찾아보세요: "money supply has grown considerably. money demand has not kept up.".
이제 정규 표현식이 끝났다고 생각하시겠지만, 아직 더 남았습니다! 이번에는 문자들의 범위를 나타내는 메타 문자들을 보겠습니다.
| 메타 문자 시퀀스 | 설명 | 예제 표현식 | 예제 매칭 |
|---|---|---|---|
| [문자들] | 대괄호 안의 문자들은 매칭 문자 집합의 일부입니다 | [abcd] |
a, b, c, d, abcd |
| [^…] | 대괄호 안의 문자들은 비매칭 집합입니다. 안에 없는 문자가 매칭 문자입니다. | [^abcd] |
a, b, c, d를 제외한 모든 문자의 발생. |
| [문자-문자] | 두 문자 사이의 범위에 있는 모든 문자가 집합의 일부입니다 | [a-z] |
모든 소문자 |
| [^문자] | 나열된 문자가 아닌 모든 문자 | [^A] |
대문자 A를 제외한 모든 문자 |
범위에는 두 가지 멋진 요령이 더 있습니다. 첫 번째는 범위들을 연결할 수 있다는 것입니다. 예를 들어 [a-c-1-5]는 a, b, c, 1, 2, 3, 4, 5 중 어느 것과도 일치합니다. 또한 수량자로 수정할 수도 있으므로 [a-c0-2]{2}는 “a0” 및 “ab”와 일치합니다.
이 부분은 이해하기가 조금 까다로우니 정신 바짝 차리세요. 정규 표현식 뒤에 ?를 추가하면 ‘탐욕적’인 방식에서 ’게으른’ 방식으로 바뀝니다. 탐욕적이라는 것은 조건을 충족하는 가장 긴 문자열을 매칭한다는 뜻입니다. 게으른이라는 것은 조건을 충족하는 가장 짧은 문자열을 얻게 된다는 뜻입니다. 예제를 통해 확인하는 것이 가장 쉽습니다:
test_string = "stackoverflow"
greedy_regex = "s.*o"
lazy_regex = "s.*?o"
print(f"탐욕적 매칭 결과: {re.findall(greedy_regex, test_string)[0]}")
print(f"게으른 매칭 결과: {re.findall(lazy_regex, test_string)[0]}")탐욕적 매칭 결과: stackoverflo
게으른 매칭 결과: stacko
전자의 경우(탐욕적), ‘s’부터 동일한 단어 내의 마지막 ’o’까지 모두 가져옵니다. 후자의 경우(게으른), 시작 지점부터 처음으로 나타나는 ’o’ 사이의 모든 것만 가져옵니다.
매칭하고 싶은 내용과 정규 표현식으로 실제로 가져오고 싶은 내용 사이에는 차이가 있는 경우가 많습니다. 예를 들어 어떤 텍스트를 파싱하고 있는데 ‘$xx.xx’ 형식(여기서 ’x’는 숫자)을 따르는 숫자만 원하고 달러 기호는 원하지 않는다고 가정해 봅시다. 이를 위해 괄호를 사용하여 캡처 그룹을 만들 수 있습니다. 예제입니다:
text = "Product 1 was $45.34, while product 2 came in at $50.00 however it was assessed that the $4.66 difference did not make up for the higher quality of product 2."
re.findall("\$(\d{2}.\d{2})", text)['45.34', '50.00']
여기서 정규 표현식을 분석해 보겠습니다. 먼저 \$를 사용하여 문자 그대로의 달러 기호를 요청했습니다. 다음으로 (를 사용하여 캡처 그룹을 열었습니다. 그런 다음 숫자 두 개, 마침표 하나, 그리고 또 다른 숫자 두 개만 달라고 했습니다(따라서 $4.66은 제외됩니다). 마지막으로 )로 캡처 그룹을 닫았습니다.
즉, 정규 표현식을 사용하여 일치 항목을 지정하는 동시에, 정규 표현식을 실행할 때는 캡처 그룹만 반환하기를 원하는 것입니다.
더 복잡한 예제를 살펴보겠습니다.
sal_r_per = r"\b([0-9]{1,6}(?:\.)?(?:[0-9]{1,2})?(?:\s?-\s?|\s?to\s?)[0-9]{1,6}(?:\.)?(?:[0-9]{1,2})?)(?:\s?per)\b"
text = "This job pays gbp 30500.00 to 35000 per year. Apply at number 100 per the below address."
re.findall(sal_r_per, text)['30500.00 to 35000']
이 경우 정규 표현식은 먼저 최대 6자리 숫자를 찾고, 선택적으로 마침표, 선택적으로 또 다른 한두 자리 숫자, 그 뒤에 ‘|’ 연산자(또는 이라는 뜻)를 사용한 대시(-)나 ‘to’, 그 뒤에 유사한 숫자, 마지막으로 ’per’가 이어지는 패턴을 찾습니다.
하지만 캡처 그룹은 일치 항목 중 숫자 범위에 해당하는 부분 집합뿐이며 나머지는 대부분 버립니다. 또한 다른 숫자들은 뒤에 ’per’가 오더라도 선택되지 않는다는 점에 유의하세요. (?:)는 비캡처 그룹(non-capture group)을 시작하며, 이는 일치 여부만 확인하고 캡처는 하지 않습니다. 따라서 (?:\s?per)는 급여 뒤에 오는 ” per”(두 번째 ?로 인해 공백은 선택 사항)를 찾지만, 결과로 반환되지는 않습니다.
{.callout-note} 연습 문제 "Salary Pay in range $9.00 - $12.02 but you must start at 8.00 - 8.30 every morning."에서 임금 범위만 캡처하는 정규 표현식을 찾아보세요.
지금까지 정규 표현식에 대해 아주 빠르게 살펴보았습니다. 정규 표현식은 외계어처럼 보일 수도 있지만, 더 복잡한 문자열 정리 및 추출 작업을 수행할 때 매우 유용하게 사용할 수 있는 도구입니다.