정규표현식(Regular Expressions)을 위한 데이터 과학자 필수 가이드

데이터 과학자로서 여러분은 자주 지저분하고 구조화되지 않은 텍스트 데이터를 만나게 됩니다. 이 데이터를 분석하기 전에 먼저 정리하고, 관련 정보를 추출하고, 구조화된 형식으로 변환해야 합니다. 이때 정규표현식이 유용하게 사용됩니다.
정규표현식은 텍스트에서 패턴을 설명하기 위한 특수한 미니 언어라고 생각하세요. 핵심 개념을 이해하면 표준 문자열 메서드를 사용할 때 수십 줄이 필요한 복잡한 텍스트 작업을 몇 줄의 코드로 수행할 수 있습니다.
정규표현식을 생각하는 방법
정규표현식을 마스터하는 핵심은 올바른 정신적 모델을 개발하는 것입니다. 근본적으로 정규표현식은 텍스트를 왼쪽에서 오른쪽으로 이동하면서 일치하는 항목을 찾으려고 시도하는 패턴입니다.
책에서 특정 패턴을 찾고 있다고 상상해보세요. 각 페이지를 스캔하면서 해당 패턴을 찾습니다. 정규표현식도 기본적으로 그런 방식으로 작동합니다. 텍스트를 문자별로 스캔하면서 현재 위치가 패턴과 일치하는지 확인합니다.
Python의 내장 re 모듈을 가져오는 것부터 시작해보겠습니다:
import re
1. 리터럴 문자: 첫 번째 정규표현식 패턴 만들기
가장 간단한 정규표현식 패턴은 정확한 텍스트와 일치합니다. 텍스트에서 "data"라는 단어를 찾으려면 다음과 같이 작성할 수 있습니다:
text = "Data science is cool as you get to work with real-world data"
matches = re.findall(r"data", text)
print(matches)
이는 소문자 "data"만 찾고 맨 앞의 "Data"는 찾지 못했습니다.
Output >>> ['data']
정규표현식은 기본적으로 대소문자를 구분합니다. 이것이 첫 번째 중요한 교훈입니다: 찾고자 하는 것을 명확하게 지정하세요.
matches = re.findall(r"data", text, re.IGNORECASE)
print(matches)
Output >>> ['Data', 'data']
문자열 앞에 있는 r은 "raw 문자열"을 생성합니다. 이는 정규표현식에서 중요한데, 백슬래시가 특수 시퀀스에 사용되기 때문에 Python이 이러한 백슬래시를 해석하지 않도록 해줍니다.
2. 메타문자: 리터럴 매칭을 넘어서
정규표현식을 유용하게 만드는 것은 메타문자를 사용하여 패턴을 정의하는 능력입니다. 이들은 문자 그대로의 표현을 넘어 특별한 의미를 갖는 특수 문자입니다.
와일드카드: 점(.)
점은 개행 문자를 제외한 모든 문자와 일치합니다. 패턴의 일부는 알지만 전체는 모를 때 특히 유용합니다:
text = "The cat sat on the mat. The bat flew over the rat."
pattern = r"The ... "
matches = re.findall(pattern, text)
print(matches)
여기서는 "The" 다음에 공백을 포함하여 아무 세 문자가 오는 패턴을 찾고 있습니다.
Output >>> ['The cat ', 'The bat ']
점은 강력하지만 때로는 너무 강력합니다. 모든 것과 일치하기 때문입니다. 이것이 문자 클래스가 필요한 이유입니다.
문자 클래스: []로 구체화하기
문자 클래스를 사용하면 일치시킬 문자 집합을 정의할 수 있습니다:
text = "The cat sat on the mat. The bat flew over the rat."
pattern = r"[cb]at"
matches = re.findall(pattern, text)
print(matches)
이 패턴은 [cb] 집합의 문자 다음에 "at"이 오는 "cat" 또는 "bat"를 찾습니다.
Output >>> ['cat', 'bat']
문자 클래스는 특정 위치에 나타날 수 있는 문자 집합이 제한되어 있을 때 완벽합니다.
문자 클래스에서 범위를 사용할 수도 있습니다:
a-d로 시작하는 모든 소문자 단어 찾기
pattern = r"\b[a-d][a-z]*\b"
text = "apple banana cherry date elephant fig grape kiwi lemon mango orange"
matches = re.findall(pattern, text)
print(matches)
여기서 \b는 단어 경계를 나타내고, [a-d]는
a부터 d까지의 소문자와 일치하며, [a-z]*는 0개 이상의 소문자와 일치합니다.
Output >>> ['apple', 'banana', 'cherry', 'date']
수량자: 반복 지정하기
종종 반복되는 패턴을 일치시키고 싶을 것입니다. 수량자를 사용하면 문자나 그룹이 나타나야 하는 횟수를 지정할 수 있습니다. 하이픈을 사용하든 그렇지 않든 모든 전화번호를 찾아 봅시다:
text = "Phone numbers: 555-1234, 555-5678, 5551234"
pattern = r"\b\d{3}-?\d{4}\b"
matches = re.findall(pattern, text)
print(matches)
결과:
Output >>> ['555-1234', '555-5678', '5551234']
이 패턴을 분석해보면:
- \b는 단어 경계를 보장합니다
- \d{3}은 정확히 3자리 숫자와 일치합니다
- -?는 0개 또는 1개의 하이픈과 일치합니다(? 기호는 하이픈을 선택적으로 만듭니다)
- \d{4}는 정확히 4자리 숫자와 일치합니다
이것은 여러 형식을 처리하기 위해 여러 패턴이나 복잡한 문자열 연산을 작성하는 것보다 훨씬 우아합니다.
3. 앵커: 특정 위치에서 패턴 찾기
때로는 텍스트의 특정 위치에서만 패턴을 찾고 싶을 때가 있습니다. 앵커가 이 문제를 해결합니다:
text = "Python is popular in data science."
^ 문자열 시작에 고정
start_matches = re.findall(r"^Python", text)
print(start_matches)
$ 문자열 끝에 고정
end_matches = re.findall(r"science\.$", text)
print(end_matches)
출력:
['Python']
['science.']
앵커는 문자를 일치시키지 않고 위치를 일치시킵니다. 이메일 주소와 같이 특정 요소가 시작 또는 끝에 나타나야 하는 형식을 검증할 때 유용합니다.
4. 캡처 그룹: 특정 부분 추출하기
데이터 과학에서는 종종 패턴을 찾는 것뿐만 아니라 해당 패턴의 특정 부분을 추출하고 싶을 때가 있습니다. 괄호로 생성된 캡처 그룹을 사용하면 이 작업을 수행할 수 있습니다:
text = "Dates: 2023-10-15, 2022-05-22"
pattern = r"(\d{4})-(\d{2})-(\d{2})"
findall은 캡처된 그룹의 튜플을 반환합니다
matches = re.findall(pattern, text)
print(matches)
구조화된 데이터를 만들기 위해 이를 사용할 수 있습니다
for year, month, day in matches:
print(f"Year: {year}, Month: {month}, Day: {day}")
출력:
[('2023', '10', '15'), ('2022', '05', '22')]
Year: 2023, Month: 10, Day: 15
Year: 2022, Month: 05, Day: 22
이는 구조화되지 않은 텍스트에서 구조화된 정보를 추출하는 데 특히 유용하며, 이는 데이터 과학에서 흔한 작업입니다.
5. 명명된 그룹: 정규표현식을 더 읽기 쉽게 만들기
복잡한 패턴의 경우 각 그룹이 무엇을 캡처하는지 기억하는 것이 어려울 수 있습니다. 명명된 그룹이 이 문제를 해결합니다:
text = "Contact: john.doe@example.com"
pattern = r"(?P[\w.]+)@(?P[\w.]+)"
match_ = re.search(pattern, text)
if match_:
print(f"Username: {match.group('username')}")
print(f"Domain: {match.group('domain')}")
출력:
Username: john.doe
Domain: example.com
명명된 그룹은 정규표현식을 더 자기 문서화하고 유지 관리하기 쉽게 만듭니다.
실제 데이터 작업: 실용적인 예제
정규표현식이 일반적인 데이터 과학 작업에 어떻게 적용되는지 살펴보겠습니다.
예제 1: 지저분한 데이터 정리
일관성 없는 제품 코드가 있는 데이터셋이 있다고 가정해 보겠습니다:
product_codes = [
"PROD-123",
"Product 456",
"prod_789",
"PR-101",
"p-202"
]
숫자 부분만 추출하여 이를 표준화하고 싶습니다:
cleaned_codes = []
for code in product_codes:
# 숫자 부분만 추출
match = re.search(r"\d+", code)
if match:
cleaned_codes.append(match.group())
print(cleaned_codes)
출력:
['123', '456', '789', '101', '202']
이는 다양한 형식을 처리하기 위해 여러 문자열 작업을 작성하는 것보다 훨씬 깔끔합니다.
예제 2: 텍스트에서 정보 추출하기
고객 서비스 로그에서 정보를 추출해야 한다고 상상해보세요:
log = "ISSUE #1234 [2023-10-15] Customer reported app crash on iPhone 12, iOS 15.2"
정규표현식으로 구조화된 데이터를 추출할 수 있습니다:
이슈 번호, 날짜, 기기, OS 버전 추출
pattern = r"ISSUE #(\d+) \[(\d{4}-\d{2}-\d{2})\].?(iPhone \d+).?(iOS \d+\.\d+)"
match = re.search(pattern, log)
if match:
issuenum, date, device, iosversion = match.groups()
print(f"Issue: {issue_num}")
print(f"Date: {date}")
print(f"Device: {device}")
print(f"iOS Version: {ios_version}")
출력:
Issue: 1234
Date: 2023-10-15
Device: iPhone 12
iOS Version: iOS 15.2
예제 3: 데이터 검증
정규표현식은 데이터 형식을 검증하는 데 유용합니다:
def validate_email(email):
"""이메일 형식을 검증하고 유효하거나 유효하지 않은 이유를 설명합니다."""
pattern = r"^[\w.%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
if not re.match(pattern, email):
# 특정 문제 확인
if '@' not in email:
return False, "@ 기호 누락"
username, domain = email.split('@', 1)
if not username:
return False, "사용자 이름이 비어 있음"
if '.' not in domain:
return False, "잘못된 도메인(최상위 도메인 누락)"
return False, "잘못된 이메일 형식"
return True, "유효한 이메일"
다양한 이메일로 테스트:
다양한 이메일로 테스트
emails = ["user@example.com", "invalid@.com", "noatsign.com", "user@example.co.uk"]
for email in emails:
valid, reason = validate_email(email)
print(f"{email}: {reason}")
출력:
user@example.com: 유효한 이메일
invalid@.com: 잘못된 이메일 형식
noatsign.com: @ 기호 누락
user@example.co.uk: 유효한 이메일
이 함수는 이메일을 검증할 뿐만 아니라 유효하거나 유효하지 않은 이유를 설명하는데, 이는 단순한 참/거짓 결과보다 더 유용합니다.
패턴을 나열하는 대신 그것들을 작동시키는 구성 요소를 이해해 봅시다:
이메일 검증
pattern = r"^[\w.%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
이 패턴을 분석해 보면:
- ^는 문자열의 시작을 보장합니다
- [\w.%+-]+는 하나 이상의 단어 문자, 점, 퍼센트 기호, 플러스 기호 또는 하이픈(일반적인 사용자 이름 문자)과 일치합니다
- @는 문자 그대로 @ 기호와 일치합니다
- [A-Za-z0-9.-]+는 하나 이상의 문자, 숫자, 점 또는 하이픈(도메인 이름)과 일치합니다
- \.는 문자 그대로 점과 일치합니다
- [A-Za-z]{2,}는 두 개 이상의 문자(최상위 도메인)와 일치합니다
이 패턴은 유효한 이메일을 허용하면서 잘못된 형식은 거부합니다.
날짜 추출
pattern = r"\b(\d{4})-(\d{2})-(\d{2})\b"
이 패턴은 ISO 날짜(YYYY-MM-DD)와 일치합니다:
- \b는 단어 경계에 있음을 보장합니다
- (\d{4})는 연도에 대해 정확히 4자리 숫자를 캡처합니다
- -는 문자 그대로 하이픈과 일치합니다
- (\d{2})는 월에 대해 정확히 2자리 숫자를 캡처합니다
- -는 문자 그대로 하이픈과 일치합니다
- (\d{2})는 일에 대해 정확히 2자리 숫자를 캡처합니다
이 구조를 이해하면 MM/DD/YYYY와 같은 다른 날짜 형식에 맞게 조정할 수 있습니다.
고급 기술: 기본 정규표현식을 넘어서
정규표현식에 더 익숙해지면 기본 패턴이 부족한 상황에 직면하게 될 것입니다. 다음은 몇 가지 고급 기술입니다:
전방 탐색과 후방 탐색
이들은 "폭이 없는 어설션"으로, 일치에 포함하지 않고 패턴이 존재하는지 확인합니다:
비밀번호 검증
password = "Password123"
has_uppercase = bool(re.search(r"(?=.*[A-Z])", password))
has_lowercase = bool(re.search(r"(?=.*[a-z])", password))
has_digit = bool(re.search(r"(?=.*\d)", password))
islongenough = len(password) >= 8
if all([hasuppercase, haslowercase, hasdigit, islong_enough]):
print("비밀번호가 요구사항을 충족합니다")
else:
print("비밀번호가 모든 요구사항을 충족하지 않습니다")
출력:
비밀번호가 요구사항을 충족합니다
전방 탐색 (?=.*[A-Z])은 실제로 캡처하지 않고 문자열 어딘가에 대문자가 있는지 확인합니다.
비탐욕적 매칭
수량자는 기본적으로 "탐욕적"입니다. 즉, 가능한 한 많이 일치시킵니다. 수량자 뒤에 ?를 추가하면 "비탐욕적"으로 만들 수 있습니다:
text = "
First contentSecond content"
탐욕적 매칭(기본값)
greedy = re.findall(r"
(.*)", text)
print(f"Greedy: {greedy}")
비탐욕적 매칭
non_greedy = re.findall(r"
(.*?)", text)
print(f"Non-greedy: {non_greedy}")
출력:
Greedy: ['First content