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

115 lines
4.3 KiB
Python

# -*- coding: utf-8 -*-
"""건설업 임금실태조사 회차별 md 의 표 안 수를 기계 대조.
1) 개별직종 표: md ↔ PDF 글자층을 다른 방식(직종코드 앵커 분할 · split_cost_docs.parse_cak_table 와 같은 방식)으로 읽은 값
2) 회차 사이: 같은 (직종, 공표일) 값이 여러 회차에 실려 있으면 서로 같은지
3) 평균임금 표(쪽 4): 회차 사이 같은 공표일 행이 서로 같은지
사용: python cak_verify.py (노임단가_건설업_대한건설협회 아래 <적용일>/ 폴더 전부)
"""
import re
from pathlib import Path
import pymupdf as fitz
BASE = Path(__file__).resolve().parent.parent / "원가계산" / "노임단가_건설업_대한건설협회"
NUM = r"\(※\d{1,3}(?:,\d{3})+\)|\d{1,3}(?:,\d{3})+|(?<![\d,])-(?![\d,])"
def md_rows(folder):
"""md → {코드: (이름, 신뢰도, {공표일: 값})}"""
f = next(folder.glob("3. *.md"))
out, dates = {}, None
for ln in f.read_text(encoding="utf-8").splitlines():
if ln.startswith("| 직종코드"):
dates = [c.strip() for c in ln.strip("|").split("|")][3:]
continue
m = re.match(r"\| (\d{4}) \| (.*?) \| (\**) \| (.*) \|$", ln)
if m and dates:
vals = [c.strip() for c in m.group(4).split("|")]
assert len(vals) == 4, ln
out[m.group(1)] = (m.group(2), m.group(3), dict(zip(dates, vals)))
return out
def pdf_rows(folder, pages=(9, 10, 11, 12)):
"""PDF 글자층 → 앵커 분할로 {코드: (이름, 신뢰도, [4값])}"""
doc = fitz.open(next(folder.glob("*.pdf")))
t = "".join(doc[p].get_text() + "\n" for p in pages)
parts = re.compile(r"(?<![\d,.])([1-5]\d{3})(?![\d,.])").split(t)
res = {}
for i in range(1, len(parts) - 1, 2):
code, seg = parts[i], parts[i + 1]
if not (1 <= int(code[1:]) <= 99):
continue
fm = re.search(r"(\*{1,2})\s*$", parts[i - 1])
v = re.findall(NUM, seg)[:4]
f = re.search(NUM, seg)
nm = "".join(re.findall(r"[가-힣A-Za-z/]", seg[: f.start()] if f else seg))
if code not in res and len(v) == 4 and nm:
res[code] = (nm, fm.group(1) if fm else "", v)
return res
def avg_rows(folder):
"""쪽 4 평균임금 표 → {공표일 앞부분: [칸들]}"""
for f in folder.glob("*.md"):
if f.name[0].isdigit() and f.name[1] == ".":
continue
if f.name.startswith("_"):
continue
full = f
out = {}
on = False
for ln in full.read_text(encoding="utf-8").splitlines():
if ln.startswith("| 공표일 (조사기준)"):
on = True
continue
if on and ln.startswith("|---"):
continue
if on and ln.startswith("|"):
c = [x.strip() for x in ln.strip("|").split("|")]
out[re.sub(r"\(.*", "", c[0]).strip()] = c[1:]
elif on:
break
return out
def main():
folders = sorted(
d for d in BASE.iterdir() if d.is_dir() and re.fullmatch(r"\d{4}-\d\d-\d\d", d.name)
)
series, conflicts, bad = {}, 0, 0
avg = {}
for d in folders:
m = md_rows(d)
p = pdf_rows(d) if any(d.glob("*.pdf")) else {}
if p and set(m) != set(p):
print(d.name, "직종 수 다름", len(m), len(p), sorted(set(m) ^ set(p))[:5])
bad += 1
for c in m:
if c not in p:
continue
vals = list(m[c][2].values())
if vals != p[c][2] or m[c][1] != p[c][1]:
bad += 1
print(d.name, c, "md", m[c][1], vals, "pdf", p[c][1], p[c][2])
for k, v in m[c][2].items():
s = series.setdefault(c, {})
if k in s and s[k][0] != v:
conflicts += 1
print("회차 사이 다름", c, k, s[k], v, d.name)
s.setdefault(k, (v, d.name))
for k, v in avg_rows(d).items() if any(d.glob("*.pdf")) else []:
if k in avg and avg[k][0] != v:
conflicts += 1
print("평균임금 회차 사이 다름", k, avg[k], v, d.name)
avg.setdefault(k, (v, d.name))
print(d.name, "직종", len(m), "md↔PDF 다른 행", 0 if not bad else "위 참조")
print("md↔PDF 이상", bad, "· 회차 사이 충돌", conflicts)
if __name__ == "__main__":
main()