파이썬 정규 표현식 (Python Regular Expressions)

PythonBeginner
지금 연습하기

소개

정규 표현식 (Regular Expression, 또는 regex) 은 텍스트 처리를 위한 강력한 도구입니다. 이는 검색 패턴을 정의하는 문자 시퀀스로, 놀라운 유연성을 가지고 텍스트를 찾고, 일치시키고, 관리할 수 있게 해줍니다. Python 에서는 re 모듈이 정규 표현식 기능을 담당합니다.

본 실습에서는 Python 에서 정규 표현식을 사용하는 기본 사항을 학습합니다. 패턴 일치의 기본부터 시작하여 그룹화, 반복을 위한 수량자 (quantifier) 사용, 문자 클래스 정의, 그리고 복잡한 일치 시나리오를 처리하기 위한 강력한 메서드 및 플래그 활용과 같은 고급 주제로 점차 이동할 것입니다. 실습이 끝날 때쯤에는 정규 표현식을 사용하여 정교한 문자열 작업을 수행하는 데 익숙해질 것입니다.

정규 표현식 매칭 소개

이 단계에서는 Python 에서 정규 표현식 (Regex) 을 사용하는 기본 워크플로우를 학습합니다. 이 과정은 일반적으로 네 가지 주요 단계로 이루어집니다.

  1. re 모듈을 임포트합니다.
  2. re.compile()을 사용하여 Regex 객체를 생성합니다. 백슬래시가 이스케이프 문자로 해석되는 것을 방지하기 위해 Raw String (r"...") 을 사용하는 것이 권장됩니다.
  3. Regex 객체의 search() 메서드를 사용하여 문자열에서 패턴을 찾습니다. 패턴을 찾으면 Match 객체가 반환됩니다.
  4. Match 객체의 group() 메서드를 사용하여 실제로 매칭된 텍스트를 가져옵니다.

먼저, 사용할 일반적인 정규 표현식 기호들을 살펴보겠습니다.

기호 매칭되는 항목
? 앞선 그룹의 0 회 또는 1 회 등장.
* 앞선 그룹의 0 회 이상 등장.
+ 앞선 그룹의 1 회 이상 등장.
{n} 앞선 그룹의 정확히 n 회 등장.
^spam 문자열이 spam 으로 시작해야 함을 의미합니다.
spam$ 문자열이 spam 으로 끝나야 함을 의미합니다.
. 줄 바꿈 문자 (newline characters) 를 제외한 모든 문자.
\d, \w, \s 각각 숫자, 단어 문자, 공백 문자.
\D, \W, \S 각각 숫자가 아닌 문자, 단어 문자가 아닌 문자, 공백이 아닌 문자.
[abc] 대괄호 안의 문자 중 하나 (예: a, b, c).
[^abc] 대괄호 안의 문자가 아닌 모든 문자.

이제 이를 실제로 적용해 보겠습니다. 문자열에서 전화번호를 찾는 스크립트를 작성할 것입니다. 미국 전화번호의 패턴은 XXX-XXX-XXXX이며, 여기서 X는 숫자입니다. 정규 표현식에서 \d는 숫자를 나타냅니다.

참고: WebIDE(VS Code 와 유사) 사용에 익숙하지 않다면, 이 랩에서 먼저 학습하십시오.

왼쪽 파일 탐색기에서 main.py 파일을 열고 다음 코드를 추가하십시오.

## Import re module for regular expression operations
import re

## re.compile(): create regex pattern object (use raw string r'' to avoid escaping)
## The pattern \d\d\d-\d\d\d-\d\d\d\d matches three digits, a hyphen, three digits, a hyphen, and four digits.
phone_num_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')

## The search() method scans the string, looking for the first location where this regex produces a match.
mo = phone_num_regex.search('My number is 415-555-4242.')

## The group() method returns the string of the matched text.
print(f'Phone number found: {mo.group()}')

스크립트를 실행하려면 터미널을 열고 다음 명령을 실행하십시오.

python3 main.py

일치하는 전화번호가 콘솔에 출력되는 것을 볼 수 있습니다.

Phone number found: 415-555-4242

이 기본 워크플로우는 Python 에서 모든 정규 표현식 작업의 기초가 됩니다.

그룹화 및 교차 (Grouping and Alternation)

이 단계에서는 괄호 ()를 사용하여 일치하는 텍스트의 특정 부분을 격리하는 방법과 교차 (Alternation) 를 사용하여 여러 가능한 패턴 중 하나를 일치시키는 방법을 학습합니다.

