Files
Aislo/resources/master_data/scripts/_설비_표.py
T

390 lines
15 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, jn
from _토목_표 import is_unit as _is_unit
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 is_unit(p):
"""단위 글 — 「인」 「㎏」 「인/ton」 따위."""
return bool(p) and all(_is_unit(x) for x in p.split("/"))
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):
"""값 칸 하나 → (값, 원문). 「ab」 는 [아래, 위]."""
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)
def numeric(c):
vs = [r[c] for r in body if r[c] not in DASH]
return bool(vs) and sum(vlike(v) and 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(numeric(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 = [re.sub(r"^([^\s/]+)\s*/\s*([^\s/]+)$", r"\1/\2", 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]
for c in range(1, ncol): # 「〃」 「"」 머리 = 왼쪽과 같음
if row[c] in ("〃", '"', "”"):
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)
if ( # 둘째 머리 단이 몸 첫 줄로 찍힌 표(앞 칸이 비고 나머지가 머리 글)
H == 1
and "H" not in spec
and len(rows) > 2
and rows[1][0] == ""
and sum(x != "" for x in rows[1]) >= 2
and not any(is_member(x) for x in rows[1])
or H == 1
and "H" not in spec
and len(rows) > 2
and rows[1][0] in ("직종", "공종")
and any(is_member(x) for x in rows[1][1:])
):
H = 2
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["L"] if "L" in spec else 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)
and not any(is_num(r[c]) for r in body)
),
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 "구분"
names.update(spec.get("NAMES", {}))
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:
cand = [c for c in txt if c < unitc]
for c in reversed(cand): # 직종 칸 = 값 대부분이 직종 이름인 라벨 칸
vs = [r[c] for r in body if r[c] not in DASH]
if vs and sum(is_member(v) for v in vs) * 2 >= len(vs):
mc = c
break
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 and r < H - 1 else "" for r in range(H)]
if "LN" in spec: # 라벨 칸 옆 칸에 층 이름이 적힌 표
lnames = [jn(rows[r][spec["LN"]]) if r < H - 1 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]
# 값 열 전체가 한 무리 이름 아래(첫 층이 하나뿐) — 그 이름이 조건 이름
top0 = {lay[clay[0]][c] for c in vcols if lay[clay[0]][c]} if clay else set()
gtitle = next(iter(top0)) if len(top0) == 1 and len(clay) > 1 and not lnames[clay[0]] else ""
# 몸 줄 읽기
recs, prev, prevu, sect = [], {}, "", ""
for r in body:
lab = {}
for c in range(L):
if c == unitc or c == mc:
continue
x = jn(r[c])
if x == "〃" or (x == "" and not any(r[q] for q in range(c))):
x = prev.get(c, "")
lab[c] = x
prev.update(lab)
if L and not any(x for x in lab.values()) and not any(r[c] for c in range(L)):
continue
if r[0] and not any(x for x in r[1:]): # 첫 칸만 있는 줄 = 뒤 줄들의 소제목
sect = jn(r[0])
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, skip = unit, "수량", None
if mlay is not None:
meas, uu = strip_unit(nm(lay[mlay][c]))
u2 = u2 or uu
if not meas: # 이름 없는 값 칸 — 위 층의 단위 글(「인/ton」)을 단위로
meas = "수량"
skip = next((q for q in range(mlay - 1, -1, -1) if lay[q][c]), None)
if skip is not None and is_unit(lay[skip][c]):
u2 = u2 or lay[skip][c]
else:
skip = None
if member:
meas = member
for q in ulay:
u2 = u2 or lay[q][c]
cond = {"구분": sect} if sect else {}
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] and q != skip]
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] and q != skip]
if path:
gname = gtitle if (gtitle and path[0] == gtitle) else ""
if gname:
path = path[1:]
if path:
key = gname or ("구분" if "구분" not in cond else "세구분")
cond[key] = " ".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 or re.search(r"\d", meas):
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)
if any(m != "수량" for _, m, _, _ in recs):
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