HWPX ๋ฌธ์๋ฅผ Python์ผ๋ก ์ฝ๊ณ , ํธ์งํ๊ณ , ์์ฑํฉ๋๋ค.
python-hwpx๋ ํ์ปด์คํผ์ค์ HWPX ํฌ๋งท์ ์์ Python์ผ๋ก ๋ค๋ฃจ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด์ CLI ๋๊ตฌ ๋ชจ์์
๋๋ค.
ํ/๊ธ ์ค์น ์์ด, OS์ ๊ด๊ณ์์ด HWPX ๋ฌธ์์ ๊ตฌ์กฐ๋ฅผ ํ์ฑํ๊ณ ์ฝํ
์ธ ๋ฅผ ์กฐ์ํ ์ ์์ต๋๋ค.
๋ฌธ์ ํธ์ง API๋ฟ ์๋๋ผ ์คํค๋ง/ํจํค์ง ๊ฒ์ฆ, unpack/pack, ํ
ํ๋ฆฟ ๋ถ์ ๊ฐ์ XML-first ์ํฌํ๋ก๋ ํจ๊ป ์ ๊ณตํฉ๋๋ค.
pyhwpx / pyhwp์ ๋ค๋ฅธ ์ ?
python-hwpx pyhwpx pyhwp ๋์ ํฌ๋งท .hwpx(OWPML/OPC).hwpx.hwp(v5 ๋ฐ์ด๋๋ฆฌ)ํ/๊ธ ์ค์น ๋ถํ์ ํ์ (Windows COM) ๋ถํ์ ํฌ๋ก์ค ํ๋ซํผ โ Linux / macOS / Windows / CI โ Windows ์ ์ฉ โ ๋ฐฉ์ ์ง์ XML ํ์ฑ COM ์๋ํ OLE ํ์ฑ
HWPX ํ์ผ์ ZIP + XML ๊ตฌ์กฐ์ด๋ฏ๋ก, ํ/๊ธ ํ๋ก๊ทธ๋จ ์์ด Python๋ง์ผ๋ก ์ฝ๊ณ ํธ์งํ๋ ์ํฌํ๋ก๋ฅผ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
| ํ๋ซํผ | ์ฝ๊ธฐ | ์ฐ๊ธฐ | ๋น๊ณ |
|---|---|---|---|
| โ Windows | โ | โ | ํ์ปด์คํผ์ค |
| โ macOS | โ | โ | ํ์ปด์คํผ์ค Mac |
| โ Linux | โ | โ | ํ์ปด์คํผ์ค Linux |
| โ CI/CD | โ | โ | Docker, GitHub Actions ๋ฑ |
pip install python-hwpx์ ์ผํ ์์กด์ฑ์
lxml์ ๋๋ค.
from hwpx import HwpxDocument
# ๋น ๋ฌธ์ ์๋ก ๋ง๋ค๊ธฐ
doc = HwpxDocument.new()
# ๊ธฐ์กด ๋ฌธ์๋ฅผ ์์ ํ๋ ค๋ฉด:
# doc = HwpxDocument.open("๋ณด๊ณ ์.hwpx")
# ๋ฌธ๋จ ์ถ๊ฐ
paragraph = doc.add_paragraph("python-hwpx๋ก ์์ฑํ ๋ฌธ๋จ์
๋๋ค.")
# ํ ์ถ๊ฐ (2ร3)
table = doc.add_table(rows=2, cols=3)
table.set_cell_text(0, 0, "์ด๋ฆ")
table.set_cell_text(0, 1, "๋ถ์")
table.set_cell_text(0, 2, "์ฐ๋ฝ์ฒ")
# ๋ฉ๋ชจ ์ถ๊ฐ (๊ธฐ๋ณธ ํ
ํ๋ฆฟ์ memo shape ์ฌ์ฉ)
doc.add_memo_with_anchor("๊ฒํ ํ์", paragraph=paragraph, memo_shape_id_ref="0")
# ์ ์ฅ
doc.save_to_path("๊ฒฐ๊ณผ๋ฌผ.hwpx")๐ก ์ปจํ ์คํธ ๋งค๋์ ๋ ์ง์ํฉ๋๋ค:
with HwpxDocument.open("๋ณด๊ณ ์.hwpx") as doc: doc.add_paragraph("์๋์ผ๋ก ๋ฆฌ์์ค๊ฐ ์ ๋ฆฌ๋ฉ๋๋ค.") doc.save_to_path("๊ฒฐ๊ณผ๋ฌผ.hwpx")
| ์นดํ ๊ณ ๋ฆฌ | ๊ธฐ๋ฅ | ์ค๋ช |
|---|---|---|
| ๐ ๋ฌธ์ I/O | ์ด๊ธฐ/์ ์ฅ/์์ฑ | ํ์ผ, ๋ฐ์ดํธ, ์คํธ๋ฆผ ์ ์ถ๋ ฅ ยท ์์์ ์ ์ฅ ยท ZIP ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ |
| ๐ ๋จ๋ฝ | ์ถ๊ฐ/์ญ์ /ํธ์ง/์์ | ํ
์คํธ ์ค์ , ๋จ๋ฝ ์ญ์ (remove_paragraph), ์คํ์ผ ์ฐธ์กฐ |
| โ๏ธ Run | ํ ์คํธ ์กฐ๊ฐ | ์ถ๊ฐ, ๊ต์ฒด, ๋ณผ๋/์ดํค๋ฆญ/๋ฐ์ค/์์ ์์ |
| ๐ ํ(Table) | ์์ฑ/ํธ์ง/๋ณํฉ | NรM ํ ์์ฑ, ์ ํ ์คํธ, ์ ๋ณํฉ/๋ถํ , ์ค์ฒฉ ํ ์ด๋ธ |
| ๐งญ ํ ์๋ํ | ํ์/์ฑ์ฐ๊ธฐ | ํ ์ด๋ธ ๋งต, ๋ผ๋ฒจ ๊ธฐ๋ฐ ์ ํ์, ๊ฒฝ๋ก ๊ธฐ๋ฐ ๋ฐฐ์น ์ฑ์ฐ๊ธฐ |
| ๐ ์น์ | ์ถ๊ฐ/์ญ์ | add_section(after=), remove_section(), manifest ์๋ ๊ด๋ฆฌ |
| ๐ผ๏ธ ์ด๋ฏธ์ง | ์๋ฒ ๋/์ญ์ | ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ ๊ด๋ฆฌ, manifest ์๋ ๋ฑ๋ก |
| โ๏ธ ๋ํ | ์ /์ฌ๊ฐํ/ํ์ | OWPML ๋ช ์ธ ์ค์ ๋ํ ์ฝ์ |
| ๐ ๋จธ๋ฆฌ๊ธ/๋ฐ๋ฅ๊ธ | ์ค์ /์ ๊ฑฐ | ํ์/์ง์/์์ชฝ ํ์ด์ง ๊ตฌ๋ถ |
| ๐ฌ ๋ฉ๋ชจ | ์ถ๊ฐ/์ญ์ | ์ต์ปค ๊ธฐ๋ฐ ๋ฉ๋ชจ, ๋ฉ๋ชจ ์ ฐ์ดํ ์ฐธ์กฐ |
| ๐ ๊ฐ์ฃผ/๋ฏธ์ฃผ | ์ถ๊ฐ | ํ ์คํธ ์ ๊ทผ |
| ๐ ๋ถ๋งํฌ/ํ์ดํผ๋งํฌ | ์ฝ์ /์กฐํ | URL ๋งํฌ, ๋ด๋ถ ๋ถ๋งํฌ |
| ๐ฐ ๋ค๋จ ํธ์ง | ์ปฌ๋ผ ์ ์ | ๋ค๋จ ๋ ์ด์์ ์ ์ด |
| ๐ ํ ์คํธ ์ถ์ถ | ํ์ดํ๋ผ์ธ | ์น์ /๋จ๋ฝ ์ํ, ์ฃผ์ ๋ ๋๋ง, ์ค์ฒฉ ๊ฐ์ฒด ์ ์ด |
| ๐ ๊ฐ์ฒด ๊ฒ์ | ํ๊ทธ/์์ฑ/XPath | ํน์ ์์ ํ์, ์ฃผ์ ์ดํฐ๋ ์ดํฐ |
| ๐จ ์คํ์ผ ์นํ | ์์ ๊ธฐ๋ฐ ํํฐ | ์์/๋ฐ์ค/charPrIDRef ๊ธฐ๋ฐ Run ๊ฒ์ ๋ฐ ๊ต์ฒด |
| ๐ค ๋ด๋ณด๋ด๊ธฐ | ํ ์คํธ/HTML/Markdown | ๋ฌธ์ ๋ณํ ์ถ๋ ฅ |
| โ ์ ํจ์ฑ ๊ฒ์ฌ | XSD + ํจํค์ง ๊ตฌ์กฐ | CLI(hwpx-validate, hwpx-validate-package) ๋ฐ API |
| ๐งฐ ์์ ๋๊ตฌ | unpack/pack/๋ถ์/๋น๊ต | pack-ready ์์ ๋๋ ํฐ๋ฆฌ ์ถ์ถ๊ณผ ์ฌ๊ตฌ์ฑ ์ ๊ฒ |
| ๐๏ธ ์ ์์ค XML | ๋ฐ์ดํฐํด๋์ค ๋งคํ | OWPML ์คํค๋ง โ Python ๊ฐ์ฒด ์ง์ ์กฐ์ |
| ๐ ๋ค์์คํ์ด์ค ํธํ | ์๋ ์ ๊ทํ | HWPML 2016 โ 2011 ์๋ ๋ณํ |
๋ฌธ๋จ, ํ, ๋ฉ๋ชจ, ๋จธ๋ฆฌ๊ธ/๋ฐ๋ฅ๊ธ์ Python ๊ฐ์ฒด๋ก ๋ค๋ฃน๋๋ค.
# ๋จ๋ฝ ์ถ๊ฐยท์ญ์
doc.add_paragraph("์ ๋ฌธ๋จ")
doc.remove_paragraph(doc.paragraphs[-1]) # ๋ง์ง๋ง ๋จ๋ฝ ์ญ์
# ์น์
์ถ๊ฐยท์ญ์
new_sec = doc.add_section() # ๋ฌธ์ ๋์ ์น์
์ถ๊ฐ
new_sec.add_paragraph("๋ ๋ฒ์งธ ์น์
๋ด์ฉ")
doc.remove_section(1) # ์ธ๋ฑ์ค๋ก ์น์
์ญ์
# ๋จธ๋ฆฌ๊ธยท๋ฐ๋ฅ๊ธ
doc.set_header_text("๊ธฐ๋ฐ ๋ฌธ์", page_type="BOTH")
doc.set_footer_text("1 / 10", page_type="BOTH")
# ํ ์
๋ณํฉยท๋ถํ
table.merge_cells(0, 0, 1, 1) # (0,0)~(1,1) ๋ณํฉ
table.set_cell_text(0, 0, "๋ณํฉ๋ ์
", logical=True, split_merged=True)
# ์์ํ ํ ์๋ ์ฑ์ฐ๊ธฐ
form = doc.add_table(2, 2)
form.cell(0, 0).text = "์ฑ๋ช
:"
form.cell(1, 0).text = "์์"
doc.find_cell_by_label("์ฑ๋ช
") # {"matches": [...], "count": 1}
doc.fill_by_path({
"์ฑ๋ช
> right": "ํ๊ธธ๋",
"์์ > right": "ํ๋ซํผํ",
})from hwpx import TextExtractor, ObjectFinder
# ํ
์คํธ ์ถ์ถ
with TextExtractor("๋ฌธ์.hwpx") as extractor:
for section in extractor.iter_sections():
for para in extractor.iter_paragraphs(section):
print(para.text())
# ํน์ ๊ฐ์ฒด ํ์
for obj in ObjectFinder("๋ฌธ์.hwpx").find_all(tag="tbl"):
print(obj.tag, obj.path)์์(์์, ๋ฐ์ค, charPrIDRef)์ผ๋ก ๋ฐ์ ํํฐ๋งํด ์ ํ์ ์ผ๋ก ๊ต์ฒดํฉ๋๋ค.
# ๋นจ๊ฐ์ ํ
์คํธ๋ง ์ฐพ์์ ์นํ
doc.replace_text_in_runs(
"์์", "ํ์ ",
text_color="#FF0000",
)
# ํน์ ์์์ ๋ฐ ๊ฒ์
runs = doc.find_runs_by_style(underline_type="SINGLE")# ํ
์คํธ, HTML, Markdown์ผ๋ก ๋ณํ
text = doc.export_text()
html = doc.export_html()
md = doc.export_markdown()OWPML ์คํค๋ง์ ๋งคํ๋ ๋ฐ์ดํฐํด๋์ค๋ก XML ๊ตฌ์กฐ๋ฅผ ์ง์ ๋ค๋ฃน๋๋ค.
# ํค๋ ์ฐธ์กฐ ๋ชฉ๋ก
doc.border_fills # ํ
๋๋ฆฌ ์ฑ์ฐ๊ธฐ
doc.bullets # ๊ธ๋จธ๋ฆฌํ
doc.styles # ์คํ์ผ
doc.track_changes # ๋ณ๊ฒฝ ์ถ์
# ๋ฐํ์ชฝยท์ด๋ ฅยท๋ฒ์ ํํธ
doc.master_pages
doc.histories
doc.versionpython-hwpx
โโโ hwpx.document # ๊ณ ์์ค ํธ์ง API (HwpxDocument)
โโโ hwpx.opc # OPC ์ปจํ
์ด๋ ์ฝ๊ธฐ/์ฐ๊ธฐ (์์์ ์ ์ฅ, ZIP ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ)
โโโ hwpx.oxml # OWPML XML โ ๋ฐ์ดํฐํด๋์ค ๋งคํ
โ โโโ document.py # ์น์
, ๋ฌธ๋จ, ํ, ๋ฐ, ๋ฉ๋ชจ, ๋ํ, ๋
ธํธ
โ โโโ header.py # ํค๋ ์ฐธ์กฐ ๋ชฉ๋ก (์คํ์ผ, ๊ธ๋จธ๋ฆฌํ, ๋ณ๊ฒฝ์ถ์ ๋ฑ)
โ โโโ body.py # ํ์
์ด ์ง์ ๋ ๋ณธ๋ฌธ ๋ชจ๋ธ
โ โโโ common.py # ๋ฒ์ฉ XML โ ๋ฐ์ดํฐํด๋์ค
โโโ hwpx.tools
โ โโโ archive_cli # unpack/pack CLI ๋ฐ ์ฌํจํน ๋ฉํ๋ฐ์ดํฐ
โ โโโ text_extractor # ํ
์คํธ ์ถ์ถ ํ์ดํ๋ผ์ธ
โ โโโ text_extract_cli # ํ
์คํธ ์ถ์ถ CLI
โ โโโ object_finder # ๊ฐ์ฒด ํ์ ์ ํธ๋ฆฌํฐ
โ โโโ exporter # ํ
์คํธ/HTML/Markdown ๋ด๋ณด๋ด๊ธฐ
โ โโโ validator # ์คํค๋ง ์ ํจ์ฑ ๊ฒ์ฌ (hwpx-validate CLI)
โ โโโ package_validator# ZIP/OPC/HWPX ๊ตฌ์กฐ ๊ฒ์ฌ
โ โโโ page_guard # ๊ตฌ์กฐ ๋ณํ ์งํ ์ ๊ฒ
โ โโโ template_analyzer# ๋ ํผ๋ฐ์ค ๋ฌธ์ ๋ถ์/์ถ์ถ
โโโ hwpx.templates # ๋ด์ฅ ๋น ๋ฌธ์ ํ
ํ๋ฆฟ
# HWPX ๋ฌธ์ ์คํค๋ง ์ ํจ์ฑ ๊ฒ์ฌ
hwpx-validate ๋ฌธ์.hwpx
# ZIP/OPC/HWPX ํจํค์ง ๊ตฌ์กฐ ๊ฒ์ฌ
hwpx-validate-package ๋ฌธ์.hwpx
# HWPX ํ๊ธฐ / ๋ค์ ๋ฌถ๊ธฐ (๊ธฐ๋ณธ๊ฐ: XML/HWPF ๋ฐ์ดํธ ๋ณด์กด)
hwpx-unpack ๋ฌธ์.hwpx ./unpacked
hwpx-unpack ๋ฌธ์.hwpx ./pretty-unpacked --pretty-xml
hwpx-pack ./unpacked ./repacked.hwpx
# ๋ ํผ๋ฐ์ค ๋ฌธ์ ๋ถ์๊ณผ ์์
๋๋ ํฐ๋ฆฌ ์ถ์ถ
hwpx-analyze-template ๋ฌธ์.hwpx --extract-dir ./template-parts --json
hwpx-pack ./template-parts ./template-roundtrip.hwpx
hwpx-validate-package ./template-roundtrip.hwpx
# plain / markdown ํ
์คํธ ์ถ์ถ
hwpx-text-extract ๋ฌธ์.hwpx --format markdown --output ๋ฌธ์.md
# ๋ฌธ์ ๊ตฌ์กฐ ๋ณํ ์งํ ๋น๊ต
hwpx-page-guard --reference ์๋ณธ.hwpx --output ๊ฒฐ๊ณผ.hwpxhwpx-page-guard๋ ๋ ๋๋ ์ค์ ์ชฝ์๋ฅผ ๊ณ์ฐํ์ง ์์ต๋๋ค. ๋์ ๋จ๋ฝ ์, ํ ์, shape/control ์, ๋ช
์์ page/column break, ํ
์คํธ ๊ธธ์ด ๊ฐ์ ๊ตฌ์กฐ ์งํ๋ฅผ ๋น๊ตํด ํธ์ง ์ ํ ๋ณํ ์งํ๋ฅผ ๋น ๋ฅด๊ฒ ์ ๊ฒํฉ๋๋ค.
hwpx-validate-package๋ Contents/content.hpf ๊ฐ์ ๊ณ ์ ๊ฒฝ๋ก๋ฅผ ์ ์ ๋ก ๋์ง ์๊ณ , META-INF/container.xml๊ณผ ์ค์ rootfile/manifest ์ ์ธ์ ๋ฐ๋ผ๊ฐ๋ฉฐ ํจํค์ง ๊ตฌ์กฐ๋ฅผ ํ์ธํฉ๋๋ค. ์์ง์ด ์ด ์ ์๋ ๋นํ์ค ํจํค์ง๋ ๊ฐ๋ฅํ ๊ฒฝ์ฐ ๊ฒฝ๊ณ ๋ก ๋ถ๋ฆฌํด ๋ณด์ฌ์ค๋๋ค.
hwpx-analyze-template --extract-dir๋ ๋ค์ ๋ฌถ๊ณ ์ ๊ฒํ๊ธฐ ์ฌ์ด ์์
๋๋ ํฐ๋ฆฌ๋ฅผ ๋ง๋ญ๋๋ค. ์ฌ๊ตฌ์ฑ๊ณผ ๊ตฌ์กฐ ๊ฒ์ฆ์ ํ์ํ ํ์ผ์ ํจ๊ป ๊บผ๋ด๋ ์ฉ๋์ด๋ฉฐ, ํธ์ง๊ธฐ์์์ ์ต์ข
๋ ๋๋ง ๊ฒฐ๊ณผ๊น์ง ๋ณด์ฅํ๋ค๋ ๋ป์ ์๋๋๋ค.
| ๐ ์ ์ฒด ๋ฌธ์ | Sphinx ๊ธฐ๋ฐ API ๋ ํผ๋ฐ์ค, ์ฌ์ฉ ๊ฐ์ด๋, FAQ |
| ๐ ๋น ๋ฅธ ์์ | 5๋ถ ์์ HWPX ๋ฌธ์ ๋ค๋ฃจ๊ธฐ |
| ๐ ์ฌ์ฉ ๊ฐ์ด๋ | 50+ ์ค์ ์ฌ์ฉ ํจํด |
| ๐ง API ๋ ํผ๋ฐ์ค | ํด๋์คยท๋ฉ์๋ ์์ธ ๋ช ์ธ |
| ๐ ์คํค๋ง ๊ฐ์ | OWPML ์คํค๋ง ๊ตฌ์กฐ ์ค๋ช |
| ํฌ๋งท | ํ์ฅ์ | ์ฝ๊ธฐ | ์ฐ๊ธฐ |
|---|---|---|---|
| HWPX | .hwpx |
โ | โ |
| HWP | .hwp |
โ | โ |
Note: HWP(v5 ๋ฐ์ด๋๋ฆฌ) ํ์ผ์ ์ง์ํ์ง ์์ต๋๋ค. ํ์ปด์คํผ์ค์์ HWPX๋ก ๋ณํ ํ ์ฌ์ฉํ์ธ์.
- Python 3.10+
- lxml โฅ 4.9
add_shape()/add_control()์ ํ/๊ธ์ด ์๊ตฌํ๋ ๋ชจ๋ ํ์ ์์๋ฅผ ์์ฑํ์ง ์์ต๋๋ค. ๋ณต์กํ ๊ฐ์ฒด๋ฅผ ์ถ๊ฐํ ๋๋ ํ/๊ธ์์ ์ด์ด ๊ฒ์ฆํด ์ฃผ์ธ์.- ์ด๋ฏธ์ง ์ฝ์
์ ๋ฐ์ด๋๋ฆฌ ์๋ฒ ๋๋ ์ง์ํ์ง๋ง,
<hp:pic>์์์ ์์ ํ ์๋ ์์ฑ์ ์ ๊ณตํ์ง ์์ต๋๋ค. - ์ํธํ๋ HWPX ํ์ผ์ ์๋ณตํธํ๋ ์ง์ํ์ง ์์ต๋๋ค.
๋ฒ๊ทธ ๋ฆฌํฌํธ, ๊ธฐ๋ฅ ์ ์, PR ๋ชจ๋ ํ์ํฉ๋๋ค. ๊ฐ๋ฐ ํ๊ฒฝ ์ค์ ๊ณผ ํ ์คํธ ๋ฐฉ๋ฒ์ CONTRIBUTING.md๋ฅผ ์ฐธ๊ณ ํ์ธ์.
git clone https://github.com/airmang/python-hwpx.git
cd python-hwpx
pip install -e ".[dev]"
pytestCustom Non-Commercial License ยฉ python-hwpx Maintainers
Commercial use requires separate permission from the copyright holders.
Primary maintainer/contact: ๊ณ ๊ทํ โ ๊ด๊ต๊ณ ๋ฑํ๊ต ์ ๋ณดยท์ปดํจํฐ ๊ต์ฌ
- โ๏ธ [email protected]
- ๐ @airmang