괄호 ()를 사용한 그룹화

괄호 ()는 정규 표현식 내에서 그룹을 만드는 데 사용됩니다. 이는 전화번호에서 지역 번호와 같이 일치하는 부분의 일부를 추출하고 싶을 때 유용합니다. group(n)을 사용하여 개별 그룹에 접근할 수 있으며, 여기서 group(1)은 첫 번째 그룹, group(2)는 두 번째 그룹 등을 나타냅니다. group(0) 또는 group()은 전체 일치 문자열을 반환합니다.

main.py를 수정하여 지역 번호를 전화번호의 나머지 부분과 분리해 보겠습니다.

import re

## 괄호는 그룹을 생성합니다: 첫 번째 그룹은 group(1), 두 번째 그룹은 group(2) 입니다.
## 첫 번째 그룹 (\d\d\d) 은 지역 번호를 캡처합니다.
## 두 번째 그룹 (\d\d\d-\d\d\d\d) 은 나머지 번호를 캡처합니다.
phone_num_regex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)')
mo = phone_num_regex.search('My number is 415-555-4242.')

## 모든 그룹을 한 번에 검색하려면 groups() 메서드를 사용하며, 이는 튜플을 반환합니다.
area_code, main_number = mo.groups()

print(f"Area Code: {area_code}")
print(f"Main Number: {main_number}")
print(f"Full Match: {mo.group(0)}")

스크립트를 실행합니다:

python3 main.py

출력 결과는 번호가 분리된 부분을 보여줍니다:

Area Code: 415
Main Number: 555-4242
Full Match: 415-555-4242

파이프 |를 사용한 교차 (Alternation)

파이프 문자 |는 "OR" 연산자처럼 작동합니다. 이를 통해 여러 표현식 중 하나와 일치시킬 수 있습니다. 예를 들어, Batman|Tina Fey는 "Batman" 또는 "Tina Fey" 중 하나와 일치합니다.

정규 표현식의 일부로 여러 가능한 패턴 중 하나와 일치시키기 위해 그룹 내에서 파이프를 사용할 수도 있습니다. "Bat-"과 관련된 다른 용어들을 일치시켜 보겠습니다. main.py를 다음 코드로 업데이트하십시오:

import re

## 파이프 |는 그룹 내에서 여러 패턴 중 하나와 일치합니다.
## 여기서는 'man', 'mobile', 'copter', 또는 'bat' 중 하나와 일치합니다.
bat_regex = re.compile(r'Bat(man|mobile|copter|bat)')
mo = bat_regex.search('Batmobile lost a wheel')

print(f"Full match: {mo.group()}")
print(f"Group 1: {mo.group(1)}")

스크립트를 다시 실행합니다:

python3 main.py

정규 표현식이 "Batmobile"과 일치했으며 첫 번째 그룹이 "mobile"을 캡처했음을 확인할 수 있습니다.

Full match: Batmobile
Group 1: mobile

수량자 (Quantifiers) 를 사용한 반복

이 단계에서는 수량자 (quantifiers) 에 대해 알아봅니다. 수량자는 앞선 그룹이 몇 번 반복되어야 하는지를 지정할 수 있게 해주는 특수 문자입니다.

?를 사용한 선택적 일치

물음표 ?는 앞선 그룹을 선택 사항 (optional) 으로 만들어, 해당 그룹이 0 번 또는 1 번 일치하도록 합니다. 이는 나타날 수도 있고 나타나지 않을 수도 있는 패턴에 완벽하게 적합합니다. 예를 들어, "Batman"과 "Batwoman" 모두를 일치시킬 수 있습니다.

main.py를 다음 코드로 업데이트하십시오:

import re

## (wo)? 그룹은 선택 사항입니다. 존재하면 'wo'와 일치하고, 존재하지 않으면 아무것도 일치시키지 않습니다.
bat_regex = re.compile(r'Bat(wo)?man')

mo1 = bat_regex.search('The Adventures of Batman')
print(mo1.group())

mo2 = bat_regex.search('The Adventures of Batwoman')
print(mo2.group())

스크립트를 실행합니다:

python3 main.py

출력 결과는 두 문자열 모두 성공적으로 일치했음을 보여줍니다:

Batman
Batwoman

*를 사용한 0 회 이상 일치

별표 * (또는 스타) 는 앞선 그룹이 0 번 이상 반복되는 경우와 일치합니다. 이는 패턴이 전혀 나타나지 않는 경우를 포함하여, 패턴이 몇 번이든 반복될 수 있을 때 유용합니다.

import re

