Files
Aislo/resources/master_data/scripts/_설비_표.py
T
eomsangdonandClaude Sonnet 5 9f617fdbfa feat(master_data): 건설 기계설비 7장 소요량 옮김
- 소요량_건설품셈_기계설비7장.json (본문 표 18 + 계수 표 17)
- 변환 스크립트 build_소요량_건설품셈_기계설비.py · _설비_읽기 · _설비_표 · _설비_계수 · _설비_손 추가
- _토목_계수.lift 에 규칙 함수 인자 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
2026-09-19 17:45:10 +09:00

335 lines
12 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 -*-
"""기계설비 표 만들기 — 원문 표 하나를 조건 칸 + 값 칸의 줄로 폄 (build_소요량_건설품셈_기계설비.py 가 씀).
표 모양은 스스로 알아냄(라벨 칸 수 · 머리 층 · 직종 위치). 못 맞추는 표는 SPEC 으로 지정.
"""
import re
from _md표_읽기 import N, R, join
from _토목_표 import bigo_items, conds_out, is_unit, jn
from _토목_읽기 import cell_val, is_num
MEMBER = re.compile(
r"(공|인부|기사|기능사|운전사|운전조|측량사|조공|반장|감독원|기술자|보조원|기능공|공사)$"
)
MEASURE = re.compile(
r"^(수량|시공량|소요량|사용기간|사용시간|시간|소비량|소요전력|용접봉소비량|중량|작업능력|인력)"
)
LABELNAME = re.compile(
r"^(구분|규격|구경|관경|호칭|외경|내경|두께|길이|용량|치수|사이즈|Pipe Size|압력|명칭|품명|장비명|"
r"직종|공종|공정별|작업구분|종류|형식|재질|층|이름|단위|비고|배관구분|작업횟수|기간|처리)"
)
VALUEISH = re.compile(r"수량|시공량|소비량|소요|인력|시간|기간|작업능력")
DASH = ("", "-")
def strip_unit(name):
"""「배관공(인)」 → (배관공, 인)."""
m = re.search(r"\(([^()]{1,8})\)$", name)
if m and is_unit(m.group(1)):
return name[: m.start()].strip(), m.group(1)
return name, ""
def is_member(x):
return bool(MEMBER.search(re.sub(r"\([^)]*\)", "", jn(x)).strip()))
def nm(x):
"""직종 이름 — 「비계 공」 처럼 줄바꿈으로 벌어진 글은 붙임."""
x = jn(x)
return (
x.replace(" ", "") if re.fullmatch(r"[가-힣 ]+", x) and is_member(x.replace(" ", "")) else x
)
def cv(x):
"""값 칸 하나 → (값, 원문). 「a∼b」 는 [아래, 위]."""
x = jn(x)
m = re.fullmatch(r"(\d[\d,]*(?:\.\d+)?)\s*[∼~]\s*(\d[\d,]*(?:\.\d+)?)", x)
if m:
return [R(m.group(1)), R(m.group(2))], None
return cell_val(x)
def vlike(x):
x = x.strip()
return x in DASH or is_num(x) or bool(re.fullmatch(r"[\d.,]+\s*%?", x)) or x == "〃"
def halves(rows, H):
"""왼쪽 · 오른쪽 짝 표(머리 이름이 되풀이) → 위아래로 이음."""
hd = rows[0]
n = len(hd)
if n % 2 == 0 and any(hd) and all(r[: n // 2] == r[n // 2 :] for r in rows[:H]):
left = [r[: n // 2] for r in rows]
right = [r[: n // 2] for r in rows[:H]] + [
r[n // 2 :] for r in rows[H:] if any(x not in DASH for x in r[n // 2 :])
]
return left + right[H:]
return rows
def label_cols(rows, H):
"""왼쪽 라벨 칸 수 L."""
ncol, body = len(rows[0]), rows[H:]
def textual(c):
vs = [r[c] for r in body if r[c] not in DASH]
return bool(vs) and sum(not vlike(v) or v == "〃" for v in vs) * 2 >= len(vs)
L = 1
while L < ncol - 1:
c, top = L, jn(rows[0][L])
if H > 1:
below = [rows[r][c] for r in range(1, H - 1)]
last = rows[H - 1][c]
span = not any(below) and (last == "" or is_unit(jn(last)))
ok = span and not VALUEISH.search(top) and not is_member(top)
ok = ok and (top == "" or textual(c) or bool(LABELNAME.match(top)))
else:
ok = textual(c) or bool(LABELNAME.match(top) and not VALUEISH.search(top))
ok = ok and not (top in ("수량",) or MEASURE.match(top))
ok = ok and any(not textual(k) for k in range(c + 1, ncol))
if not ok:
break
L += 1
return L
def layers(rows, H, start, ncol):
"""머리 층 H 줄 → [층][열] 글(빈 칸은 같은 윗 무리 안에서 왼쪽 값으로 채움)."""
out = []
for r in range(H):
row = [jn(x) for x in rows[r]]
for c in range(start if r == 0 else start + 1, ncol):
if row[c] or c == 0:
continue
same = all(out[q][c] == out[q][c - 1] for q in range(r))
if row[c - 1] and same and (r == 0 or out[r - 1][c]):
row[c] = row[c - 1]
out.append(row)
return out
def measure_hm(entries):
es = {e for e in entries if e}
return bool(es) and sum(is_member(e) for e in es) * 2 >= len(es)
def measure_score(entries):
es = [e for e in entries if e]
if not es:
return 0
return sum(bool(is_member(e) or MEASURE.match(e) or strip_unit(e)[1]) for e in set(es)) / len(
set(es)
)
def convert(t, spec=None):
"""표 → (조건 사전, 값칸 사전, 줄 목록)."""
spec = spec or {}
rows, H = [list(r) for r in t.rows], spec.get("H", t.hdr)
rows = halves(rows, H)
ncol = max(len(r) for r in rows)
rows = [r + [""] * (ncol - len(r)) for r in rows]
bigo = []
for r in rows[H:]:
if jn(r[0]) == "비고":
bigo += bigo_items(" ".join(x for x in r[1:] if x))
rows = rows[:H] + [r for r in rows[H:] if jn(r[0]) != "비고"]
body = rows[H:]
L = spec.get("L") or label_cols(rows, H)
unitc = next(
(c for c in range(L) if jn(rows[H - 1][c]) == "단위" or jn(rows[0][c]) == "단위"), None
)
lab_unit = None # 머리 없는 「〃」 단위 칸 = 앞 라벨 칸의 단위
if unitc is None:
unitc = next(
(
c
for c in range(1, L)
if sum(r[c] == "〃" for r in body) * 2 >= max(len(body) - 1, 1)
),
None,
)
lab_unit = unitc
if lab_unit is not None and not any(
measure_hm([jn(rows[r][c]) for c in range(L, ncol)]) for r in range(H)
):
unitc = lab_unit = None # 값 머리가 직종이 아니면 그냥 라벨 칸
# 라벨 칸 이름
names = {}
for c in range(L):
top, low = jn(rows[0][c]), jn(rows[H - 1][c]) if H > 1 else ""
names[c] = (
f"{top}({low})" if low and is_unit(low) else (low if c == 0 and low and H > 1 else top)
) or "구분"
if lab_unit is not None and any(
measure_hm([jn(rows[r][c]) for c in range(L, ncol)]) for r in range(H)
):
u0 = next((jn(r[lab_unit]) for r in body if r[lab_unit] not in ("", "〃")), "")
names[lab_unit - 1] += f"({u0})"
lab_unit = -1
# 직종 칸(줄에 직종이 있는 표)
txt = [c for c in range(L) if c != unitc]
mc = spec.get("M", None)
head_members = any(measure_hm([jn(rows[r][c]) for c in range(L, ncol)]) for r in range(H))
if mc is None and unitc is not None and not head_members:
mc = max(c for c in txt if c < unitc) if any(c < unitc for c in txt) else None
if mc is None and txt:
last = txt[-1]
vs = [r[last] for r in body if r[last] not in DASH]
if vs and sum(is_member(v) for v in vs) * 2 >= len(vs):
mc = last
if mc == -1:
mc = None
if ( # 첫 칸이 값 이름인 옆으로 누운 표
mc is None
and L == 1
and unitc is None
and not head_members
and H == 1
and body
and all(r[0] not in DASH and not vlike(r[0]) and not is_member(r[0]) for r in body)
):
mc = 0
names[0] = jn(rows[0][0]) or "구분"
tposed = True
else:
tposed = False
# 값 열 머리 층
vcols = list(range(L, ncol))
lay = layers(rows, H, L, ncol)
corner = [jn(rows[r][0]) for r in range(H)]
named = H > 1 and all(corner) and len(set(corner)) == H
if named:
names[0] = corner[-1]
lnames = [corner[r] if named else "" for r in range(H)]
if tposed:
lnames = [names[0]]
mlay, ulay = None, set()
for r in range(H):
ents = [lay[r][c] for c in vcols]
if ents and all(is_unit(e) for e in ents if e) and any(ents):
ulay.add(r)
if mc is None:
sc = {r: measure_score([lay[r][c] for c in vcols]) for r in range(H) if r not in ulay}
if sc:
best = max(sc, key=lambda r: (sc[r], r))
if sc[best] >= 0.5:
mlay = best
else: # 직종이 줄에 있음 — 값 이름 층은 「수량」 따위뿐
for r in range(H):
if (
r not in ulay
and lay[r]
and all(MEASURE.match(lay[r][c]) for c in vcols if lay[r][c])
and any(lay[r][c] for c in vcols)
):
mlay = r
only = {
r
for r in range(H)
if any(lay[r][c] for c in vcols)
and all(lay[r][c] in ("", "수량", "시공량", "소요량") for c in vcols)
}
dropped = ulay | only | ({mlay} if mlay is not None else set())
clay = [r for r in range(H) if r not in dropped]
# 몸 줄 읽기
recs, prev, prevu = [], {}, ""
for r in body:
lab = {}
for c in range(L):
if c == unitc or c == mc:
continue
x = jn(r[c])
if x in ("", "〃") and not any(r[q] for q in range(c)):
x = prev.get(c, "")
lab[c] = x
prev.update(lab)
if not any(x for x in lab.values()) and not any(r[c] for c in range(L)):
continue
unit = ""
if unitc is not None:
u = jn(r[unitc])
prevu = u if u and u != "〃" else prevu
unit = "" if lab_unit == -1 else prevu
member = nm(r[mc]) if mc is not None else ""
made = 0
for c in vcols:
v = r[c].strip()
if v in DASH:
continue
u2, meas = unit, "수량"
if mlay is not None:
meas, uu = strip_unit(nm(lay[mlay][c]))
u2 = u2 or uu
if member:
meas = member
for q in ulay:
u2 = u2 or lay[q][c]
cond = {}
for c0 in range(L):
if c0 in lab and lab[c0]:
cond[names[c0]] = lab[c0]
path = [lay[q][c] for q in clay if lay[q][c]]
un = [q for q in clay if lnames[q] and lay[q][c]]
if un:
for q in un:
cond[lnames[q]] = lay[q][c]
path = [lay[q][c] for q in clay if lay[q][c] and not lnames[q]]
if path:
cond["구분" if "구분" not in cond else "세구분"] = " ".join(dict.fromkeys(path))
recs.append((cond, meas, v, u2))
made += 1
if not made and mc is not None and member and not is_member(member):
cond = {names[c0]: lab[c0] for c0 in lab if lab[c0]}
recs.append((cond, "수량", member, unit))
return (*pack(recs, spec, t), bigo)
def pack(recs, spec, t):
"""(조건, 값이름, 값, 단위) 목록 → 조건 · 값칸 · 줄."""
unit_of, multi = {}, False
groups = {}
for cond, meas, v, u in recs:
key = tuple(cond.items())
g = groups.setdefault(key, {})
if meas in g:
multi = True
g[meas] = (v, u)
if unit_of.setdefault(meas, u) != u:
multi = True
rows, cols = [], {}
if not multi:
for key, g in groups.items():
d = dict(key)
for meas, (v, u) in g.items():
val, orig = cv(v)
cols.setdefault(meas, unit_of[meas])
d[meas] = val
if orig:
d[meas + "원문"] = orig
cols[meas + "원문"] = "글"
rows.append(d)
cn = list(dict.fromkeys(k for r in rows for k in r if k not in cols))
else: # 값 이름이 조건으로 — 값은 「수량」 하나 · 줄마다 단위
for cond, meas, v, u in recs:
d = dict(cond)
d["항목"] = meas
val, orig = cv(v)
d["수량"] = val
if orig:
d["수량원문"] = orig
if u:
d["단위"] = u
rows.append(d)
cols = {"수량": ""}
if any("수량원문" in r for r in rows):
cols["수량원문"] = "글"
cn = list(dict.fromkeys(k for r in rows for k in r if k not in cols and k != "단위"))
cond = conds_out(cn, rows)
return cond, cols, rows