1 정규표현식 기초
1.1 정규표현식이란?
- 토큰화같은 텍스트 전처리에 필수적으로 사용되는 도구
- 정규표현식(Regular Expression)은 특정한 패턴을 가진 문자열을 검색, 매칭, 치환하는 데 사용되는 형식 언어
- 자연어 처리(NLP)에서 텍스트 전처리 단계에서 중요한 도구로 활용
1.2 기본 문자 매칭
- 일반 문자: 문자 자체를 매칭
- 메타 문자: 특별한 의미를 가진 문자들 (., ^, $, *, +, ? 등)
1.3 기본 패턴 규칙
.: 임의의 한 문자와 매칭- 예시:
a.c는 “abc”, “adc”, “a3c” 등과 매칭됨
- 예시:
^: 문자열의 시작- 예시:
^Hello는 “Hello World”와 매칭되지만 “World Hello”와는 매칭되지 않음
- 예시:
$: 문자열의 끝- 예시:
world$는 “Hello world”와 매칭되지만 “world hello”와는 매칭되지 않음
- 예시:
*: 앞의 요소가 0번 이상 반복- 예시:
ab*c는 “ac”, “abc”, “abbc”, “abbbc” 등과 매칭됨
- 예시:
+: 앞의 요소가 1번 이상 반복- 예시:
ab+c는 “abc”, “abbc”, “abbbc” 등과 매칭되지만 “ac”와는 매칭되지 않음
- 예시:
?: 앞의 요소가 0번 또는 1번 등장- 예시:
colou?r는 “color”와 “colour” 모두와 매칭됨
- 예시:
{n}: 앞의 요소가 정확히 n번 반복- 예시:
a{3}는 “aaa”와 매칭됨
- 예시:
{n,}- 앞의 요소가 n번 이상 반복- 예시:
a{2,}는 “aa”, “aaa”, “aaaa” 등과 매칭됨
- 예시:
{n,m}- 앞의 요소가 n번 이상 m번 이하 반복- 예시:
a{1,3}는 “a”, “aa”, “aaa”와 매칭됨
- 예시:
1.4 문자 클래스
[abc]: a, b, c 중 하나와 매칭- 예시:
[abc]at는 “aat”, “bat”, “cat”과 매칭됨
- 예시:
[^abc]: ^ 문자는 문자 클래스 [ ] 내에서 사용될 때 부정(negation)을 의미하며, a, b, c를 제외한 문자와 매칭- 예시:
[^abc]at는 “dat”, “eat”, “fat” 등과 매칭되지만 “aat”, “bat”, “cat”과는 매칭되지 않음
- 예시:
[a-z]: - 문자는 문자 클래스 [ ] 내에서 사용될 때 범위(range)를 의미하며, a부터 z까지의 소문자와 매칭- 예시:
[a-z]oo는 “foo”, “zoo” 등과 매칭됨
- 예시:
[A-Z]: A부터 Z까지의 대문자와 매칭- 예시:
[A-Z]ello는 “Hello”, “Jello” 등과 매칭됨
- 예시:
[0-9]- 숫자와 매칭- 예시:
page[0-9]는 “page0”, “page1” 등과 매칭됨
- 예시:
[_]: 문자 클래스 [ ] 내에서 사용될 때 문자 클래스를 의미하며, 언더바(_)와 매칭- 예시 ```python import re # [^a-zA-Z0-9_] 패턴은 영문자, 숫자, 언더스코어를 제외한 모든 문자와 일치 text = “Hello_world! This-is a sample@123.” cleaned = re.sub(r’[^a-zA-Z0-9_]‘,’ ’, text) # 특수문자만 공백으로 됨 print(cleaned) # “Hello_world This is a sample 123”
1.5 미리 정의된 문자 클래스
\d- 숫자와 매칭 (=[0-9])- 예시:
\d\d\d는 “123”, “456” 등 세 자리 숫자와 매칭됨
- 예시:
\D- 숫자가 아닌 문자와 매칭 (=[^0-9])- 예시:
\D\D\D는 “abc”, “XYZ” 등 숫자가 아닌 세 문자와 매칭됨
- 예시:
\w- 단어 문자와 매칭 (=[a-zA-Z0-9_])예시:
\w+는 단어 문자가 1회 이상 반복되어야 한다.“hello123”, “Python_3”, “user_name”, “variable42”, “data_science2023” 등과 매칭됨
예시:
\W- 단어 문자가 아닌 문자와 매칭- 예시:
\W+는 “!@#”, ” ” 등과 매칭됨
- 예시:
\s- 공백 문자와 매칭 (스페이스, 탭, 줄바꿈 등)- 예시:
hello\sworld는 “hello world”와 매칭됨
- 예시:
\S- 공백이 아닌 문자와 매칭- 예시:
\S+는 “hello”, “world” 등 공백이 없는 문자열과 매칭됨
- 예시:
1.6 그룹과 참조
(...)- 그룹화 및 캡처- 예시:
(ab)+는 “ab”, “abab”, “ababab” 등과 매칭됨 - 예시:
(a+b+)는 “ab”, “aab”, “abb”, “aaabbb” 등 a가 하나 이상, b가 하나 이상 연속된 패턴과 매칭됨 - 예시:
(a{2}b{2})는 “aabb”와 같이 정확히 a가 2번, b가 2번 반복되는 패턴과 매칭됨 - 예시:
(a{2,3}b{2,3})는 “aabb”, “aaabb”, “aabbb”, “aaabbb” 등 a가 2~3번, b가 2~3번 반복되는 패턴과 매칭됨
- 예시:
(?:...)- 그룹화만 하고 캡처하지 않음 (non-capturing group)- 일반 그룹
(...)은 패턴을 그룹화하고 매칭된 부분을 메모리에 저장(캡처)함 - 반면
(?:...)는 패턴을 그룹화하지만 매칭된 부분을 메모리에 저장하지 않음 - 단순히 패턴을 묶어서 처리하고 싶을 때 사용하며, 나중에 참조할 필요가 없을 때 메모리 효율을 위해 사용함
- 예시:
(?:ab)+c는 “abc”, “ababc”, “abababc” 등과 매칭됨 - 예시:
(?:ab)+는 “ab”가 1회 이상 반복됨 - 예시:
(?:ab)+c는 “ab”가 1회 이상 반복된 후 “c”가 오는 패턴 - 예시:
ab는 정확히 “ab”라는 문자열과 매칭 - 예시:
(ab)는 “ab”를 하나의 단위로 그룹화하고 캡처함 - 예시:
(ab)+c는 “ab”가 1회 이상 반복된 후 “c”가 오는 패턴 - 주요 차이점:
- 패턴 자체의 차이:
(?:ab)+: “ab”의 반복만 매치(ab)+c: “ab”의 반복 + “c”를 매치
- 캡처 여부:
(?:ab)+: 비캡처 그룹이므로 매치된 내용을 캡처하지 않음(ab)+c: 캡처 그룹이므로 마지막으로 매치된 “ab”를 캡처함
- 매치되는 문자열:
(?:ab)+: “ab”, “abab”, “ababab”…(ab)+c: “abc”, “ababc”, “abababc”…
- 패턴 자체의 차이:
import re pattern = r'(?:ab)+' strings = ['ab', 'abab', 'ababab', 'a', 'abc'] for s in strings: match = re.fullmatch(pattern, s) if match: print(f"'{s}' 매치됨") print(f" 그룹: {match.groups()}") # 빈 튜플 () - 캡처된 그룹 없음 else: print(f"'{s}' 매치되지 않음") # 결과 # 'ab' 매치됨 # 그룹: () # 'abab' 매치됨 # 그룹: () # 'ababab' 매치됨 # 그룹: () # 'a' 매치되지 않음 # 'abc' 매치되지 않음 pattern = r'(ab)+c' strings = ['abc', 'ababc', 'abababc', 'ab', 'c'] for s in strings: match = re.fullmatch(pattern, s) if match: print(f"'{s}' 매치됨") print(f" 그룹: {match.groups()}") # ('ab',) - 마지막 'ab'가 캡처됨 else: print(f"'{s}' 매치되지 않음") # 결과: # # 'abc' 매치됨 # 그룹: ('ab',) # 'ababc' 매치됨 # 그룹: ('ab',) # 'abababc' 매치됨 # 그룹: ('ab',) # 'ab' 매치되지 않음 # 'c' 매치되지 않음- 일반 그룹
\1, \2, ...- 이전에 캡처된 그룹을 참조하는 역참조(backreference) 기능- \1: 첫 번째 캡처 그룹과 동일한 내용을 의미
- \2: 두 번째 캡처 그룹과 동일한 내용을 의미
- \3: 세 번째 캡처 그룹과 동일한 내용을 의미
- 예시:
(\w+) \1는- 첫 번째 캡처 그룹(단어)이 그대로 반복되는 패턴
- 매치됨: “hello hello”, “python python”
- 매치되지 않음: “hello world”, “python java”
- 예시:
(\w+) (\w+) \2는- 첫 번째 단어 다음에 두 번째 단어가 나오고, 그 다음에 두 번째 단어가 반복되는 패턴
- 매치됨: “hello world world”, “python java java”
- 매치되지 않음: “hello hello world”, “python python java”
- 예시:
(\w+) (\w+) (\w+) \3는- 세 개의 단어가 나오고, 그 다음에 세 번째 단어가 반복되는 패턴
- 매치됨: “apple banana cherry cherry”, “one two three three”
- 매치되지 않음: “apple banana cherry banana”, “one two three one” ## 경계 매칭
\b: 단어 경계- 예시:
\bcat\b는 “The cat sat” 문장에서 “cat”과 매칭되지만 “category”의 일부와는 매칭되지 않음
- 예시:
\B: 단어 경계가 아닌 위치- 예시:
\Bcat\B는 “location”의 “cat”과 매칭되지만 독립된 단어 “cat”과는 매칭되지 않음
- 예시:
1.7 선택과 대안
|- 대안 패턴 (OR 연산자)- 예시:
cat|dog는 “cat”과 “dog” 모두와 매칭됨 ### NLP에서의 정규표현식 활용 예시
- 예시:
- 정규표현식은 자연어 처리(NLP)에서 다양한 텍스트 전처리 및 정보 추출 작업에 활용
1.7.0.1 이메일 주소 추출
import re
text = "연락처: john.doe@example.com, support@company.co.kr, help-desk@org.net"
# 이메일 패턴
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
# 모든 이메일 주소 추출
emails = re.findall(email_pattern, text)
print(emails)
# 출력: ['john.doe@example.com', 'support@company.co.kr', 'help-desk@org.net']1.7.0.2 URL 추출
1.7.0.3 문장 토큰화
1.7.0.4 해시태그 추출 (소셜 미디어 분석)
1.7.0.5 날짜 형식 표준화
import re
dates = [
"2023-05-15",
"05/15/2023",
"15.05.2023",
"May 15, 2023"
]
# 다양한 날짜 패턴 처리
for date in dates:
# YYYY-MM-DD 패턴
if re.match(r'^\d{4}-\d{2}-\d{2}$', date):
print(f"{date} -> 이미 표준 형식")
# MM/DD/YYYY 패턴
elif re.match(r'^\d{2}/\d{2}/\d{4}$', date):
m, d, y = re.findall(r'(\d{2})/(\d{2})/(\d{4})', date)[0]
print(f"{date} -> {y}-{m}-{d}")
# DD.MM.YYYY 패턴
elif re.match(r'^\d{2}\.\d{2}\.\d{4}$', date):
d, m, y = re.findall(r'(\d{2})\.(\d{2})\.(\d{4})', date)[0]
print(f"{date} -> {y}-{m}-{d}")
# 월 이름 패턴
elif re.match(r'^[A-Za-z]+ \d{1,2}, \d{4}$', date):
month_names = {"January": "01", "February": "02", "March": "03", "April": "04",
"May": "05", "June": "06", "July": "07", "August": "08",
"September": "09", "October": "10", "November": "11", "December": "12"}
match = re.match(r'^([A-Za-z]+) (\d{1,2}), (\d{4})$', date)
if match:
month, day, year = match.groups()
month_num = month_names.get(month, "00")
day = day.zfill(2) # 한 자리 숫자면 앞에 0 추가
print(f"{date} -> {year}-{month_num}-{day}")1.7.0.6 개체명 추출
- 간단한 규칙 기반으로 정확한 원하는 답을 얻을 순 없지만 규칙을 정밀하게 보완하면 어느 정도 후보군을 추릴 수 있다.
- 이런 규칙 기반의 텍스트 추출을 할 때 정규표현식이 필수적으로 사용된다.)
- 예를 들어 아래의 인명을 추출하는 것은 다음과 같은 추가 조건을 추가하면 좋다.
- 사전 기반 접근법: 흔한 한국 성씨 목록을 사용해 필터링
- 문맥 분석: 이름 앞뒤로 나오는 “씨”, “님”, “대표” 등의 호칭 고려
- 기계학습 방식 (ex.NER, Named Entity Recognition 모델 사용)
import re
text = """
김철수 대표는 삼성전자에서 5년간 근무했으며, 현재 서울특별시 강남구에 거주 중입니다.
그는 한국대학교를 졸업했고, 현재 7,500,000원의 월급을 받고 있습니다.
"""
# 인명 패턴 (성+이름) : 성이 2~4글자, 이름이 2~4글자 또는 2~5글자
person_pattern = r'[가-힣]{2,4}\s[가-힣]{2,4}|[가-힣]{2,5}'
persons = re.findall(person_pattern, text)
print("인명:", persons) # 인명: ['김철수', '대표는', '현재', '서울특별시', '강남구에', '거주', '현재']
# 기관명 패턴
org_pattern = r'[가-힣a-zA-Z0-9]+[대학교|회사|전자|은행|그룹]'
orgs = re.findall(org_pattern, text)
print("기관:", orgs)
# 금액 패턴
money_pattern = r'[0-9,]+원'
money = re.findall(money_pattern, text)
print("금액:", money)
# 지역 패턴
location_pattern = r'[가-힣]+[시|도|군|구](\s[가-힣]+[시|도|군|구])?'
locations = re.findall(location_pattern, text)
print("지역:", locations)1.7.0.7 텍스트 정제 (HTML 태그 제거)
1.7.0.8 특수문자 정규화
1.7.0.9 단어 빈도 분석
1.7.0.10 전화번호 추출 및 정규화
import re
text = "연락처 목록: 010-1234-5678, 02)987-6543, 01099998888, +82-10-5555-1234"
# 전화번호 패턴
phone_pattern = r'(\+\d{1,3}[-\s]?)?(\d{2,3}[-\)\s]?)?(\d{3,4}[-\s]?)(\d{4})'
phones = re.findall(phone_pattern, text)
# 전화번호 정규화
normalized_phones = []
for phone in phones:
country, area, first, last = phone
normalized = f"{area.replace(')', '')}-{first.replace('-', '')}-{last}"
normalized_phones.append(normalized)
print(normalized_phones)
# 출력: ['010-1234-5678', '02-987-6543', '010-9999-8888', '10-5555-1234']- 이러한 정규표현식 활용은 NLP 파이프라인의 전처리 단계에서 텍스트 정제, 토큰화, 정보 추출에 매우 유용하다
- 하지만 복잡한 언어 패턴 분석에는 한계가 있어 고급 NLP 작업에는 딥러닝 모델과 함께 사용되는 경우가 많다.