파이썬 XML 파싱부터 생성까지 완벽 가이드

API 연동 프로젝트를 하다가 XML 파일을 받았는데 어떻게 처리해야 할지 막막했던 경험, 있으신가요? JSON은 익숙한데 XML은 왠지 어렵게 느껴지죠. 실제로 개발자의 67%가 XML보다 JSON을 선호한다는 조사 결과가 있습니다.

하지만 여전히 많은 레거시 시스템, 공공 API, 설정 파일이 XML을 사용합니다. 이 글에서는 파이썬으로 XML을 다루는 4가지 방법을 실전 예제와 함께 완벽 정리했습니다.

XML이란? 3분 만에 이해하기

XML(eXtensible Markup Language)은 데이터를 저장하고 전송하기 위한 마크업 언어입니다. HTML과 비슷하게 생겼지만 목적이 다릅니다.

XML의 기본 구조

<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
    <book category="cooking">
        <title lang="en">Everyday Italian</title>
        <author>Giada De Laurentiis</author>
        <year>2005</year>
        <price>30.00</price>
    </book>
</bookstore>

  • 선언문: 첫 줄의 XML 버전과 인코딩 정보
  • 루트 엘리먼트: 모든 것을 감싸는 최상위 태그 (bookstore)
  • 자식 엘리먼트: 계층 구조로 데이터 표현
  • 속성: category, lang처럼 태그 안의 추가 정보

파이썬 XML 라이브러리 4가지 비교

어떤 라이브러리를 선택해야 할까?

라이브러리난이도속도메모리추천 용도
ElementTree⭐⭐빠름적음일반적인 XML 처리
lxml⭐⭐⭐매우 빠름보통대용량 XML, XPath
minidom느림많음간단한 XML 생성
BeautifulSoup⭐⭐보통보통깨진 XML 복구

결론: 대부분의 경우 ElementTree면 충분합니다. 대용량이거나 복잡한 쿼리가 필요하면 lxml을 쓰세요.

ElementTree로 XML 파싱하기 (기본편)

파일에서 XML 읽기

import xml.etree.ElementTree as ET

# XML 파일 파싱
tree = ET.parse('books.xml')
root = tree.getroot()

print(root.tag)  # bookstore
print(root.attrib)  # 루트의 속성들

모든 책 정보 출력하기

# 모든 book 엘리먼트 찾기
for book in root.findall('book'):
    title = book.find('title').text
    author = book.find('author').text
    price = book.find('price').text

    print(f"제목: {title}")
    print(f"저자: {author}")
    print(f"가격: ${price}\\n")

속성 값 가져오기

for book in root.findall('book'):
    category = book.get('category')  # 속성 값
    title = book.find('title')
    lang = title.get('lang')  # 언어 속성

    print(f"[{category}] {title.text} ({lang})")

실무 꿀팁: find()는 첫 번째 매칭만, findall()은 모든 매칭을 반환합니다. 헷갈리지 마세요!

문자열로 XML 처리하기

API 응답처럼 문자열 형태의 XML을 받을 때가 많습니다.

xml_string = '''
<users>
    <user id="1">
        <name>홍길동</name>
        <email>hong@example.com</email>
    </user>
    <user id="2">
        <name>김철수</name>
        <email>kim@example.com</email>
    </user>
</users>
'''

root = ET.fromstring(xml_string)

for user in root.findall('user'):
    user_id = user.get('id')
    name = user.find('name').text
    email = user.find('email').text
    print(f"ID {user_id}: {name} - {email}")

XPath로 복잡한 데이터 찾기

XPath는 XML에서 원하는 데이터를 찾는 강력한 방법입니다.

기본 XPath 패턴

# 특정 카테고리의 책만 찾기
cooking_books = root.findall(".//book[@category='cooking']")

# 가격이 30 이하인 책
cheap_books = root.findall(".//book[price<=30]")

# 모든 title 태그 (깊이 상관없이)
all_titles = root.findall(".//title")

# 영어 제목만
english_titles = root.findall(".//title[@lang='en']")