## (wo)* 그룹은 0 회 이상 나타날 수 있습니다.
bat_regex = re.compile(r'Bat(wo)*man')

mo1 = bat_regex.search('The Adventures of Batman') ## 'wo'가 0 번
print(mo1.group())

mo2 = bat_regex.search('The Adventures of Batwowowowoman') ## 'wo'가 4 번
print(mo2.group())

+를 사용한 1 회 이상 일치

더하기 기호 +는 앞선 그룹이 1 번 이상 반복되는 경우와 일치합니다. 별표와 달리, 해당 그룹은 최소한 한 번은 나타나야 합니다.

import re

## (wo)+ 그룹은 최소한 한 번 나타나야 합니다.
bat_regex = re.compile(r'Bat(wo)+man')

mo1 = bat_regex.search('The Adventures of Batwoman') ## 'wo'가 1 번
print(mo1.group())

mo2 = bat_regex.search('The Adventures of Batman') ## 'wo'가 0 번
print(mo2 is None) ## 일치하는 항목이 없으므로 True 를 출력합니다.

{}를 사용한 특정 횟수 반복

중괄호 {}를 사용하면 정확한 반복 횟수 또는 범위를 지정할 수 있습니다.

  • {3}은 정확히 3 번 일치합니다.
  • {3,5}는 3 번에서 5 번 사이로 일치합니다.
  • {3,}는 3 번 이상 일치합니다.
  • {,5}는 최대 5 번까지 일치합니다.
import re

## (Ha){3}은 'Ha'가 정확히 3 번 반복되는 경우와 일치합니다.
ha_regex = re.compile(r'(Ha){3}')
mo1 = ha_regex.search('HaHaHa')
print(mo1.group())

탐욕적 (Greedy) 및 비탐욕적 (Non-Greedy) 일치

기본적으로 Python 의 정규 표현식은 "탐욕적 (greedy)"입니다. 즉, 가능한 가장 긴 문자열과 일치하려고 합니다. "비탐욕적 (non-greedy)"으로 만들어 가장 짧은 문자열과 일치시키려면 수량자 뒤에 물음표 ?를 추가합니다.

차이점을 살펴보겠습니다. main.py를 지우고 다음 코드를 추가하십시오:

import re

## 탐욕적 일치: {3,5}는 가능한 가장 긴 문자열 (5 개의 'Ha') 과 일치합니다.
greedy_ha_regex = re.compile(r'(Ha){3,5}')
mo1 = greedy_ha_regex.search('HaHaHaHaHa')
print(f"Greedy match: {mo1.group()}")

## 비탐욕적 일치: {3,5}?는 가능한 가장 짧은 문자열 (3 개의 'Ha') 과 일치합니다.
non_greedy_ha_regex = re.compile(r'(Ha){3,5}?')
mo2 = non_greedy_ha_regex.search('HaHaHaHaHa')
print(f"Non-greedy match: {mo2.group()}")

스크립트를 실행합니다:

python3 main.py

출력 결과는 차이점을 명확하게 보여줍니다:

Greedy match: HaHaHaHaHa
Non-greedy match: HaHaHa

문자 클래스, 앵커 및 와일드카드 (Character Classes Anchors and Wildcards)

이 단계에서는 모든 일치 항목 찾기, 사용자 정의 문자 집합 생성, 일치를 문자열의 시작 또는 끝에 고정하기, 와일드카드 사용을 포함하여 더 고급 기술을 학습합니다.

findall() 메서드

search()는 첫 번째 일치 항목만 반환하는 반면, findall() 메서드는 문자열 내의 겹치지 않는 모든 일치 항목의 리스트를 반환합니다. 정규 표현식에 그룹이 있는 경우, findall()은 각 그룹의 문자열을 포함하는 튜플 리스트를 반환합니다.

다음 예제로 main.py를 업데이트하십시오:

import re

## 정규 표현식에 그룹이 없으면 findall() 은 문자열 리스트를 반환합니다.
phone_num_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
matches = phone_num_regex.findall('Cell: 415-555-9999 Work: 212-555-0000')
print(matches)

출력을 확인하기 위해 스크립트를 실행합니다:

python3 main.py
['415-555-9999', '212-555-0000']

사용자 정의 문자 클래스 [] 만들기

대괄호 []를 사용하면 그 안에 있는 단일 문자와 일치하는 사용자 정의 문자 클래스를 정의할 수 있습니다. 예를 들어, [aeiou]는 모든 소문자 모음과 일치합니다. 또한 [a-z]와 같이 범위를 지정하여 모든 소문자 또는 [0-9]와 같이 모든 숫자를 지정할 수도 있습니다.

