390 lines
15 KiB
Python
390 lines
15 KiB
Python
# -*- 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):
|
||
"""값 칸 하나 → (값, 원문). 「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)
|
||
|
||
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
|