1학년 / 2학기 / AI-R-PY / 6강

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+tct✗a가 0번 → +는 1번 이상 필요
ca+tcat○a 1번
ca+tcaaat○a 3번
ca{2}tcat✗a가 1번뿐
ca{2}tcaat○a가 정확히 2번
ca{2,5}tcat✗1번은 범위 밖
ca{2,5}tcaat○2번
ca{2,5}tcaaaaat○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")

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 / short

match 객체의 메서드

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 Ran

Drill 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 / falseTrue / False
nullNone

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은 파이썬에서 이 된다.
오늘도 한 걸음 앞으로.

내용을 이해했다면 학습 완료를 체크해 주세요.

학습 기록은 현재 브라우저에 저장됩니다.
HJS / STUDY한지석