Step 48. 정규표현식 — 모양으로 찾는 자석
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 3.5시간
전제: Step 41~46을 마쳤다. 문자열 조작(split, in)과 리스트를 쓸 수 있다.
- 준비물: 파이썬이 설치된 컴퓨터, 텍스트 에디터. 새로 설치하는 것은 없습니다.
- 주의: 오늘 실습은 100% 안전합니다. 다만 처음 보는 기호들이 암호처럼 보일 수 있습니다 — 처음엔 헷갈리는 것이 정상입니다.
백만 줄짜리 로그에서 "IP 주소처럼 생긴 것"만 뽑고 싶다고 합시다. in으로는 못 합니다. "1.2.3.4"라는 정확한 문자열을 찾는 것이 아니라, "숫자.숫자.숫자.숫자 모양 전부"를 찾아야 하니까요. 모양으로 찾기 — 이것이 오늘의 주제이고, 그 "모양을 묘사하는 언어"가 정규표현식(regular expression, regex)입니다. 로그 분석, 플래그 찾기, 입력 검증까지 보안 공부의 거의 모든 텍스트 작업에 등장하는, 배워 두면 평생 쓰는 도구입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
\d,\w,.,+,*,{n,m}의 뜻을 설명하고 패턴을 조립한다re.findall로 텍스트에서 원하는 조각(IP, 이메일, 날짜)을 뽑아 낸다- 괄호 그룹으로 패턴의 일부만 캡처한다
re.search와re.fullmatch의 차이를 알고 검증에 올바른 쪽을 쓴다- 패턴을 단순한 것부터 쌓아 올리는 작업법을 익힌다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 + 표준 re 모듈 (설치 불필요) |
| 오늘의 함수 | re.findall(전부 뽑기), re.search(첫 하나), re.fullmatch(전체 일치 검증) |
| 오늘의 기호 | \d \w . + * {n,m} [ ] ( ) \. |
| 필요한 개념 | 메타문자, raw 문자열(r""), 그룹 캡처, 이스케이프 |
2-1. 메타문자 — 기호의 뜻표
정규식의 재료는 특별한 뜻을 가진 기호(메타문자)들입니다. 오늘 쓸 핵심 여섯:
\d: 숫자 하나 (0~9)\w: 글자 하나 (영문, 숫자, 밑줄).: 아무 글자나 하나+: 앞의 것이 1개 이상*: 앞의 것이 0개 이상{n,m}: 앞의 것이 n개 이상 m개 이하
일반 글자는 그냥 그 글자를 뜻합니다. 그래서 \d+\.\d+는 "숫자 하나 이상, 점, 숫자 하나 이상"이라는 패턴입니다. 암호가 아니라 축약일 뿐입니다.
2-2. raw 문자열 — 역슬래시 보호막
정규식에는 \가 많이 나오는데, 파이썬 문자열에서도 \는 특수 취급(예: \n은 줄 바꿈)됩니다. 충돌을 막기 위해 정규식 패턴은 항상 r"..."(raw 문자열)로 씁니다. 앞의 r은 "있는 그대로 해석할"이라는 표시입니다. 이것은 규칙이라기보다 예방접종입니다 — 안 붙이면 언젠가 반드시 사고가 납니다.
2-3. re 모듈 — 세 가지 질문법
파이썬의 정규식 도구는 표준 라이브러리 re입니다. 자주 쓰는 질문이 세 가지입니다.
re.findall(패턴, 텍스트): 패턴에 맞는 것을 전부 리스트로re.search(패턴, 텍스트): 첫 번째로 맞는 것 하나. 없으면 Nonere.fullmatch(패턴, 텍스트): 텍스트 전체가 패턴과 일치하는가
오늘은 findall을 주력으로 씁니다 — "뽑아 내기"에는 이것이 정답이니까요.
2-4. 그룹 — 패턴의 일부만 가져오기
괄호 ( )로 묶은 부분은 "이 부분만 따로 주세요"라는 표시가 됩니다. 이것을 그룹 캡처(group capture)라고 합니다. 날짜 2026-09-08에서 연도만 빼고 싶다면 (\d+)-\d+-\d+처럼 연도 부분을 괄호로 묶습니다.
3. 따라 하기
3-1. 첫 패턴 — 숫자 뽑기
입력 (re1.py)
import re
text = "포트 80과 443이 열리고, 22는 닫혔다"
result = re.findall(r"\d+", text)
print(result)
출력 (2026-09-09 실측):
['80', '443', '22']
읽는 법: \d+ — "숫자(\d)가 하나 이상(+) 연속된 덩어리"를 전부(findall) 찾았습니다. 뽑힌 것은 숫자가 아니라 문자열이라는 것도 기억하세요. 계산하려면 int() 변환이 필요합니다.
3-2. 개수 지정 — 정확히 몇 자리
입력
text = "전화 010-1234-5678, 사서함 12345"
print(re.findall(r"\d{4}", text))
print(re.findall(r"\d{3}-\d{4}-\d{4}", text))
출력 (2026-09-09 실측):
['1234', '5678', '1234']
['010-1234-5678']
읽는 법: \d{4}는 "숫자 정확히 4개"입니다. 전화번호의 중간·끝 네 자리와, 사서함 번호(12345)의 앞 네 자리가 잡혀 세 개가 나왔습니다. 두 번째 패턴은 "숫자3개-숫자4개-숫자4개"라는 전화번호 모양 그 자체라 정확히 하나만 잡힙니다. 패턴이 구체적일수록 잡히는 것이 정확해집니다.
예측:
\d{2,4}는 무엇을 잡을까요? "2개 이상 4개 이하"입니다. 실측 결과는['010', '1234', '5678', '1234']— 12345에서는 앞의 1234만 잡히는 것에 주목하세요. 긴 덩어리를 만나면 최대치(4개)까지 욕심껏 잡는 것이 정규식의 기본 성질입니다.
3-3. IP 주소 뽑기 — 오늘의 주력 훈련
입력 (re3.py)
import re
log = "접속 192.168.0.23 성공, 접속 10.0.0.7 실패, 공격 시도 1.2.3.4 탐지"
ips = re.findall(r"\d+\.\d+\.\d+\.\d+", log)
print(ips)
출력 (2026-09-09 실측):
['192.168.0.23', '10.0.0.7', '1.2.3.4']
읽는 법: \d+\.\d+\.\d+\.\d+ — "숫자 덩어리, 점, 숫자 덩어리, 점, 숫자 덩어리, 점, 숫자 덩어리". 점 앞의 \는 "진짜 점 문자"라는 뜻입니다. 그냥 .은 "아무 글자나"이므로, 점 자체를 찾을 때는 \.으로 눌러 줍니다.
주의할 한계: 이 패턴은 모양만 봅니다. 실험해 보면 999.999.999.999 같은 가짜 IP도 잡힙니다 (2026-09-09 실측 확인). 정교한 검증(각 덩어리가 255 이하인지)은 나중의 과제로 남깁니다.
3-4. 이메일 모양 뽑기
입력
text = "문의는 admin@example.com 또는 support@test.org 로"
emails = re.findall(r"\w+@\w+\.\w+", text)
print(emails)
출력 (2026-09-09 실측):
['admin@example.com', 'support@test.org']
읽는 법: \w+@\w+\.\w+ — "글자 덩어리, @, 글자 덩어리, 점, 글자 덩어리". 단순하지만 꽤 쓸 만합니다. 실제 완벽한 이메일 정규식은 수백 자짜리 괴수지만, 실무에서는 이 정도 단순 패턴으로 대부분을 해결합니다.
3-5. 그룹으로 부분만 캡처
입력
text = "날짜: 2026-09-08, 날짜: 2025-12-31"
years = re.findall(r"(\d+)-\d+-\d+", text)
print(years)
출력 (2026-09-09 실측):
['2026', '2025']
읽는 법: 패턴 전체는 날짜 모양이지만, 괄호로 묶은 연도 부분만 리스트에 담겼습니다. "전체 모양으로 찾되, 가져올 것은 일부" — 이것이 그룹 캡처의 요령입니다.
괄호를 세 개 쓰면 어떻게 될까요? 실측 (2026-09-09):
[('2026', '09', '08'), ('2025', '12', '31')]
(연, 월, 일) 튜플들의 리스트가 나옵니다.
3-6. search와 판별 — "있느냐 없느냐"
입력
line = "ALERT: 접근 거부됨 from 10.0.0.7"
if re.search(r"ALERT", line):
print("경고 줄입니다!")
found = re.search(r"\d+\.\d+\.\d+\.\d+", line)
print(found.group())
출력 (2026-09-09 실측):
경고 줄입니다!
10.0.0.7
읽는 법: search는 "찾았다/못 찾았다"를 돌려주니 if와 잘 어울립니다. 찾은 내용물은 .group()으로 꺼냅니다. 못 찾았을 때는 None이 돌아옵니다 — .group()을 쓰기 전에 찾았는지 확인하는 습관을 들이세요 (벽 3에서 사고를 봅니다).
3-7. 문자 클래스 — "이 중 하나" 찾기
대괄호 [ ]는 "이 안의 글자 중 하나"라는 뜻입니다.
입력
import re
text = "파일: a1.txt, b2.txt, c10.txt"
print(re.findall(r"[abc]\d", text))
print(re.findall(r"[a-z]\d+", text))
출력 (2026-09-09 실측):
['a1', 'b2', 'c1']
['a1', 'b2', 'c10']
읽는 법: [abc]는 a 또는 b 또는 c. [a-z]는 a부터 z까지 아무 소문자. 첫 패턴은 "a/b/c + 숫자 하나"라서 c10이 c1만 잡히고, 두 번째는 + 덕분에 통째로 잡힙니다. 대괄호 안에서 -는 범위, ^는 "아닌 것"입니다 — [^0-9]는 "숫자가 아닌 것 하나".
3-8. 검증 패턴 — "이 입력은 맞는가"
뽑기(findall)와 다른 용도가 있습니다. 검증 — "사용자 입력이 규칙에 맞는가"를 검사합니다.
입력
import re
def is_valid_port(text):
return re.fullmatch(r"\d{1,5}", text) is not None
print(is_valid_port("8080"))
print(is_valid_port("80a0"))
print(is_valid_port(""))
출력 (2026-09-09 실측):
True
False
False
읽는 법: re.fullmatch는 "텍스트 전체가 패턴과 정확히 일치하는가"를 묻습니다. 일부만 맞아도 안 됩니다. search와의 차이가 중요합니다 — search는 "안에 있니?", fullmatch는 "전부가 이 모양이니?". 검증에는 반드시 fullmatch를 씁니다.
왜: 입력 검증은 Step 46 방어적 프로그래밍의 전진 배치판입니다. "규칙에 맞는 것만 받는다"를 정규식으로 표현하면, 이상한 입력이 프로그램 내부로 못 들어옵니다.
4. 미션과 연습문제
미션 — IP 추출 함수 완성하기
extract_ips(text) 함수를 완성하세요. 요구사항:
- 임의의 텍스트를 받아 IPv4 모양을 전부 뽑아 리스트로 돌려주는 함수를 작성한다
- 반환하기 전에 중복을 제거한다 (힌트: Step 45의 set)
- 사설 IP만 따로 골라내는 기능을 추가한다 — 뽑은 IP가 "192.168." 또는 "10."으로 시작하는지는 문자열의
startswith로 검사 - 직접 만든 가짜 로그 파일(log_sample.txt, IP가 여러 개 섞인 다섯 줄 이상)을 읽어 와서 함수를 시험한다
- 결과를 "전체 IP 수, 서로 다른 IP 수, 그중 사설 IP 목록"으로 요약 출력한다
보너스 도전: flag{...} 모양을 찾는 패턴 flag\{[^}]+\}을 해석해 보세요.
연습문제
문제 1. \d, \w, ., +, *, {n,m} 여섯 기호의 뜻을 각각 말해 보세요.
문제 2. 정규식 패턴을 쓸 때 raw 문자열(r"...")을 쓰는 이유는 무엇인가요?
문제 3. re.search와 re.fullmatch의 차이를 설명하고, "사용자 입력 검증"에는 어느 쪽을 써야 하는지 말해 보세요.
문제 4. 패턴 \d+\.\d+\.\d+\.\d+는 999.999.999.999도 IP로 잡습니다. 이 한계의 이유를 설명하고, 이 패턴이 그래도 유용한 이유를 말해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
import re
def extract_ips(text):
found = re.findall(r"\d+\.\d+\.\d+\.\d+", text)
return list(set(found))
def private_only(ips):
result = []
for ip in ips:
if ip.startswith("192.168.") or ip.startswith("10."):
result.append(ip)
return result
with open("log_sample.txt", encoding="utf-8") as f:
log = f.read()
ips = extract_ips(log)
privates = private_only(ips)
print(f"전체 IP 수(중복 포함): {len(re.findall(r'\d+\.\d+\.\d+\.\d+', log))}")
print(f"서로 다른 IP 수: {len(ips)}")
print(f"사설 IP: {privates}")
검증하는 법: ① 가짜 로그에 같은 IP를 세 번 적고, "서로 다른 IP 수"가 1로 세어지는지 확인. ② 192.168.x.x와 10.x.x.x, 그리고 8.8.8.8(공인)을 섞어 사설만 골라지는지 확인. ③ "1.2.3"처럼 덩어리가 세 개뿐인 가짜는 안 잡히는지 확인.
보너스 해설: flag\{[^}]+\}는 "flag, 진짜 여는 중괄호(\{), 닫는 중괄호가 아닌 것 1개 이상([^}]+), 진짜 닫는 중괄호"라는 뜻입니다. 실측으로 결과는 flag{h3ll0_w0rld} 이고에서 flag{h3ll0_w0rld}가 정확히 잡힙니다 (2026-09-09).
연습문제 해답
문제 1 해답. \d 숫자 하나, \w 글자 하나(영문·숫자·밑줄), . 아무 글자나 하나, + 앞의 것 1개 이상, * 앞의 것 0개 이상, {n,m} 앞의 것 n개 이상 m개 이하.
문제 2 해답. 파이썬 문자열에서 \는 \n(줄 바꿈) 같은 특수 의미로 해석되는데, 정규식도 \를 많이 쓰기에 둘이 충돌하기 때문입니다. r"..."은 "있는 그대로 해석하라"는 표시로, 패턴의 \d가 파이썬에 의해 깨지는 것을 막습니다.
문제 3 해답. search는 "텍스트 안에 패턴이 있느냐", fullmatch는 "텍스트 전체가 패턴 그대로이냐"를 묻습니다. 입력 검증에는 fullmatch를 씁니다 — "8080abc"처럼 앞부분만 맞는 입력을 search는 통과시키기 때문입니다.
문제 4 해답. 이 패턴은 "모양"만 보고 각 덩어리의 값 범위(0~255)는 검사하지 않기 때문입니다. 그래도 유용한 이유: 로그에 등장하는 "IP 모양"의 대부분은 진짜 IP라서, 1차 수집용으로는 이 단순 패턴이면 충분합니다. 수집 후 범위 검사를 덧붙이는 식으로 단계를 나누는 것이 실무적입니다.
완료 기준 체크리스트
- [ ] \d, \w, ., +, *, {n,m}의 뜻을 설명할 수 있다
- [ ] raw 문자열(r"")이 필요한 이유를 안다
- [ ] findall로 패턴에 맞는 조각을 뽑을 수 있다
- [ ] 그룹 ( )으로 일부만 캡처할 수 있다
- [ ] search와 fullmatch를 구분해 검증에 fullmatch를 쓴다
- [ ] 진짜 점은 .으로 찾아야 함을 안다
- [ ] 미션: extract_ips 함수와 사설 IP 분류를 완성했다
6. 흔한 실수와 해결
벽 1. 패턴이 하나도 안 잡힌다
증상: 분명 있을 텐데 빈 리스트 []가 나옵니다.
원인: 대표적으로 세 가지. r을 빠뜨려 \d가 깨진 경우, 패턴의 철자 실수, 찾으려는 모양과 패턴이 미묘하게 다른 경우(공백, 대소문자).
해결: 패턴을 가장 단순한 것부터 쌓아 올리세요. \d로 시작해 되면 \d+, 그다음 \. 추가. 한 번에 완성하려 하지 않는 것이 정규식 작업의 철칙입니다.
벽 2. 점을 찾으려는데 엉뚱한 것이 잡힌다
증상: "151", "1a1" 같은 것까지 잡힙니다. 실측 비교 (2026-09-09):
패턴 r"1.1" -> ['151', '1a1', '1.1']
패턴 r"1\.1" -> ['1.1']
원인: .은 "아무 글자나 하나"라는 뜻입니다. 진짜 점은 \.입니다.
해결: 특수 의미가 있는 글자(., *, +, ?, {, }, [, ], (, ), ^, $, , |)를 글자 자체로 찾을 때는 앞에 \를 붙입니다. 이것을 이스케이프(escape)라고 합니다.
벽 3. NoneType에는 group이 없다는 오류
증상 (2026-09-09 실측):
AttributeError: 'NoneType' object has no attribute 'group'
원인: search가 못 찾아서 None을 돌려줬는데, 바로 .group()을 호출한 것입니다. 없는 물건의 뚜껑을 연 셈입니다.
해결: if found: 검사 후에만 .group()을 씁니다. Step 46의 방어적 프로그래밍이 여기서도 통합니다.
벽 4. 너무 짧은 패턴이 반쯤만 잡는다
증상: IP를 잡았는데 앞 두 덩어리만 잡히거나, 반대로 주변 글자까지 딸려 옵니다.
원인: 패턴이 실제 모양보다 짧거나(덜 구체적) 긴 경우입니다. \d+\.\d+는 IP의 앞 두 덩어리만 잡습니다.
해결: 잡고 싶은 대상의 "정확한 모양"을 글로 먼저 써 보세요. "숫자덩어리 점 숫자덩어리 점 숫자덩어리 점 숫자덩어리". 그 글이 그대로 패턴의 설계도입니다.
벽 5. 긴 숫자 덩어리가 잘린다
증상: 12345를 잡고 싶은데 1234만 잡힙니다.
원인: \d{4}는 "정확히 4개"라서 다섯 자리의 앞 네 자리만 잡습니다 (3-2절 실측에서 사서함 12345가 1234로 잡힌 것이 그 사례입니다).
해결: 의도에 맞게 고르세요. 정확한 자릿수면 {4}, "최소 4자리"면 {4,}, 범위면 {4,6}. 긴 덩어리를 만나면 최대치까지 욕심껏 잡는 것이 기본 성질임도 함께 기억하세요.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 정규표현식(regex) | "모양을 묘사하는 언어" — 모양으로 찾기 |
| 메타문자 | 특별한 뜻의 기호 (\d, \w, ., +, *, {n,m}) |
| raw 문자열 | r"…" — 역슬래시 충돌 예방 접종 |
| 그룹 캡처 | ( )로 묶은 부분만 따로 가져오기 |
| 이스케이프 | 특수 글자를 글자 자체로 찾기 (. 등) |
| 문자 클래스 | [ ] — "이 안의 글자 중 하나" |
오늘의 함수·기호
| 도구 | 하는 일 |
|---|---|
re.findall(패턴, 텍스트) |
맞는 것 전부 리스트로 |
re.search(패턴, 텍스트) |
첫 하나. 없으면 None |
re.fullmatch(패턴, 텍스트) |
전체 일치 검증 (검증용) |
\d \w . |
숫자 / 글자 / 아무 글자 하나 |
+ * {n,m} |
1개 이상 / 0개 이상 / 개수 범위 |
[abc] [^0-9] |
이 중 하나 / 이것이 아닌 것 |
\. \{ |
진짜 점 / 진짜 괄호 |
명령어보다 중요한 감각
정규식 공부의 절반은 쓰기가 아니라 남의 패턴 읽기입니다. 인터넷의 코드에서 \w+@를 만나면 건너뛰지 말고 한 기호씩 풀어 보세요 — "글자 덩어리, 골뱅이". 실무자도 정규식은 외우지 않고 검색합니다. 검색해 쓰되, 읽을 줄 아는 사람이 되는 것이 목표입니다.
보안과의 연결: CTF 대회의 정답은 보통 flag{...} 같은 약속된 모양으로 숨겨져 있고, 로그에서 침입자의 IP를 찾는 일도 오늘의 기술 그대로입니다. 실시간 시험대 regex101.com에 오늘 패턴들을 옮겨 쳐 보면, 잡히는 부분이 색으로 바로 보여 감각이 빨리 붙습니다. 처음 완성한 \d+\.\d+\.\d+\.\d+를 노트에 남겨 두세요 — 정규식 세계로의 입국 도장입니다.
전부 체크되면 Step 48 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.