6주차: 정규표현식, Pandas 문자열, XML, JSON
이번 강의에서 배울 것
- 정규표현식 메타문자(^ $ . \s \S * + ? [] {} ())의 뜻을 읽고 쓸 수 있다.
- re 모듈의 match, search, findall, finditer의 차이를 설명할 수 있다.
- 괄호 그룹으로 원하는 부분만 추출할 수 있다.
- Pandas의 .str 접근자로 열 전체의 문자열을 가공·검색·치환할 수 있다.
- ElementTree로 XML을 탐색·수정·생성하고, json 모듈로 JSON과 파이썬 객체를 변환할 수 있다.
학습 경로
이전: 5주차 · NumPy · 다음: 7주차 · 데이터 클리닝과 시각화
범위: 6주차 슬라이드 3–47 · Drill 601–606 · 실습 노트북 RPY6주차.ipynb · 실습 파일 mbox-short.txt, country_data.xml, person.json
단원 도입
| 핵심 포인트 | 중요도 | 이렇게 확인한다 |
|---|---|---|
메타문자 표(^ $ . \s \S * + [ ] ( )) | ★★★ | 패턴을 보고 어떤 문자열과 매치되는지 판단하기 |
match(처음부터) vs search(전체에서) | ★★★ | "3 python"에 두 메서드를 적용한 결과 쓰기 |
findall(리스트) vs finditer(반복자) | ★★ | 결과의 자료형과 두 번 순회했을 때의 차이 |
| 괄호 그룹으로 숫자만 추출 | ★★ | ^X\S*: ([0-9.]+)의 결과 예측하기 |
.str 접근자와 regex=True | ★★ | split(', ').str.get(1) 결과 쓰기 |
XML find·findall·iter, get vs .text | ★★ | 태그·속성·텍스트를 꺼내는 코드 쓰기 |
JSON loads(문자열) vs load(파일), dumps | ★★ | 함수 이름의 s 의미 설명하기 |
형광펜 읽는 법: 노랑 핵심 정의·결론 · 빨강 헷갈리기 쉬운 구분·함정 · 파랑 외워야 할 순서·목록
코드 읽는 법: 코드 옆 주석의 # → 뒤가 실제 실행 결과다. 패턴 문자열은 모두 raw string(r'...')으로 적었다. 이유는 raw string을 쓰는 이유에서 설명한다.
PART 1 · 정규표현식 문법
정규표현식이란
3p~4p
- 찾고자 하는 문자열, 더 나아가 문자열 관련 ‘정보’를 효율적으로 검색하기 위한 표현 방법
- 컴퓨터 언어의 문법이라기보다 텍스트 매칭과 서칭을 위한 작은 언어
- 웹 프로그래밍처럼 문자열을 많이 다루는 분야에서는 거의 필수
- 와일드카드(wild card) 기법: 카드 게임의 조커처럼 어떤 문자든 대신할 수 있는 기호를 쓴다
- 배우고 기억하기는 어렵다 → 아래 표를 반복해서 본다
메타문자 표
5p
| 기호 | 뜻 | 예 |
|---|---|---|
^ | 줄의 시작 | ^From → From으로 시작 |
$ | 줄의 끝 | Will$ → Will로 끝남 |
. | 아무 문자 하나 (줄바꿈 \n 제외) | a.b → aab, a0b |
\s | 공백 문자 하나 | 스페이스, 탭, 줄바꿈 |
\S | 공백이 아닌 문자 하나 | 글자, 숫자, @ 등 |
* | 앞 문자를 0번 이상 반복 | ca*t → ct, cat, caat |
*? | 0번 이상 반복, 가능한 한 적게 (non-greedy) | |
+ | 앞 문자를 1번 이상 반복 | ca+t → cat, caat |
+? | 1번 이상 반복, 가능한 한 적게 (non-greedy) | |
[aeiou] | 목록 안의 문자 하나 | 모음 하나 |
[^XYZ] | 목록에 없는 문자 하나 | X, Y, Z가 아닌 문자 |
[a-z0-9] | 범위로 지정한 문자 하나 | 소문자 또는 숫자 |
( ) | 추출할 부분의 시작과 끝 (그룹) | rev=([0-9]+) → 숫자만 추출 |
문자 클래스 [ ]
6p~9p
[ ] 사이의 문자 중 한 개와 매치된다.
import re
for s in ["a", "before", "dude"]:
print(s, bool(re.search(r"[abc]", s)))
# → a True ← "a"가 있음
# → before True ← "b"가 있음
# → dude False ← a, b, c 중 아무것도 없음대괄호 안에서 두 문자 사이의 하이픈 -는 범위(From–To)를 뜻한다. [a-c]는 [abc], [0-5]는 [012345]와 같다. [a-zA-Z]는 알파벳 전체, [0-9]는 숫자 전체다.
자주 쓰는 클래스는 줄임 표기가 있다.
| 줄임 | 같은 표현 | 뜻 |
|---|---|---|
\d | [0-9] | 숫자 |
\D | [^0-9] | 숫자가 아닌 것 |
\s | [ \t\n\r\f\v] | 공백 문자 (맨 앞 칸은 스페이스) |
\S | [^ \t\n\r\f\v] | 공백이 아닌 것 |
\w | [a-zA-Z0-9_] | 영문자·숫자·밑줄 (alphanumeric) |
\W | [^a-zA-Z0-9_] | 위가 아닌 것 |
Dot(.) — 아무 문자 하나
10p~11p
for s in ["aab", "a0b", "abc"]:
print(s, bool(re.match(r"a.b", s)))
# → aab True ← 가운데 "a"가 . 과 매치
# → a0b True ← 가운데 "0"이 . 과 매치
# → abc False ← "a"와 "b" 사이에 문자가 없음반복: * + {m,n}
12p~16p
| 정규식 | 문자열 | 매치 | 이유 |
|---|---|---|---|
ca+t | ct | ✗ | a가 0번 → +는 1번 이상 필요 |
ca+t | cat | ○ | a 1번 |
ca+t | caaat | ○ | a 3번 |
ca{2}t | cat | ✗ | a가 1번뿐 |
ca{2}t | caat | ○ | a가 정확히 2번 |
ca{2,5}t | cat | ✗ | 1번은 범위 밖 |
ca{2,5}t | caat | ○ | 2번 |
ca{2,5}t | caaaaat | ○ | 5번 |
괄호 ( ) — 필요한 부분만 꺼내기
5p, 20p
괄호는 매치는 전체 패턴으로 하되, 결과로 돌려줄 부분만 지정한다.
line = "X-DSPAM-Confidence: 0.8475"
re.findall(r"^X\S*: [0-9.]+", line) # → ['X-DSPAM-Confidence: 0.8475'] 괄호 없음: 매치 전체
re.findall(r"^X\S*: ([0-9.]+)", line) # → ['0.8475'] 괄호 안만실습 노트북의 cell 3과 cell 4가 바로 이 차이를 보여 준다. 괄호가 있으면 숫자만 나오고, 없으면 'X-DSPAM-Confidence: 0.8475' 같은 줄 전체가 나온다.
PART 2 · 파이썬 re 모듈
mbox 파일에서 줄 찾기
18p~21p
mbox-short.txt는 이메일 기록을 이어 붙인 텍스트 파일이다. 각 예제는 “파일을 한 줄씩 읽고 → 끝의 줄바꿈을 지우고(rstrip) → 패턴을 검사”하는 같은 틀을 쓴다.
import re
with open("mbox-short.txt", encoding="utf-8") as hand:
for line in hand:
line = line.rstrip()
if re.search(r"^From:.+@", line):
print(line)
# → From: stephen.marquard@uct.ac.za
# → From: louis@media.berkeley.edu
# → ... (27줄)| 슬라이드 | 패턴 | 찾는 것 | 실제 결과 예 |
|---|---|---|---|
| 18p | ^From:.+@ | From:으로 시작하고, 아무 문자 1개 이상 뒤에 @가 있는 줄 | From: stephen.marquard@uct.ac.za |
| 19p | [a-zA-Z0-9]\S+@\S+[a-zA-Z] | 영문자·숫자로 시작해 공백 없이 @를 지나 영문자로 끝나는 부분 = 이메일 주소 | ['stephen.marquard@uct.ac.za'] |
| 20p | ^X\S*: ([0-9.]+) | X로 시작하는 헤더 줄의 숫자 | ['0.8475'] |
| 21p | ^Details: rev=([0-9.]+) | 아무것도 찾지 못함 (아래 정정) | 없음 |
raw string을 쓰는 이유
18p~22p 보충
파이썬 문자열에서 \는 \n(줄바꿈), \t(탭)처럼 특수 문자를 만든다. 정규식의 \s, \d, \b도 \로 시작하므로 일반 문자열로 쓰면 둘이 충돌한다.
"\b" # 파이썬이 백스페이스 문자로 바꿔 버림 → 정규식의 단어 경계가 아님
r"\b" # 역슬래시와 b 두 글자 그대로 → 정규식이 단어 경계로 해석re 모듈의 네 가지 검색 메서드
17p, 22p~23p
| 메서드 | 하는 일 | 결과 |
|---|---|---|
match() | 문자열의 처음부터 패턴과 맞는지 검사 | match 객체 또는 None |
search() | 문자열 전체에서 처음 맞는 곳을 찾음 | match 객체 또는 None |
findall() | 맞는 부분 문자열을 모두 찾음 | 리스트 |
finditer() | 맞는 부분을 모두 찾음 | 반복자(iterator), 각 원소는 match 객체 |
패턴을 여러 번 쓸 때는 re.compile()로 패턴 객체를 미리 만들어 두는 것이 편하다.
m = re.match(r"[a-z]+", "python") # 축약형: 한 번만 쓸 때
p = re.compile(r"[a-z]+") # 컴파일형: 여러 번 쓸 때
m = p.match("python")match vs search
24p~27p
p = re.compile(r"[a-z]+")
p.match("python") # → <re.Match object; span=(0, 6), match='python'>
p.match("3 python") # → None ← 첫 글자 "3"이 [a-z]가 아님
p.search("python") # → <re.Match object; span=(0, 6), match='python'>
p.search("3 python") # → <re.Match object; span=(2, 8), match='python'> ← 전체를 훑어 찾음findall vs finditer
28p~29p
p.findall("life is too short") # → ['life', 'is', 'too', 'short']
result = p.finditer("life is too short")
result # → <callable_iterator object at 0x...>
for r in result:
print(r.group()) # → life / is / too / shortmatch 객체의 메서드
30p~31p
| 메서드 | 돌려주는 것 |
|---|---|
group() | 매치된 문자열 |
start() | 매치 시작 위치 |
end() | 매치 끝 위치 (그 위치의 문자는 포함하지 않음) |
span() | (start, end) 튜플 |
m = p.match("python")
m.group(), m.start(), m.end(), m.span() # → ('python', 0, 6, (0, 6))
m = p.search("3 python")
m.group(), m.start(), m.end(), m.span() # → ('python', 2, 8, (2, 8))text = "3 Hello Python3 World 2026!"
m = re.compile(r"[a-zA-Z]+").search(text)
m.group(), m.start(), m.end() # → ('Hello', 2, 7)PART 3 · Pandas 문자열 처리
.str 접근자
32p
Series에 .str을 붙이면 파이썬 문자열 메서드를 열 전체에 한 번에 적용할 수 있다. 실습 데이터는 보스턴 마라톤 기록 1,000행이다.
import pandas as pd
boston_url = "https://andybek.com/pandas-marathon"
boston = pd.read_csv(boston_url)
boston.info()
# → RangeIndex: 1000 entries, 0 to 999
# → Name, Age, M/F, City, State, Country, Official Time, Overall, Gender, Years RanDrill 601 · 대소문자, 공백, 분리, 결합
boston.City.str.upper() # KERINGET, PORTLAND, ...
boston.City.str.lower() # keringet, portland, ...
boston.City.str.capitalize() # 첫 글자만 대문자: Machida-City → Machida-city
boston.City.str.swapcase() # 대소문자 뒤집기: Keringet → kERINGET
boston.Name.iloc[0:2].str.strip() # 앞뒤 공백 제거 → 'Kirui, Geoffrey', 'Rupp, Galen'boston.Name.str.split(", ")[0] # → [' Kirui', 'Geoffrey '] 0번 행의 리스트
boston.Name.str.split(", ").str.get(1) # → 'Geoffrey ', 'Galen ', ... 각 리스트의 1번 원소
boston.Name.str.split(", ").str.get(0) # → 'Kirui', 'Rupp', ...
boston["First Name"] = boston.Name.str.split(", ").str.get(1)
boston["Last Name"] = boston.Name.str.split(", ").str.get(0)
boston["M/F"].str.cat(boston.Age.astype(str), sep="_") # → 'M_24', 'M_30', ...Drill 602 · 자르기와 패턴 매치
33p
boston.Country.str.slice(0, 2, 1) # 'KEN' → 'KE'
boston.Country.str.slice(start=-2) # 'KEN' → 'EN' 뒤에서 두 글자
boston.Country.str.match("ITA") # 처음부터 ITA인지 True/False
boston[boston.Country.str.match("ITA")] # 이탈리아 선수 행만
wills = boston.Name.str.contains("Will") # 이름 어딘가에 Will이 있는지Drill 603 · contains와 replace에 정규식 쓰기
34p
boston.Name[boston.Name.str.contains(r"[wW]ill", regex=True)]
# → Vanos, William / Guzick, William F / ... / Williamson, David / ... (15명)
boston.Name[boston.Name.str.contains(r",\s[wW]ill$", regex=True)]
# → Cunha, Will / Swenson, Will / Vargas, Will / Rivera, Will (이름이 정확히 Will)
boston["Official Time"].str.replace(
r"(\d+):(\d+):(\d+)",
r"\1 hours, \2 minutes, and \3 seconds",
regex=True,
)
# → '2 hours, 09 minutes, and 37 seconds', ....str 메서드 | 하는 일 |
|---|---|
upper·lower·capitalize·swapcase·title | 대소문자 변환 |
strip·lstrip·rstrip | 공백 제거 |
split(sep) → .str.get(i) | 나눈 뒤 i번 조각 꺼내기 |
cat(other, sep) | 두 문자열 열 이어 붙이기 |
slice(start, stop, step) | 부분 문자열 |
match(pat) | 처음부터 패턴과 맞는지 |
contains(pat, regex=True, na=False) | 어딘가에 패턴이 있는지 |
replace(pat, repl, regex=True) | 패턴을 찾아 치환 |
PART 4 · 데이터 교환 형식: XML과 JSON
XML의 구조
35p~37p
<person>
<name>Chuck</name>
<phone type="intl">
+1 734 303 4456
</phone>
<email hide="yes" />
</person>Drill 604 · ElementTree로 XML 읽기
38p
country_data.xml은 나라 세 개(Liechtenstein, Singapore, Panama)의 순위·연도·1인당 GDP·이웃 나라를 담고 있다.
import xml.etree.ElementTree as ET
tree = ET.parse("country_data.xml") # 파일에서 읽기
root = tree.getroot() # 뿌리 요소 <data>
root.tag # → 'data'
root.attrib # → {} <data>에는 속성이 없음
for child in root:
print(child.tag, child.attrib)
# → country {'name': 'Liechtenstein'}
# → country {'name': 'Singapore'}
# → country {'name': 'Panama'}
root[0][1].text # → '2008' 0번 country의 1번 자식 <year>Drill 605 · 찾기, 수정, 저장
39p~41p
for neighbor in root.iter("neighbor"): # 깊이에 상관없이 모든 <neighbor>
print(neighbor.attrib)
# → {'name': 'Austria', 'direction': 'E'} ... (5개)
for country in root.findall("country"): # 바로 아래 자식 중 <country>
rank = country.find("rank").text # 첫 번째 <rank>의 텍스트
name = country.get("name") # 속성 name의 값
print(name, rank)
# → Liechtenstein 1 / Singapore 4 / Panama 68| 메서드 | 찾는 범위 | 결과 |
|---|---|---|
elem.find("tag") | 바로 아래 자식 중 첫 번째 | 요소 하나 또는 None |
elem.findall("tag") | 바로 아래 자식 전부 | 요소 리스트 |
elem.iter("tag") | 모든 후손(손자 이하 포함) | 반복자 |
elem.findtext("tag") | 바로 아래 첫 번째 자식의 텍스트 | 문자열 |
elem.get("attr") | 자기 자신의 속성 | 속성 값 문자열 |
elem.text | 자기 자신의 텍스트 | 문자열 |
수정하고 저장하기
for rank in root.iter("rank"):
new_rank = int(rank.text) + 1
rank.text = str(new_rank) # 텍스트 바꾸기 (문자열로 넣어야 함)
rank.set("updated", "yes") # 속성 추가
tree.write("output.xml")
for country in root.findall("country"):
rank = int(country.find("rank").text)
if rank > 50:
root.remove(country) # 순위가 50보다 낮은 나라 삭제
tree.write("output2.xml")output2.xml에는 Panama(순위 69)가 빠지고, 남은 두 나라의 <rank>가 <rank updated="yes">2</rank>, <rank updated="yes">5</rank>로 바뀌어 있다.
XML을 DataFrame으로
실습 노트북 cell 25~26
root = ET.fromstring(xml_data)
rows = []
for country in root.findall("country"):
rows.append({
"name": country.get("name"),
"rank": int(country.findtext("rank")),
"year": int(country.findtext("year")),
"gdppc": int(country.findtext("gdppc")),
"neighbors": [n.get("name") for n in country.findall("neighbor")],
})
df = pd.DataFrame(rows)
# name rank year gdppc neighbors
# 0 Liechtenstein 1 2008 141100 [Austria, Switzerland]
# 1 Singapore 4 2011 59900 [Malaysia]
# 2 Panama 68 2011 13600 [Costa Rica, Colombia]XML 만들기
42p
a = ET.Element("a") # 뿌리 요소
b = ET.SubElement(a, "b") # a의 자식
c = ET.SubElement(a, "c") # a의 자식
d = ET.SubElement(c, "d") # c의 자식
ET.dump(a)
# → <a><b /><c><d /></c></a>JSON이란
43p~45p
- XML보다 훨씬 간단한 데이터 교환 형식
- 사람이 읽고 쓰기 쉽고, 기계가 분석·생성하기도 쉬움
- 자바스크립트(JavaScript) 문법에 기반 → 웹에서 널리 쓰임
{"name": "Bob",
"languages": ["English", "Fench"]
}| JSON | 파이썬 |
|---|---|
객체 { } | dict |
배열 [ ] | list |
문자열 "..." | str |
| 숫자 | int, float |
true / false | True / False |
null | None |
Drill 606 · json 모듈
44p~47p
import json
person = '{"name": "Bob", "languages": ["English", "Fench"]}' # JSON 문자열
person_dict = json.loads(person) # 문자열 → 파이썬 dict
person_dict # → {'name': 'Bob', 'languages': ['English', 'Fench']}
person_dict["languages"] # → ['English', 'Fench']
with open("person.json") as f:
data = json.load(f) # 파일 → 파이썬 dict
person_json = json.dumps({"name": "Bob", "age": 12, "children": None})
person_json # → '{"name": "Bob", "age": 12, "children": null}'person_string = '{"name": "Bob", "languages": "English", "numbers": [2, 1.6, null]}'
person_dict = json.loads(person_string)
print(json.dumps(person_dict, indent=4, sort_keys=True))
# {
# "languages": "English",
# "name": "Bob",
# "numbers": [
# 2,
# 1.6,
# null
# ]
# }정답과 해설
1. re.match(r"^\d+", "abc 123") → None(맨 앞이 숫자가 아님). re.search(r"^\d+", "abc 123")도 → None. 패턴에 ^가 있어 “줄의 시작”에 숫자가 와야 하기 때문이다. ^를 빼고 re.search(r"\d+", ...)로 쓰면 '123'을 찾는다.
2. ['39772']. 첫 번째 rev는 뒤에 =이 아니라 &가 오므로 맞지 않고, 두 번째 rev=39772의 괄호 부분만 추출된다.
3. caat, caaaaaat. {2,}는 2번 이상이다.
4. .str.get(0)은 모든 행의 리스트에서 0번 조각을 꺼낸 Series(성 목록)이고, [0]은 0번 행의 리스트 하나([' Kirui', 'Geoffrey '])다.
5. country.get("name") → 'Panama', country.find("rank").text → '68'(문자열). 숫자로 쓰려면 int(...)로 감싼다.
6. loads는 JSON 문자열을, load는 열린 파일 객체를 받아 파이썬 객체로 바꾼다.
플래시카드
6주차 — 정규식 ^, $, ., \s, \S의 뜻은?
줄의 시작, 줄의 끝, 줄바꿈을 뺀 아무 문자 하나, 공백 문자 하나, 공백이 아닌 문자 하나.
6주차 — *, +, {m,n}의 차이는?
*는 0번 이상({0,}), +는 1번 이상({1,}), {m,n}은 m번 이상 n번 이하 반복.
6주차 — [^0-9]와 ^[0-9]의 차이는?
[^0-9]는 숫자가 아닌 문자 하나, ^[0-9]는 숫자로 시작하는 줄. 대괄호 안의 ^는 부정, 밖의 ^는 줄의 시작.
6주차 — match()와 search()의 차이는?
match는 문자열 처음부터만 검사하고, search는 전체를 훑어 처음 맞는 곳을 찾는다. "3 python"에 [a-z]+를 쓰면 match는 None, search는 'python'.
6주차 — findall()과 finditer()의 차이는?
findall은 문자열 리스트를, finditer는 match 객체를 하나씩 내는 반복자를 돌려준다. 반복자는 한 번 순회하면 소진된다.
6주차 — 정규식에서 괄호 ( )의 역할은?
매치는 전체 패턴으로 하되, 결과로 꺼낼 부분을 지정한다. ^X\S*: ([0-9.]+)는 숫자만 돌려준다. replace에서는 \1, \2로 다시 쓸 수 있다.
6주차 — Pandas str.replace에 정규식을 쓸 때 주의할 점은?
Pandas 2.0부터 기본값이 regex=False이므로 regex=True를 반드시 써야 한다.
6주차 — XML에서 .get(), .text, find, findall, iter의 차이는?
get은 속성 값, text는 태그 사이 내용. find는 바로 아래 자식 중 첫 번째, findall은 바로 아래 자식 전부, iter는 모든 후손을 찾는다.
6주차 — json.loads, json.load, json.dumps, json.dump는?
s는 string. loads는 JSON 문자열→객체, load는 파일→객체, dumps는 객체→JSON 문자열, dump는 객체→파일.
핵심 점검
- 정규식 패턴 문자열 앞에는 역슬래시 충돌을 막기 위해 을 붙인다.
+는 앞 문자를 ,*는 반복한다.- 문자열 처음부터 검사하는 메서드는 , 전체에서 찾는 메서드는 다.
findall의 결과 자료형은 다.^Details: rev=가 실제 mbox 줄과 맞지 않는 이유는Details:뒤에 이 먼저 오기 때문이다.- Pandas에서 열 전체에 문자열 메서드를 쓰려면 접근자를 붙인다.
- XML 요소의 속성 값은 , 태그 사이의 내용은 로 꺼낸다.
- JSON의
null은 파이썬에서 이 된다.
내용을 이해했다면 학습 완료를 체크해 주세요.