Files
Aislo/resources/knowledge/original/_pipeline/hwpx_text.py
T
eomsangdonandClaude Fable 5 81cd7e23c3 feat(knowledge): 도메인 지식저장소 메인 통합 + resources 그룹 체계 재편
knowledge (구 Aislo-law 독립 저장소 → resources/knowledge 이관, 저장소 폐지):
- 법령·행정규칙·표준시방서·교본 원문 + 기술문서 55건 + 실무 분석·종합비교
- 루트 지침 체계: README(지도)·00_운영지침·01_수집지침·02_분석지침·
  03_미결_및_확인사항(교본 충돌 리스트 포함)·04_참조_법령기준_목록
- 기술문서 55건 원문 전수 검증 완료 (사방 설계홍수량 법정 기준 등 반영)
- 정리: CAD·오피스 잔재 142건, 중복 zip 7건(413MB), 빈 폴더 30개 제거

resources 그룹 재편 (이름순 그룹핑):
- app_branding(구 prog_icon.jpg)·app_policies(구 legal)·
  data_global_contours(구 grobal_contours)·data_rainfall_idf_cache(구 wamis_contours)·
  template_2dDrawing(구 dwg_analysis/templete — 오타 교정, 상수·경로 동기화)
- dwg_analysis(분석 완료 1.8GB)·templates(빈 폴더)·templete_calc_cost.xlsx 삭제
- 참조 코드 5파일 경로 수정 + 프론트 재빌드 (구 경로 잔존 0)
- .gitignore: resources 추적 전환, national_contours.gpkg(22GB) 영구 제외
- .env: knowledge 수집용 API 정보 주석 통합 (KCSC·법령센터·조달청 제비율)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 19:14:32 +09:00

180 lines
6.1 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} → 재추출")