실전 예제: 조건부 검색

# 2005년 이후 출간된 요리책
recent_cooking = root.findall(
    ".//book[@category='cooking'][year>2005]"
)

for book in recent_cooking:
    print(book.find('title').text)

주의: ElementTree의 XPath는 제한적입니다. 복잡한 쿼리가 필요하면 lxml을 쓰세요.

XML 파일 생성하고 저장하기

처음부터 XML 만들기

# 루트 엘리먼트 생성
root = ET.Element('company')

# 자식 엘리먼트 추가
employee = ET.SubElement(root, 'employee', id='001')
ET.SubElement(employee, 'name').text = '박지성'
ET.SubElement(employee, 'position').text = '개발자'
ET.SubElement(employee, 'salary').text = '5000'

# 두 번째 직원
employee2 = ET.SubElement(root, 'employee', id='002')
ET.SubElement(employee2, 'name').text = '손흥민'
ET.SubElement(employee2, 'position').text = '디자이너'
ET.SubElement(employee2, 'salary').text = '4500'

# 파일로 저장
tree = ET.ElementTree(root)
tree.write('company.xml', encoding='utf-8', xml_declaration=True)

예쁘게 포맷팅해서 저장하기

import xml.dom.minidom as minidom

def prettify_xml(element):
    rough_string = ET.tostring(element, encoding='utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent="  ")

# 사용
pretty_xml = prettify_xml(root)
with open('company_pretty.xml', 'w', encoding='utf-8') as f:
    f.write(pretty_xml)

ElementTree는 기본적으로 들여쓰기를 하지 않습니다. 이 방법으로 가독성을 높이세요.

XML 데이터 수정하고 업데이트하기

특정 값 변경하기

tree = ET.parse('books.xml')
root = tree.getroot()

# 모든 책 가격을 10% 인상
for book in root.findall('book'):
    price_elem = book.find('price')
    current_price = float(price_elem.text)
    new_price = current_price * 1.1
    price_elem.text = str(round(new_price, 2))

# 변경사항 저장
tree.write('books_updated.xml', encoding='utf-8', xml_declaration=True)

엘리먼트 추가/삭제

# 새 책 추가
new_book = ET.SubElement(root, 'book', category='fiction')
ET.SubElement(new_book, 'title', lang='ko').text = '파이썬 마스터'
ET.SubElement(new_book, 'author').text = '김코딩'
ET.SubElement(new_book, 'year').text = '2025'
ET.SubElement(new_book, 'price').text = '25.00'

# 특정 책 삭제 (2005년 이전 책)
for book in root.findall('book'):
    year = int(book.find('year').text)
    if year < 2005:
        root.remove(book)

tree.write('books_modified.xml', encoding='utf-8', xml_declaration=True)

lxml로 고급 XML 처리하기

대용량 파일이나 복잡한 XPath가 필요할 때는 lxml이 최고입니다.

lxml 설치

pip install lxml

lxml의 강력한 XPath

from lxml import etree

tree = etree.parse('books.xml')

# 복잡한 XPath 쿼리
expensive_books = tree.xpath(
    '//book[price > 25]/title/text()'
)
print(expensive_books)  # ['Everyday Italian', ...]

# 조건부 속성 검색
categories = tree.xpath(
    '//book[@category="cooking"]/@category'
)

# 부모 노드 접근
authors = tree.xpath(
    '//book[title="Everyday Italian"]/../author/text()'
)

lxml의 속도 장점

# 10MB 이상의 대용량 XML 처리
from lxml import etree

# 이터레이터로 메모리 효율적 처리
for event, elem in etree.iterparse('huge_file.xml', tag='book'):
    title = elem.find('title').text
    print(title)
    elem.clear()  # 메모리에서 제거

lxml은 ElementTree보다 2-3배 빠르고, 대용량 파일 처리에서 압도적입니다.

네임스페이스가 있는 XML 다루기

SOAP API나 RSS 피드 같은 곳에서 자주 만나는 골칫거리입니다.

네임스페이스 예제

<rss xmlns:content="<http://purl.org/rss/1.0/modules/content/>">
    <item>
        <title>Python Tutorial</title>
        <content:encoded>Full article content here</content:encoded>
    </item>
</rss>

네임스페이스 처리하기

# 네임스페이스 정의
namespaces = {
    'content': '<http://purl.org/rss/1.0/modules/content/>'
}

root = ET.fromstring(xml_with_namespace)

# 네임스페이스를 명시해서 검색
for item in root.findall('.//item'):
    title = item.find('title').text
    content = item.find('content:encoded', namespaces).text
    print(f"{title}: {content}")

실무 팁: 네임스페이스를 딕셔너리로 정의해두면 코드가 훨씬 깔끔합니다.

XML과 JSON 상호 변환하기

API 개발하다 보면 XML을 JSON으로 바꿔야 할 때가 많습니다.

XML → JSON

import json

def xml_to_dict(element):
    result = {}

    # 속성 처리
    if element.attrib:
        result['@attributes'] = element.attrib

    # 텍스트 처리
    if element.text and element.text.strip():
        result['#text'] = element.text.strip()

    # 자식 엘리먼트 처리
    for child in element:
        child_data = xml_to_dict(child)
        if child.tag in result:
            # 같은 태그가 여러 개면 리스트로
            if not isinstance(result[child.tag], list):
                result[child.tag] = [result[child.tag]]
            result[child.tag].append(child_data)
        else:
            result[child.tag] = child_data

    return result

# 사용 예제
tree = ET.parse('books.xml')
root = tree.getroot()
data_dict = {root.tag: xml_to_dict(root)}

# JSON으로 저장
with open('books.json', 'w', encoding='utf-8') as f:
    json.dump(data_dict, f, indent=2, ensure_ascii=False)

JSON → XML

def dict_to_xml(tag, data):
    element = ET.Element(tag)

    if isinstance(data, dict):
        for key, value in data.items():
            if key == '@attributes':
                element.attrib.update(value)
            elif key == '#text':
                element.text = str(value)
            else:
                child = dict_to_xml(key, value)
                element.append(child)
    elif isinstance(data, list):
        for item in data:
            child = dict_to_xml(tag, item)
            return child
    else:
        element.text = str(data)

    return element

# JSON 읽어서 XML로 변환
with open('books.json', 'r', encoding='utf-8') as f:
    json_data = json.load(f)

root_tag = list(json_data.keys())[0]
root = dict_to_xml(root_tag, json_data[root_tag])

tree = ET.ElementTree(root)
tree.write('books_from_json.xml', encoding='utf-8', xml_declaration=True)

실전 프로젝트: 공공데이터 XML API 활용

날씨 정보 가져오기 (기상청 API 예제)

import requests

def get_weather_data(api_key, location):
    url = "<http://apis.data.go.kr/1360000/VilageFcstInfoService/getUltraSrtNcst>"

    params = {
        'serviceKey': api_key,
        'numOfRows': 10,
        'pageNo': 1,
        'dataType': 'XML',
        'base_date': '20251012',
        'base_time': '0600',
        'nx': 60,
        'ny': 127
    }

    response = requests.get(url, params=params)

    # XML 파싱
    root = ET.fromstring(response.content)

    items = root.findall('.//item')
    weather_data = {}

    for item in items:
        category = item.find('category').text
        value = item.find('obsrValue').text
        weather_data[category] = value

    return weather_data

# 사용
# weather = get_weather_data('YOUR_API_KEY', 'Seoul')
# print(f"기온: {weather.get('T1H')}°C")

RSS 피드 파서 만들기

def parse_rss_feed(url):
    response = requests.get(url)
    root = ET.fromstring(response.content)

    posts = []

    for item in root.findall('.//item'):
        post = {
            'title': item.find('title').text,
            'link': item.find('link').text,
            'description': item.find('description').text,
            'pubDate': item.find('pubDate').text
        }
        posts.append(post)

    return posts

# 사용 예제
# feed_url = "<https://example.com/rss>"
# articles = parse_rss_feed(feed_url)
# for article in articles[:5]:
#     print(f"{article['title']} - {article['pubDate']}")

XML 처리 시 자주 하는 실수 TOP 7

1. None 체크 안 하기

# ❌ 잘못된 코드
title = root.find('title').text

# ✅ 올바른 코드
title_elem = root.find('title')
title = title_elem.text if title_elem is not None else "제목 없음"

2. 인코딩 문제

# ✅ 항상 UTF-8 명시
tree.write('output.xml', encoding='utf-8', xml_declaration=True)

3. 메모리 관리 실수

# ❌ 대용량 파일을 한 번에 로드
tree = ET.parse('huge_file.xml')  # 메모리 부족!

# ✅ 이터레이터 사용
for event, elem in ET.iterparse('huge_file.xml'):
    # 처리 후 메모리에서 제거
    elem.clear()

4. XPath 문법 오류

# ❌ 슬래시 개수 주의
root.findall('/book')  # 작동 안 함

# ✅ 상대 경로 또는 절대 경로
root.findall('.//book')  # 모든 하위
root.findall('book')  # 바로 아래 자식만

5. 속성과 텍스트 혼동

# <book id="1">Python</book>

book = root.find('book')
book_id = book.get('id')  # 속성은 get()
book_title = book.text  # 텍스트는 .text

6. 네임스페이스 무시

# ❌ 네임스페이스 무시하면 찾을 수 없음
root.find('content:encoded')

# ✅ 네임스페이스 딕셔너리 사용
ns = {'content': 'http://...'}
root.find('content:encoded', ns)

7. 특수 문자 이스케이핑 누락

# ✅ 특수 문자는 자동으로 이스케이핑됨
element.text = "5 < 10 & 3 > 1"  # OK
# 저장 시: 5 &lt; 10 &amp; 3 &gt; 1

성능 최적화 5가지 전략

1. 필요한 부분만 파싱

# iterparse로 선택적 파싱
for event, elem in ET.iterparse('large.xml', events=('start', 'end')):
    if event == 'end' and elem.tag == 'book':
        # book만 처리
        process_book(elem)
        elem.clear()

2. lxml 활용

# ElementTree보다 2-3배 빠름
from lxml import etree
tree = etree.parse('file.xml')

3. XPath 대신 직접 순회

# 단순 구조는 XPath보다 직접 순회가 빠름
for book in root:
    if book.tag == 'book':
        title = book.find('title').text

4. 캐싱 활용

# 반복 검색은 결과 캐싱
books_cache = root.findall('.//book')

for book in books_cache:
    # 처리
    pass

5. 멀티프로세싱

from multiprocessing import Pool

def process_chunk(xml_chunk):
    # 각 청크 처리
    return result

# 큰 파일을 청크로 나눠서 병렬 처리
with Pool(4) as p:
    results = p.map(process_chunk, xml_chunks)

체크리스트: XML 처리 전 필수 점검

✅ 인코딩 확인 (UTF-8 권장)

✅ None 체크 로직 추가

✅ 대용량 파일은 iterparse 사용

✅ 네임스페이스 정의 확인

✅ 에러 핸들링 추가

✅ 출력 XML 포맷팅 확인

마치며: XML은 여전히 중요합니다

JSON이 대세지만 XML은 여전히 많은 곳에서 사용됩니다. 공공 API, 레거시 시스템, 설정 파일 등에서 XML을 피할 수 없습니다. 이 가이드의 패턴들을 익혀두면 어떤 XML도 자신 있게 다룰 수 있습니다.

처음엔 복잡해 보이지만, ElementTree의 기본 메서드 5개만 익히면 90%의 상황을 해결할 수 있습니다. find(), findall(), get(), text, attrib – 이것만 기억하세요!

이 글이 도움되셨다면 북마크하고, XML로 고생하는 동료에게 공유해주세요!


참고 자료

댓글 남기기