Files
Aislo/resources/knowledge/original/_pipeline/hwp5_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

172 lines
6.4 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 -*-
import os as _os
from pathlib import Path as _P
# 이 스크립트는 original/_pipeline/ 에 위치. 코퍼스 루트 = 상위 폴더.
ROOT_DIR = _P(__file__).resolve().parent.parent # ...\original
DATA_DIR = _P(__file__).resolve().parent / "data" # 생성 데이터(JSON)
# API 키: 커밋 금지 파일(law/.secrets.local.md) 또는 환경변수에서 읽는다.
def _load_key(name):
v = _os.environ.get(name)
if v: return v.strip()
sec = ROOT_DIR.parent / ".secrets.local.md"
if sec.exists():
import re as _re
for pat in (r"KCSC[\s\S]*?`([A-Za-z0-9]{20,})`", r"인증키[:\s]*`?([A-Za-z0-9]{30,})`?"):
m = _re.search(pat, sec.read_text(encoding="utf-8"))
if m: return m.group(1)
return ""
"""HWP5(OLE) 본문 텍스트 추출 — 순수 파이썬.
BodyText/Section* 스트림을 (필요시 raw-deflate 해제) 레코드 파싱해
문단 텍스트(HWPTAG_PARA_TEXT)를 UTF-16LE로 뽑는다.
"""
import sys, re, zlib, struct
import olefile
HWPTAG_BEGIN = 0x10
HWPTAG_PARA_HEADER = HWPTAG_BEGIN + 50 # 0x42
HWPTAG_PARA_TEXT = HWPTAG_BEGIN + 51 # 0x43
HWPTAG_CTRL_HEADER = HWPTAG_BEGIN + 55 # 0x47
HWPTAG_LIST_HEADER = HWPTAG_BEGIN + 56 # 0x48
HWPTAG_TABLE = HWPTAG_BEGIN + 61 # 0x4d
def is_compressed(ole):
with ole.openstream("FileHeader") as f:
data = f.read()
# 36바이트 오프셋의 속성 플래그 bit0 = 압축여부
flags = struct.unpack("<I", data[36:40])[0]
return bool(flags & 1)
def records(buf):
i, n = 0, len(buf)
while i + 4 <= n:
header = struct.unpack("<I", buf[i:i+4])[0]
i += 4
tag = header & 0x3FF
level = (header >> 10) & 0x3FF
size = (header >> 20) & 0xFFF
if size == 0xFFF:
size = struct.unpack("<I", buf[i:i+4])[0]
i += 4
yield tag, level, buf[i:i+size]
i += size
# 인라인 확장 제어문자(뒤에 14 WCHAR = 28바이트 추가로 따라옴)
EXT_CTRL = {1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 14, 15, 16, 17, 18, 21, 22, 23}
# 인라인 문자 제어(그 자체로 1 WCHAR)
INLINE = {0, 10, 13, 24, 25, 26, 27, 28, 29, 30, 31}
def para_text(data):
out = []
i, n = 0, len(data)
while i + 1 < n:
code = struct.unpack("<H", data[i:i+2])[0]
if code in EXT_CTRL:
i += 16 * 2 # 제어문자 1 + 확장 14 + 종료 1 = 16 WCHAR
continue
if code in INLINE:
if code == 13: # 문단 구분? (para text엔 없음)
out.append("\n")
i += 2
continue
out.append(chr(code))
i += 2
return "".join(out)
def extract(path):
"""PARA_TEXT 문단만 평문 리스트로(하위호환)."""
ole = olefile.OleFileIO(path)
comp = is_compressed(ole)
secs = sorted(e[-1] for e in ole.listdir() if e[0] == "BodyText")
paras = []
for sec in secs:
with ole.openstream(f"BodyText/{sec}") as f:
raw = f.read()
buf = zlib.decompress(raw, -15) if comp else raw
for tag, level, data in records(buf):
if tag == HWPTAG_PARA_TEXT:
paras.append(para_text(data).strip())
ole.close()
return paras
def _table_md(cells, ncols):
"""셀 텍스트 리스트를 nCols 기준으로 md 표로."""
cells = [re.sub(r"\s+", " ", (c or "").replace("|", "")).strip() for c in cells]
if ncols < 1 or len(cells) < ncols * 2:
# 표라기엔 빈약 → 줄 텍스트
return None
rows = [cells[i:i + ncols] for i in range(0, len(cells), ncols)]
rows = [r + [""] * (ncols - len(r)) for r in rows]
out = ["| " + " | ".join(rows[0]) + " |", "|" + "|".join(["---"] * ncols) + "|"]
for r in rows[1:]:
out.append("| " + " | ".join(r) + " |")
return "\n".join(out)
def extract_items(path):
"""(kind, val) 아이템 리스트. kind='text'|'table'. 표를 복원한다."""
ole = olefile.OleFileIO(path)
comp = is_compressed(ole)
secs = sorted(e[-1] for e in ole.listdir() if e[0] == "BodyText")
items = []
for sec in secs:
with ole.openstream(f"BodyText/{sec}") as f:
raw = f.read()
buf = zlib.decompress(raw, -15) if comp else raw
recs = list(records(buf))
i, n = 0, len(recs)
while i < n:
tag, level, data = recs[i]
# 표 컨트롤: CTRL_HEADER ctrl_id 'tbl ' (역순 b' lbt')
if tag == HWPTAG_CTRL_HEADER and data[:4] == b" lbt":
tbl_level = level
# 다음 TABLE 레코드에서 열수
ncols = 0
j = i + 1
while j < n and recs[j][0] != HWPTAG_TABLE:
j += 1
if j < n:
d = recs[j][2]
if len(d) >= 8:
ncols = struct.unpack("<H", d[6:8])[0]
j += 1
# 셀 수집: level > tbl_level 인 동안, LIST_HEADER마다 새 셀
cells, cur = [], None
while j < n and recs[j][1] > tbl_level:
t2, l2, d2 = recs[j]
if t2 == HWPTAG_LIST_HEADER:
if cur is not None:
cells.append(cur)
cur = ""
elif t2 == HWPTAG_PARA_TEXT and cur is not None:
seg = para_text(d2).strip()
cur = (cur + " " + seg).strip() if cur else seg
j += 1
if cur is not None:
cells.append(cur)
md = _table_md(cells, ncols) if ncols else None
if md:
items.append(("table", md))
else:
for c in cells:
if c.strip():
items.append(("text", c))
i = j
continue
if tag == HWPTAG_PARA_TEXT:
s = para_text(data).strip()
if s:
items.append(("text", s))
i += 1
ole.close()
return items
if __name__ == "__main__":
paras = extract(sys.argv[1])
text = "\n".join(p for p in paras if p)
if len(sys.argv) > 2:
open(sys.argv[2], "w", encoding="utf-8").write(text)
print(f"문단 {len(paras)} / 문자 {len(text)}")
print("--- 처음 40줄 ---")
for p in [x for x in paras if x][:40]:
print(p[:100])