Files
Aislo/resources/knowledge/original/_pipeline/hwpx_text.py
T

200 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""HWPX(zip+XML) → Markdown. 공백·표·박스·계층 구조를 보존한다.
HWP→PDF 변환에서 공백이 소실된 첨부 md를 이 원본에서 재생성하는 용도.
- 문단 <hp:p>, 텍스트 <hp:t>, 표 <hp:tbl>/<hp:tr>/<hp:tc> 를 문서 순서대로 처리
- 단일셀 표(글상자) → 인용블록(줄바꿈 보존)
- 번호체계(제N장 / N-N-N. / 1. / 가. / (1) / ①) → 헤딩·중첩 리스트
"""
import re, sys, zipfile
from pathlib import Path
import xml.etree.ElementTree as ET
NS = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
def _local(tag):
return tag.split("}")[-1]
def para_text(p):
buf = []
for el in p.iter():
t = _local(el.tag)
if t == "t":
buf.append("".join(el.itertext()))
elif t == "tab":
buf.append("\t")
elif t == "lineBreak":
buf.append("\n")
return "".join(buf)
def cell_paras(tc):
parts = []
for sub in tc.iter(f"{NS}p"):
s = para_text(sub).strip()
if s:
parts.append(s)
return parts
def cell_text(tc):
return " ".join(cell_paras(tc)).replace("|", "/")
def table_md(tbl):
"""(kind, value) 반환. kind = 'table' | 'box' | 'text'."""
rows = [tr.findall(f"{NS}tc") for tr in tbl.findall(f"{NS}tr")]
rows = [r for r in rows if r]
if not rows:
return ("text", "")
# 단일셀(1행 1열) = 글상자 → 줄바꿈 보존 박스
if len(rows) == 1 and len(rows[0]) == 1:
return ("box", cell_paras(rows[0][0]))
txt = [[cell_text(tc) for tc in r] for r in rows]
if len(txt) < 2 or max(len(r) for r in txt) < 2:
return ("text", "\n".join(" ".join(r) for r in txt))
w = max(len(r) for r in txt)
txt = [r + [""] * (w - len(r)) for r in txt]
out = ["| " + " | ".join(txt[0]) + " |", "|" + "|".join(["---"] * w) + "|"]
for r in txt[1:]:
out.append("| " + " | ".join(c.replace("\n", "<br>") for c in r) + " |")
return ("table", "\n".join(out))
def walk(container, out):
for child in container:
if _local(child.tag) != "p":
continue
tbls = child.findall(f".//{NS}tbl")
if tbls:
for tb in tbls:
out.append(table_md(tb))
else:
out.append(("text", para_text(child)))
def extract(path):
z = zipfile.ZipFile(path)
secs = sorted(n for n in z.namelist() if re.search(r"Contents/section\d+\.xml$", n))
out = []
for sec in secs:
walk(ET.fromstring(z.read(sec)), out)
return out
# ── 계층 마커: (정규식, 종류) — 리스트 깊이는 등장 순서 스택으로 결정 ──
CHAP = re.compile(r"^제\d+\s*장(\s|$)")
SECN = re.compile(r"^\d+-\d+(-\d+)?\.?(\s|$)") # 품셈 절/항 번호 (1-2, 1-2-3.)
JO = re.compile(r"^제\d+조(의\d+)?\s*\(")
MARKERS = [
("num", re.compile(r"^(\d{1,2}\.)\s*(.*)$")),
("kor", re.compile(r"^([가-힣]\.)\s*(.*)$")),
("pnum", re.compile(r"^(\(\d{1,2}\))\s*(.*)$")),
("circ", re.compile(r"^([①-⑳])\s*(.*)$")),
("dash", re.compile(r"^([-∙·○])\s+(.*)$")),
]
def marker(s):
for k, rx in MARKERS:
m = rx.match(s)
if m:
return k, m.group(1), m.group(2)
return None, "", s
def structure(items, header):
"""(kind, val) 아이템 리스트 → 구조화 md 라인. hwpx/hwp5 공용."""
lines = list(header)
stack = [] # 리스트 마커 종류 스택
for kind, val in items:
if kind == "table":
lines += ["", val, ""]
stack = []
continue
if kind == "box":
# 글상자 → 인용블록, 문단 줄바꿈 보존
lines.append("")
for p in val:
lines.append(f"> {p}")
lines.append("")
stack = []
continue
st = (val or "").strip()
if not st:
continue
# 헤딩류
if CHAP.match(st):
lines += ["", f"## {st}", ""]
stack = []
continue
if SECN.match(st):
lines += ["", f"### {st}", ""]
stack = []
continue
if JO.match(st):
m = re.match(r"^(제\d+조(?:의\d+)?\s*\([^)]*\))\s*(.*)$", st, re.S)
lines += ["", f"### {m.group(1)}", ""]
if m.group(2).strip():
lines.append(m.group(2).strip())
stack = []
continue
# 리스트 마커
k, mk, rest = marker(st)
if k:
if k in stack:
depth = stack.index(k)
del stack[depth + 1 :]
else:
stack.append(k)
depth = len(stack) - 1
ind = " " * depth
lines.append(f"{ind}- {mk} {rest}".rstrip())
else:
# 리스트 진행 중이면 현재 깊이의 본문(연속 문단)으로 들여쓰기
if stack:
lines.append(" " * len(stack) + st)
else:
lines.append(st)
# 빈 줄 정리
md, blank = [], 0
for l in lines:
if l.strip() == "":
blank += 1
if blank > 1:
continue
else:
blank = 0
md.append(l)
out = "\n".join(md).strip() + "\n"
# HWP 사설영역·서로게이트 등 UTF-8 인코딩 불가 문자 제거(깨진 글리프)
out = "".join(c for c in out if not ("\ud800" <= c <= "\udfff"))
out = out.encode("utf-8", "ignore").decode("utf-8")
return out
def to_md(path):
items = extract(path)
header = [f"# {Path(path).stem}", "", f"> 원본: `{Path(path).name}` (HWPX 재추출)", ""]
return structure(items, header)
def hwp5_to_md(path, header=None):
"""구형 HWP5(OLE) → 구조화 md. 표를 복원(extract_items)해 계층·표 보존."""
import hwp5_text
items = hwp5_text.extract_items(str(path))
if header is None:
header = [f"# {Path(path).stem}", "", f"> 원본: `{Path(path).name}` (HWP 재추출)", ""]
return structure(items, header)
if __name__ == "__main__":
for f in sys.argv[1:]:
p = Path(f)
p.with_suffix(".md").write_text(to_md(p), encoding="utf-8")
print(f"{p.name} → 재추출")