대괄호 안에 첫 번째 문자로 캐럿 ^을 사용하면 음수 (negative) 문자 클래스가 생성되며, 이는 클래스에 포함되지 않은 모든 문자와 일치합니다.

문자열에서 모든 모음을 찾아보겠습니다. main.py를 업데이트하십시오:

import re

## [aeiouAEIOU] 는 소문자 또는 대문자인 모든 모음과 일치합니다.
vowel_regex = re.compile(r'[aeiouAEIOU]')
vowels = vowel_regex.findall('Robocop eats baby food. BABY FOOD.')
print(vowels)

앵커 (Anchors): 캐럿 ^ 및 달러 기호 $

앵커는 문자열 내의 위치를 단언 (assert) 하는 데 사용됩니다.

  • 캐럿 ^은 문자열의 시작과 일치합니다.
  • 달러 기호 $은 문자열의 끝과 일치합니다.

함께 사용될 때 (^...$), 전체 문자열이 패턴과 일치해야 함을 강제합니다.

import re

## ^Hello 는 'Hello'로 시작하는 문자열과 일치합니다.
begins_with_hello = re.compile(r'^Hello')
match1 = begins_with_hello.search('Hello world!')
print(match1 is not None) ## True

## \d+$는 하나 이상의 숫자로 끝나는 문자열과 일치합니다.
ends_with_number = re.compile(r'\d+$')
match2 = ends_with_number.search('Your number is 42')
print(match2.group()) ## '42'

## ^\d+$는 오직 숫자로만 구성된 문자열과 일치합니다.
whole_string_is_num = re.compile(r'^\d+$')
match3 = whole_string_is_num.search('1234567890')
print(match3 is not None) ## True

와일드카드 문자 .

점 . (또는 와일드카드) 문자는 줄 바꿈 문자를 제외한 모든 단일 문자와 일치합니다. 이는 "모든 문자"를 나타내는 유용한 약어입니다.

"at"으로 끝나는 모든 세 글자 단어를 찾아보겠습니다. main.py를 업데이트하십시오:

import re

## . 문자는 모든 문자 (줄 바꿈 제외) 와 일치합니다.
## 따라서 .at 은 'cat', 'hat', 'sat' 등과 일치합니다.
at_regex = re.compile(r'.at')
matches = at_regex.findall('The cat in the hat sat on the flat mat.')
print(matches)

스크립트를 실행합니다:

python3 main.py

출력 결과는 일치하는 모든 단어의 리스트가 됩니다:

['cat', 'hat', 'sat', 'lat', 'mat']

고급 메서드 및 플래그 (Advanced Methods and Flags)

마지막 단계에서는 정규 표현식의 동작을 수정하여 더욱 다재다능하게 만드는 강력한 메서드와 플래그에 대해 다룹니다.

점 - 별표 .*를 사용한 모든 것 일치

조합 .*는 모든 문자 (.) 가 0 회 이상 (*) 반복되는 일반적인 패턴입니다. 이는 모든 문자 시퀀스와 일치시키는 데 사용됩니다. 기본적으로 탐욕적 (greedy) 이며 가능한 한 많은 텍스트와 일치하려고 합니다.

import re

## 패턴 (.*) 은 'First Name: '와 ' Last Name: ' 사이의 모든 것을 캡처합니다.
name_regex = re.compile(r'First Name: (.*) Last Name: (.*)')
mo = name_regex.search('First Name: Al Last Name: Sweigart')
print(f"First Name: {mo.group(1)}")
print(f"Last Name: {mo.group(2)}")

비탐욕적 (non-greedy) 으로 만들려면 .*?를 사용합니다. 이는 가능한 가장 짧은 문자열과 일치합니다.

re.DOTALL을 사용한 줄 바꿈 일치

기본적으로 점 (.) 문자는 줄 바꿈 문자 (\n) 와 일치하지 않습니다. 이를 변경하려면 re.compile()의 두 번째 인수로 re.DOTALL을 전달할 수 있습니다. 이 플래그를 사용하면 .이 줄 바꿈 문자를 포함하여 모든 문자와 일치할 수 있습니다.

import re

## re.DOTALL 을 사용하면 '.*'가 여러 줄에 걸쳐 일치합니다.
newline_regex = re.compile('.*', re.DOTALL)
mo = newline_regex.search('Serve the public trust.\nProtect the innocent.\nUphold the law.')
print(mo.group())

