Blogger XML 백업 파일에서 이미지를 옮길 때는 전체 XML 문자열을 한 번에 치환하는 방식보다, 글 본문 노드만 분리해서 처리하는 방식이 안전하다. 이번 기록은 Blogger 백업 XML 안의 본문 이미지 URL을 추출해 로컬 폴더에 저장하고, 글 본문 안의 src 경로만 로컬 상대경로로 바꾸는 파이썬 자동화 구현 과정이다.
목차
문제 상황
Blogger에서 내보낸 XML 백업 파일에는 게시글 본문, 라벨, 댓글 관련 정보, 템플릿과 위젯에 가까운 여러 데이터가 함께 들어간다. 겉으로는 하나의 XML 파일이지만, 내부에는 Atom 피드 구조와 HTML 본문 문자열이 섞여 있다. 이 상태에서 원격 이미지 주소를 모두 찾아 로컬 파일로 저장하고, 나중에 다른 환경에서도 참조할 수 있도록 경로를 바꾸는 작업이 필요했다.
처음에는 단순하게 XML 파일 전체에서 이미지 URL을 정규표현식으로 찾고, 다운로드한 뒤 str.replace()로 치환하면 충분해 보였다. 그러나 Blogger XML은 일반 HTML 문서가 아니라 구조화된 백업 파일이다. 전체 문자열을 대상으로 작업하면 실제 글 본문 이미지뿐 아니라 의도하지 않은 영역까지 함께 건드릴 수 있다.
에러 증상
문제는 세 방향에서 나타났다. 첫째, 브라우저에서는 열리는 이미지가 파이썬 요청에서는 실패할 수 있었다. 둘째, URL에 포함된 쿼리 문자열이나 특수문자가 로컬 파일명으로 들어가면서 Windows 파일 시스템에서 저장 오류가 발생할 수 있었다. 셋째, XML 전체를 무리하게 치환하면 백업 파일의 구조가 깨져 다시 가져오기나 후처리 과정에서 파싱 오류로 이어질 수 있었다.
이 작업에서 중요한 점은 “이미지를 다운로드한다”가 아니라 “Blogger XML의 구조를 보존한 채 글 본문 안의 이미지 참조만 바꾼다”는 것이다. 다운로드 성공률보다 XML 무결성이 더 우선이었다.
환경
- 대상 파일: Blogger에서 내보낸 XML 백업 파일
- 처리 대상:
atom:entry내부의atom:content본문 HTML - 사용 언어: Python 3
- 주요 모듈:
xml.etree.ElementTree,urllib.request,re - 출력 결과: 이미지 폴더와 경로가 수정된 새 XML 파일
시도했지만 실패한 방법
실패 가능성이 큰 방식은 XML 전체를 일반 텍스트 파일처럼 다루는 것이다.
전체 XML 읽기
→ 정규표현식으로 모든 이미지 URL 추출
→ 다운로드
→ XML 전체 문자열에서 URL 일괄 치환
→ 새 XML 저장
이 방식은 빠르지만 Blogger 백업 파일의 계층 구조를 고려하지 않는다. 글 본문과 무관한 위치에 있는 URL까지 같이 바뀔 수 있고, XML 엔티티 처리나 네임스페이스 구조를 우회하기 때문에 결과 파일을 다시 XML로 다룰 때 문제가 생길 수 있다. 특히 백업 파일을 장기 보관하거나 다른 도구로 재처리할 계획이 있다면 위험한 접근이다.
오류가 난 원인
이 문제의 핵심 원인은 처리 단위를 잘못 잡은 데 있다. Blogger XML에서 실제로 수정해야 하는 영역은 전체 파일이 아니라 각 게시글의 본문 HTML이다. Atom 구조 기준으로 보면 entry 안의 content가 주요 대상이다.
또 하나의 원인은 파일명 정제 누락이다. 원격 URL은 웹 주소로는 정상이어도 로컬 파일명으로 바로 사용할 수 없다. 예를 들어 경로 구분자, 쿼리 문자열, 예약 문자가 포함될 수 있다. 운영체제가 허용하지 않는 문자를 제거하지 않으면 다운로드 자체는 성공해도 파일 저장 단계에서 실패한다.
마지막으로 네트워크 요청은 항상 실패 가능성을 전제로 작성해야 한다. 이미지 서버가 응답하지 않거나, 응답이 이미지가 아니거나, 연결이 지연될 수 있다. 따라서 타임아웃, Content-Type 확인, 실패 시 원본 URL 유지 같은 방어 로직이 필요하다.
최종 해결
최종 구조는 다음과 같이 잡았다. 먼저 ElementTree로 XML을 파싱한다. 그다음 Atom 네임스페이스를 기준으로 각 entry를 순회하고, content 안의 본문 HTML 문자열에서만 img src를 추출한다. 다운로드에 성공한 URL만 로컬 상대경로로 치환하고, 실패한 URL은 원본을 유지한다.
이렇게 하면 작업 범위가 글 본문으로 제한된다. XML 전체 구조는 파서가 관리하고, 이미지 URL 치환은 본문 HTML 내부에서만 일어난다. 결과적으로 “다운로드 자동화”와 “백업 XML 구조 보존”을 동시에 만족할 수 있다.
사용한 코드
import os
import re
import sys
import time
import mimetypes
import urllib.request
import xml.etree.ElementTree as ET
from urllib.parse import urlparse, unquote
ATOM_NS = "http://www.w3.org/2005/Atom"
NS = {"atom": ATOM_NS}
IMG_SRC_PATTERN = re.compile(
r'<img\b[^>]*\bsrc=["\']([^"\']+)["\']',
re.IGNORECASE
)
def sanitize_filename(name: str, fallback: str) -> str:
name = unquote(name or "").split("?")[0].split("#")[0]
name = os.path.basename(name)
name = re.sub(r'[\\/:*?"<>|]+', "_", name).strip(" ._")
return name if name else fallback
def download_file(url: str, target_path: str, timeout: int = 15) -> bool:
request = urllib.request.Request(
url,
headers={
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
)
},
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
content_type = response.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
print(f"[SKIP] image가 아닌 응답: {url} ({content_type})")
return False
with open(target_path, "wb") as f:
f.write(response.read())
return True
except Exception as exc:
print(f"[FAIL] 다운로드 실패: {url} | {exc}")
return False
def migrate_blogger_xml_images(
source_xml: str,
image_dir: str = "blogger_images",
output_xml: str = "blogger_images_rewritten.xml",
local_prefix: str = "blogger_images",
delay: float = 0.2,
) -> None:
if not os.path.exists(source_xml):
raise FileNotFoundError(f"XML 파일을 찾을 수 없습니다: {source_xml}")
os.makedirs(image_dir, exist_ok=True)
ET.register_namespace("", ATOM_NS)
tree = ET.parse(source_xml)
root = tree.getroot()
total_found = 0
total_saved = 0
url_to_local: dict[str, str] = {}
for entry_index, entry in enumerate(root.findall("atom:entry", NS), start=1):
content = entry.find("atom:content", NS)
if content is None or not content.text:
continue
html = content.text
updated_html = html
for url in IMG_SRC_PATTERN.findall(html):
if not url.startswith(("http://", "https://")):
continue
total_found += 1
if url not in url_to_local:
parsed = urlparse(url)
base_name = sanitize_filename(
parsed.path,
fallback=f"entry_{entry_index}_image_{total_found}.jpg"
)
if not os.path.splitext(base_name)[1]:
base_name = base_name + ".jpg"
local_name = f"{total_found:04d}_{base_name}"
target_path = os.path.join(image_dir, local_name)
print(f"[GET] {url}")
if download_file(url, target_path):
total_saved += 1
url_to_local[url] = f"{local_prefix}/{local_name}"
else:
url_to_local[url] = url
time.sleep(delay)
updated_html = updated_html.replace(url, url_to_local[url])
content.text = updated_html
tree.write(output_xml, encoding="utf-8", xml_declaration=True)
print("[DONE]")
print(f"발견한 원격 이미지 URL: {total_found}")
print(f"저장 성공: {total_saved}")
print(f"출력 XML: {output_xml}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("사용법: python blogger_xml_image_migrator.py backup.xml [image_dir] [output.xml]")
sys.exit(1)
source = sys.argv[1]
images = sys.argv[2] if len(sys.argv) >= 3 else "blogger_images"
output = sys.argv[3] if len(sys.argv) >= 4 else "blogger_images_rewritten.xml"
migrate_blogger_xml_images(source, images, output)
검증 결과
- 원본 Blogger XML 파일을 별도로 백업한다.
- 스크립트를 실행해 이미지 폴더와 새 XML 파일이 생성되는지 확인한다.
- 콘솔 출력에서 발견한 이미지 수와 저장 성공 수를 확인한다.
- 생성된 XML 파일을 다시 XML 파서로 열어 구문 오류가 없는지 확인한다.
- 새 XML 안의 본문
img src가 의도한 상대경로로 바뀌었는지 일부 샘플을 확인한다. - 다운로드 실패 URL은 원본 주소로 유지되는지 확인한다.
python blogger_xml_image_migrator.py backup.xml blogger_images blogger_images_rewritten.xml
python -c "import xml.etree.ElementTree as ET; ET.parse('blogger_images_rewritten.xml'); print('XML OK')"
같은 문제 겪는 분들에게
- XML 전체 문자열 치환부터 하지 말고, 먼저 수정 대상 노드를 정한다.
- Blogger 백업 파일이면
atom:entry와atom:content구조를 먼저 확인한다. - 이미지 URL을 파일명으로 직접 쓰지 말고 반드시 정제한다.
- 다운로드 실패 시 XML에서 원본 URL을 지우지 않는다.
- 출력 XML은 반드시 다시 파싱해 구문 오류 여부를 확인한다.
- 실제 이전 작업에 쓰기 전 작은 XML 샘플로 먼저 테스트한다.
이 코드는 Blogger XML 내부의 글 본문 이미지를 로컬 경로로 바꾸는 기본형이다. 이미지를 클라우드 스토리지에 올리거나, CDN 주소로 재작성하거나, 병렬 다운로드를 적용하는 단계까지 포함하지는 않는다. 대량 파일에서 속도가 중요하다면 다운로드 큐, 재시도 횟수, 실패 로그 저장, 중복 URL 캐시 파일 같은 기능을 추가하는 편이 좋다.
관련 ToolSignal 기록
같은 아카이브의 다른 운영 기록입니다. 지금 겪는 문제와 가까운 기록을 골라 보세요.
- 구글 색인 누락 200% 극복 기록 (관련 글)
- Blogger 테마 XML 자동 업로드 기록 (관련 글)
- Blogger 라벨 아카이브 네비게이션 복구 기록 (관련 글)
- Blogger 라벨 페이지 H1 중복 해결 기록 (관련 글)
- AI 파헤치기 전체 글목록 (관련 글)