붙여주신 내용은 Markdown 코드블록과 Python 코드가 섞이면서 들여쓰기, triple quote, 정규식, YAML Frontmatter 문자열, sanitize_filename() 등이 깨진 상태입니다.
아래처럼 그대로 confluence_to_markdown.py로 저장해서 실행할 수 있는 형태로 정리했습니다.
#!/usr/bin/env python3
"""
Confluence Page to Markdown Exporter
- Confluence REST API (Cloud 또는 Server/Data Center)를 통해 문서를 조회
- Confluence 전용 매크로/태그(code, info panel, status 등)를 전처리
- markdownify 라이브러리를 사용해 깨끗한 Markdown으로 변환
- LLM/Vector DB 인덱싱을 위한 YAML Frontmatter 자동 주입
"""
import argparse
import os
import re
from typing import Any, Dict, Optional
import requests
from markdownify import markdownify as md
def clean_confluence_storage_xhtml(xhtml_content: str) -> str:
"""
Confluence Storage Format(XHTML) 고유 태그를
표준 HTML 태그로 1차 정규화합니다.
"""
if not xhtml_content:
return ""
content = xhtml_content
# ------------------------------------------------------------------
# 1. Confluence Code Block 매크로 변환
#
# <ac:structured-macro ac:name="code">
# ...
# <ac:plain-text-body><![CDATA[ ... ]]></ac:plain-text-body>
# ...
# </ac:structured-macro>
#
# =>
#
# <pre><code> ... </code></pre>
# ------------------------------------------------------------------
code_block_pattern = re.compile(
r'<ac:structured-macro[^>]*ac:name="code"[^>]*>.*?'
r'<ac:plain-text-body><!\[CDATA\[(.*?)\]\]></ac:plain-text-body>.*?'
r"</ac:structured-macro>",
re.DOTALL,
)
content = code_block_pattern.sub(
r"<pre><code>\1</code></pre>",
content,
)
# ------------------------------------------------------------------
# 2. Confluence Info / Note / Warning / Tip 패널 변환
#
# <ac:structured-macro ac:name="info"> ... </ac:structured-macro>
#
# =>
#
# <blockquote> ... </blockquote>
# ------------------------------------------------------------------
panel_pattern = re.compile(
r'<ac:structured-macro[^>]*ac:name="(info|note|warning|tip)"[^>]*>.*?'
r"<ac:rich-text-body>(.*?)</ac:rich-text-body>.*?"
r"</ac:structured-macro>",
re.DOTALL,
)
content = panel_pattern.sub(
r"<blockquote>\2</blockquote>",
content,
)
# ------------------------------------------------------------------
# 3. Confluence 전용 태그 제거
#
# ac:* / ri:* 태그 자체는 제거하고 내부 HTML 내용은 유지합니다.
# ------------------------------------------------------------------
content = re.sub(r"</?ac:[^>]*>", "", content)
content = re.sub(r"</?ri:[^>]*>", "", content)
# ------------------------------------------------------------------
# 4. 불필요한 XML namespace 제거
# ------------------------------------------------------------------
content = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', "", content)
return content
def get_confluence_page(
base_url: str,
page_id: str,
auth_token: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
verify_ssl: bool = True,
) -> Dict[str, Any]:
"""
Confluence REST API를 호출하여
페이지 메타데이터 및 Storage 본문을 조회합니다.
"""
url = (
f"{base_url.rstrip('/')}"
f"/rest/api/content/{page_id}"
f"?expand=body.storage,version,space"
)
headers = {
"Accept": "application/json",
}
auth = None
# ------------------------------------------------------------------
# Authentication
# ------------------------------------------------------------------
if auth_token:
# Personal Access Token(PAT) 또는 Bearer Token
headers["Authorization"] = f"Bearer {auth_token}"
elif username and password:
# Basic Auth
# Confluence Cloud에서는 password 자리에 API Token을 사용할 수 있음
auth = (username, password)
else:
raise ValueError(
"인증을 위해 auth_token 또는 username/password가 필요합니다."
)
response = requests.get(
url,
headers=headers,
auth=auth,
verify=verify_ssl,
timeout=15,
)
response.raise_for_status()
return response.json()
def convert_to_markdown_with_frontmatter(
page_data: Dict[str, Any],
base_url: str,
) -> str:
"""
페이지 메타데이터와 본문을 결합하여
YAML Frontmatter가 포함된 Markdown을 생성합니다.
"""
page_id = page_data.get("id", "")
title = page_data.get("title", "Untitled")
space_key = page_data.get("space", {}).get(
"key",
"UNKNOWN",
)
version = page_data.get("version", {}).get(
"number",
1,
)
last_modified = page_data.get("version", {}).get(
"when",
"",
)
# ------------------------------------------------------------------
# Storage Format 본문 추출
# ------------------------------------------------------------------
raw_storage = (
page_data
.get("body", {})
.get("storage", {})
.get("value", "")
)
# ------------------------------------------------------------------
# Confluence 전용 매크로 1차 정제
# ------------------------------------------------------------------
preprocessed_html = clean_confluence_storage_xhtml(
raw_storage
)
# ------------------------------------------------------------------
# HTML -> Markdown 변환
#
# heading_style="ATX"
# -> # Heading 형태
#
# autolinks=True
# -> URL 자동 링크
#
# strip
# -> script/style 제거
# ------------------------------------------------------------------
converted_md = md(
preprocessed_html,
heading_style="ATX",
autolinks=True,
code_language="bash",
strip=["script", "style"],
).strip()
# ------------------------------------------------------------------
# Source URL
# ------------------------------------------------------------------
source_url = (
f"{base_url.rstrip('/')}"
f"/pages/viewpage.action?pageId={page_id}"
)
# ------------------------------------------------------------------
# YAML Frontmatter
# ------------------------------------------------------------------
frontmatter = f"""---
page_id: "{page_id}"
title: "{title}"
space: "{space_key}"
version: {version}
last_modified: "{last_modified}"
source_url: "{source_url}"
---
# {title}
{converted_md}
"""
return frontmatter
def sanitize_filename(name: str) -> str:
"""
파일명으로 사용할 수 없는 특수문자를 '_'로 치환합니다.
"""
# Windows/Linux/macOS에서 문제가 될 수 있는 문자
return re.sub(
r'[\\/*?:"<>|]',
"_",
name,
).strip()
def main() -> None:
"""
CLI 진입점
"""
parser = argparse.ArgumentParser(
description=(
"Confluence 문서를 Markdown으로 변환하여 저장합니다."
)
)
parser.add_argument(
"--url",
default=os.getenv(
"CONFLUENCE_URL",
"https://confluence.internal.net",
),
help="Confluence Base URL",
)
parser.add_argument(
"--page-id",
required=True,
help="가져올 Confluence Page ID",
)
parser.add_argument(
"--token",
default=os.getenv("CONFLUENCE_TOKEN"),
help="Personal Access Token (PAT)",
)
parser.add_argument(
"--user",
default=os.getenv("CONFLUENCE_USER"),
help="Basic Auth 사용자명",
)
parser.add_argument(
"--password",
default=os.getenv("CONFLUENCE_PASSWORD"),
help="Basic Auth 비밀번호/API Token",
)
parser.add_argument(
"--output-dir",
default="./confluence_md",
help="저장 디렉터리 경로",
)
parser.add_argument(
"--no-verify-ssl",
action="store_true",
help="사내 사설 인증서 SSL 검증 비활성화",
)
args = parser.parse_args()
# ------------------------------------------------------------------
# Output directory
# ------------------------------------------------------------------
os.makedirs(
args.output_dir,
exist_ok=True,
)
print(
f"[*] Confluence 페이지 조회 중... "
f"(Page ID: {args.page_id})"
)
# ------------------------------------------------------------------
# Get Confluence page
# ------------------------------------------------------------------
try:
page_data = get_confluence_page(
base_url=args.url,
page_id=args.page_id,
auth_token=args.token,
username=args.user,
password=args.password,
verify_ssl=not args.no_verify_ssl,
)
except requests.exceptions.RequestException as exc:
print(
f"[!] Confluence API 호출 실패: {exc}"
)
return
except ValueError as exc:
print(
f"[!] 설정 오류: {exc}"
)
return
# ------------------------------------------------------------------
# Convert to Markdown
# ------------------------------------------------------------------
title = page_data.get(
"title",
f"page_{args.page_id}",
)
markdown_result = convert_to_markdown_with_frontmatter(
page_data,
args.url,
)
# ------------------------------------------------------------------
# Generate filename
# ------------------------------------------------------------------
safe_title = sanitize_filename(title)
output_file = os.path.join(
args.output_dir,
f"{safe_title}_{args.page_id}.md",
)
# ------------------------------------------------------------------
# Save
# ------------------------------------------------------------------
try:
with open(
output_file,
"w",
encoding="utf-8",
) as file:
file.write(markdown_result)
except OSError as exc:
print(
f"[!] Markdown 파일 저장 실패: {exc}"
)
return
print(
f"[+] Markdown 변환 완료: {output_file}"
)
if __name__ == "__main__":
main()
pip install requests markdownify
export CONFLUENCE_URL="https://confluence.example.com"
export CONFLUENCE_TOKEN="YOUR_TOKEN"
python3 confluence_to_markdown.py \
--page-id 123456789
사내 인증서 때문에 SSL 검증이 필요한 경우:
python3 confluence_to_markdown.py \
--page-id 123456789 \
--no-verify-ssl
Basic Auth를 사용하는 경우:
python3 confluence_to_markdown.py \
--url "https://confluence.example.com" \
--page-id 123456789 \
--user "username" \
--password "password-or-api-token"
생성 결과는 기본적으로:
./confluence_md/
└── 페이지제목_123456789.md
형태가 됩니다.
그리고 생성되는 Markdown의 앞부분은 다음처럼 됩니다.
---
page_id: "123456789"
title: "페이지 제목"
space: "DEVOPS"
version: 15
last_modified: "2026-09-27T10:20:30.000Z"
source_url: "https://confluence.example.com/pages/viewpage.action?pageId=123456789"
---
# 페이지 제목
한 가지 중요한 점: 원래 코드의 frontmatter는 ---가 깨져 있어서 실제 YAML Frontmatter가 아니었습니다. 위 버전에서는 ---로 정상적으로 열고 닫도록 수정했습니다.
또한 원본 코드에 있던 sanitize_filename()의 정규식도 잘못 깨져 있었는데, r'[\\/*?:"<>|]'로 수정했습니다.