re.IGNORECASE를 사용한 대소문자 구분 없는 일치

정규 표현식이 대소문자를 무시하도록 하려면 (예: "cat", "Cat", "CAT" 모두 일치), re.compile()의 두 번째 인수로 re.IGNORECASE 또는 약어인 re.I를 전달합니다.

import re

## re.I 는 패턴 'robocop'이 'Robocop', 'ROBOCOP' 등과 일치하도록 만듭니다.
robocop = re.compile(r'robocop', re.I)
match = robocop.search('Robocop is part man, part machine, all cop.')
print(match.group())

sub() 메서드를 사용한 문자열 치환

sub() 메서드는 패턴의 모든 발생 항목을 찾아 지정된 문자열로 대체합니다. 이 메서드는 대체 문자열과 검색할 문자열이라는 두 가지 인수를 받습니다.

에이전트 이름을 "CENSORED"로 바꿔보겠습니다. main.py를 업데이트하십시오:

import re

## 패턴 'Agent \w+'는 'Agent ' 뒤에 하나 이상의 단어 문자 (\w+) 가 오는 것과 일치합니다.
names_regex = re.compile(r'Agent \w+')
result = names_regex.sub('CENSORED', 'Agent Alice gave the secret documents to Agent Bob.')
print(result)

스크립트를 실행합니다:

python3 main.py

출력 결과는 치환된 문자열을 보여줍니다:

CENSORED gave the secret documents to CENSORED.

re.VERBOSE를 사용한 복잡한 정규 표현식 관리

정규 표현식 패턴이 복잡해지면 읽기 어려워질 수 있습니다. re.compile()에 re.VERBOSE를 전달하면 정규 표현식을 여러 줄에 걸쳐 작성하고 주석을 추가하여 각 부분을 설명할 수 있습니다. 문자열 내의 공백과 주석 (#) 은 무시됩니다.

이 복잡한 정규 표현식: phone_regex = re.compile(r'((\d{3}|\(\d{3}\))?(\s|-|\.)?\d{3}(\s|-|\.)\d{4}(\s*(ext|x|ext.)\s*\d{2,5})?)')

re.VERBOSE를 사용하면 다음과 같이 읽기 쉬운 버전이 됩니다:

import re

phone_regex = re.compile(r'''(
    (\d{3} | \(\d{3}\))?          ## area code (optional)
    (\s | - | \.)?                ## separator (optional)
    \d{3}                         ## first 3 digits
    (\s | - | \.)                 ## separator
    \d{4}                         ## last 4 digits
    (\s*(ext|x|ext.)\s*\d{2,5})?  ## extension (optional)
    )''', re.VERBOSE)

mo = phone_regex.search("My phone number is 415-555-4242 ext. 55")
if mo:
    print(f"Found: {mo.group()}")

이렇게 하면 코드를 훨씬 더 유지 관리하기 쉬워집니다.

요약

본 실습 (lab) 에서는 re 모듈을 사용하여 Python 에서 정규 표현식 (regular expressions) 의 기본 사항을 숙달했습니다. 패턴을 re.compile()로 컴파일하는 것부터 search()로 일치 항목을 찾고 group()으로 텍스트를 검색하는 전체 워크플로우를 학습했습니다.

다음과 같은 주요 개념들을 살펴보았습니다:

  • 일치 항목의 특정 부분을 캡처하기 위한 ()를 사용한 그룹화 (Grouping).
  • 여러 패턴 중 하나와 일치시키기 위한 |를 사용한 교대 (Alternation).
  • 반복을 제어하기 위한 ?, *, +, {}와 같은 수량자 (Quantifiers).
  • 일치 항목의 길이를 제어하기 위한 탐욕적 (Greedy) 대 비탐욕적 (Non-greedy) 일치.
  • 모든 일치 항목을 가져오는 findall(), 사용자 정의 집합을 위한 문자 클래스 [], 문자열 경계에 일치를 고정하기 위한 앵커 ^ 및 $와 같은 고급 도구.
  • 다중 라인 일치를 위한 re.DOTALL, 대소문자 구분 없는 일치를 위한 re.IGNORECASE, 깔끔하고 주석이 달린 정규 표현식을 작성하기 위한 re.VERBOSE와 같은 강력한 플래그.
  • sub() 메서드를 사용한 문자열 치환 (Substitution).

이러한 기술을 통해 이제 간단한 데이터 유효성 검사부터 복잡한 데이터 추출 및 조작에 이르기까지 광범위한 텍스트 처리 작업을 처리할 수 있는 능력을 갖추게 되었습니다.