Files
Aislo/resources/knowledge/original/_pipeline/cak_hwp_md.py
T
eomsangdonandClaude Sonnet 5 98ee14ca9f knowledge(노임): 건설노임 옛 회차 2020.1 ~ 2026.9 원문 13회 수집 · 회차별 완전 md 화 · 미공표 14 직종 추적표
- 대한건설협회 임금실태조사 보고서 13회(2020.1 · 2020.9 · 2021.1 · 2021.9 ~ 2026.9) 원문 보관 — 2020.9 · 2021.1 은 게시판에 HWP 만 있어 HWP 로 보관
- 회차마다 전체본 md + 장별 md 4개(쪽 표기 · 표 좌표 재구성) — _pipeline/cak_report_md.py · cak_hwp_md.py
- 표 안 수 기계 대조(cak_verify.py) — md 대 PDF 이상 0 · 회차 사이 충돌 1(3011 · 2020.1 원문 그대로) · 쪽 그림 대조
- 미공표직종_추적.md — 원문 4-라 산정 근거(마지막 공표값 · 전체직종 평균 · 이어 곱한 증가율) · ESTX 견줌 14 중 12 일치
- 마스터 미변경

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
2026-09-20 00:44:11 +09:00

214 lines
7.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 -*-
"""건설업 임금실태조사 보고서 HWP(5.x) → 전체본 md + 장별 md 4개. (PDF 가 없는 회차용)
사용: python cak_hwp_md.py <회차 폴더> ... (폴더 안에 .hwp 한 개)
- HWP 본문 레코드에서 글·표(칸 좌표 · 병합)를 직접 읽음. 쪽 구분은 HWP 에 없으므로 쪽 표기 없음.
- 검증은 cak_verify.py 의 회차 사이 대조(같은 공표일 값이 다른 회차 PDF 에도 실려 있음)로 함.
"""
import re
import struct
import sys
import zlib
from pathlib import Path
import olefile
NUM = r"\(※\d{1,3}(?:,\d{3})+\)|\d{1,3}(?:,\d{3})+|(?<![\d,])-(?![\d,])"
INFO = {"2020-09-01": ("2020-08-31", 5311), "2021-01-01": ("2021-01-01", 5312)}
CHAPTERS = [
("1. 조사개요", ".조사개요"),
("2. 임금적용요령", "Ⅱ.임금적용요령"),
("3. 개별직종 노임단가", "Ⅲ.개별직종노임단가"),
("4. 직종해설", "Ⅳ.직종해설"),
]
def para_text(b):
"""PARA_TEXT → 글. 제어문자(8 단위 차지) 건너뜀."""
u = struct.unpack("<%dH" % (len(b) // 2), b)
out, i = [], 0
while i < len(u):
c = u[i]
if c >= 32:
out.append(chr(c))
i += 1
elif c == 9:
out.append("\t")
i += 8
elif c in (1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23):
i += 8
elif c in (10, 13):
i += 1
else:
i += 1
return "".join(out)
def read_blocks(path):
"""→ [("p", 글) | ("t", 칸목록[(row, col, colspan, rowspan, 글)] , 행수, 열수)]"""
o = olefile.OleFileIO(str(path))
comp = o.openstream("FileHeader").read()[36] & 1
recs = []
i = 0
while o.exists(f"BodyText/Section{i}"):
d = o.openstream(f"BodyText/Section{i}").read()
if comp:
d = zlib.decompress(d, -15)
p = 0
while p < len(d):
h = struct.unpack_from("<I", d, p)[0]
tag, lvl, sz = h & 0x3FF, (h >> 10) & 0x3FF, h >> 20
p += 4
if sz == 0xFFF:
sz = struct.unpack_from("<I", d, p)[0]
p += 4
recs.append((tag, lvl, d[p : p + sz]))
p += sz
i += 1
blocks = []
k = 0
while k < len(recs):
tag, lvl, b = recs[k]
if tag == 71 and b[:4] == b" lbt": # 표
tl = lvl
rows = cols = 0
cells = []
cur = None
k += 1
while k < len(recs) and (
recs[k][1] > tl or recs[k][0] in (72, 77) and recs[k][1] >= tl
):
t2, l2, b2 = recs[k]
if t2 == 77:
rows, cols = struct.unpack_from("<HH", b2, 4)
elif t2 == 72 and l2 == tl + 1:
col, row, cs, rs = struct.unpack_from("<HHHH", b2, 8)
cur = [row, col, cs, rs, []]
cells.append(cur)
elif t2 == 67 and cur is not None:
cur[4].append(para_text(b2).rstrip("\r"))
k += 1
blocks.append(
(
"t",
[
(r, c, cs, rs, "<br>".join(x.strip() for x in t if x.strip() != ""))
for r, c, cs, rs, t in cells
],
rows,
cols,
)
)
continue
if tag == 67:
blocks.append(("p", para_text(b).rstrip("\r")))
k += 1
return blocks
def esc(s):
return s.replace("|", "\\|")
def md_grid(cells, rows, cols):
g = [[""] * cols for _ in range(rows)]
for r, c, cs, rs, t in cells:
if r < rows and c < cols:
g[r][c] = t
o = []
for i, row in enumerate(g):
o.append("| " + " | ".join(esc(x) for x in row) + " |")
if i == 0:
o.append("|" + "---|" * cols)
return o
def jobs_table(cells):
"""개별직종 표 → 행 [(코드, 이름, 신뢰도, [4값])] · 머리 글"""
byrow = {}
for r, c, cs, rs, t in cells:
byrow.setdefault(r, {})[c] = t
rows, head = [], []
for r in sorted(byrow):
row = [byrow[r][c] for c in sorted(byrow[r])]
m = re.fullmatch(r"(\*{0,2})\s*(\d{4})", row[0].strip()) if row else None
if m and len(row) >= 6:
vals = row[-4:]
rows.append((m.group(2), "".join(row[1:-4]).replace(" ", ""), m.group(1), vals))
else:
head.append(row)
return rows, head
def convert(folder):
folder = Path(folder)
hwp = next(folder.glob("*.hwp"))
stem = hwp.stem
apply = folder.name
reg, seq = INFO[apply]
blocks = read_blocks(hwp)
title = f"{stem[:4]}{stem[4:6]} 적용"
hdr = (
f"> 원문: {hwp.name} (대한건설협회, 게시 {reg}, 적용 {apply})\n"
"> 변환: cak_hwp_md.py — HWP 본문에서 글 · 표 칸 좌표를 직접 읽음. HWP 에는 쪽 구분이 없어 쪽 표기 없음 · 원문 PDF 없음(쪽 그림 대조 불가) — "
"표 값은 같은 공표일이 실린 다른 회차 PDF 와 기계 대조(cak_verify.py). 표 안 글자 사이 벌림은 붙여 씀.\n\n"
)
# 장 나누기: 표 밖 문단 중 목차(탭 없는) 장 제목
idx = {}
for i, b in enumerate(blocks):
if b[0] == "p" and "\t" not in b[1]:
n = re.sub(r"\s", "", b[1])
for name, key in CHAPTERS:
if n == key and name not in idx:
idx[name] = i
order = [idx[n] for n, _ in CHAPTERS]
assert order == sorted(order), idx
bounds = order + [len(blocks)]
def render(bs):
out = []
for b in bs:
if b[0] == "p":
out.append(b[1] + " ")
continue
_, cells, rows, cols = b
jr, head = jobs_table(cells)
if len(jr) >= 10: # 개별직종 표
dates = []
for h in head:
dates += [x for x in h if re.fullmatch(r"\d{4}\.\d{1,2}\.\d{1,2}", x.strip())]
out.append("")
out.append("| 직종코드 | 직종명 | 신뢰도 | " + " | ".join(dates[:4]) + " |")
out.append("|---|---|---|---|---|---|---|")
for c, n, f, v in jr:
out.append(f"| {c} | {n} | {f} | " + " | ".join(esc(x) for x in v) + " |")
out.append("")
else:
out.append("")
out += md_grid(cells, rows, cols)
out.append("")
return out
pre = render(blocks[: order[0]])
chapters = {}
for j, (name, _) in enumerate(CHAPTERS):
chapters[name] = render(blocks[bounds[j] : bounds[j + 1]])
full_md = (
f"# {title}\n\n> 원본: `{hwp.name}`\n\n"
+ "\n".join(pre + [x for n, _ in CHAPTERS for x in chapters[n]])
+ "\n"
)
(folder / f"{stem}.md").write_text(full_md, encoding="utf-8")
for name, _ in CHAPTERS:
(folder / f"{name}.md").write_text(
f"# {name.split('. ', 1)[1]}\n\n{hdr}" + "\n".join(chapters[name]) + "\n",
encoding="utf-8",
)
return len([1 for b in blocks if b[0] == "t"])
if __name__ == "__main__":
for f in sys.argv[1:]:
print(f, "표", convert(f))