- 계수_산림품셈_01장: 1-1 · 1-2 · 1-6 · 1-7 표 10개 + 글 속 계수 표 13개 추가 (1-3 · 1-4 그대로) - 소요량_건설품셈_공통2장: 표 112개(계수 58개 포함) - 소요량_건설품셈_공통3장: 표 100개(계수 34개 포함) - 옮기는 스크립트 남김 · 틀 · 본문 검사 0건 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
308 lines
9.7 KiB
Python
308 lines
9.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""건설품셈 공통 제3장 md 읽기 · 표 만들기 · 쓰기 도구 (build_소요량_건설품셈_공통3장.py 가 씀)."""
|
||
|
||
import json
|
||
import re
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[3]
|
||
SRC = (
|
||
ROOT / "resources/knowledge/original/원가계산/건설공사_표준품셈/본문/01_공통부문/제03장_토공사"
|
||
)
|
||
OUT = ROOT / "resources/master_data/소요량_건설품셈_공통3장.json"
|
||
DIV = "건설품셈 공통 "
|
||
|
||
|
||
class Raw(str):
|
||
"""원문 수 글자 그대로(0.040 유지)."""
|
||
|
||
|
||
NUMRE = re.compile(r"^\d[\d,]*(\.\d+)?$")
|
||
|
||
|
||
def N(x):
|
||
x = x.strip()
|
||
if not NUMRE.match(x):
|
||
raise ValueError(f"수 아님: {x!r}")
|
||
return Raw(x.replace(",", ""))
|
||
|
||
|
||
def R(x):
|
||
return Raw(x.replace(",", ""))
|
||
|
||
|
||
def join(cell):
|
||
parts = [p.strip() for p in cell.split("<br>")]
|
||
out = parts[0]
|
||
for p in parts[1:]:
|
||
out += ("" if out.endswith("∼") else " ") + p
|
||
return out
|
||
|
||
|
||
def unesc(s):
|
||
return re.sub(r"\\([\\*\-#>.|])", r"\1", s).replace("<br>", " ")
|
||
|
||
|
||
# ── md 읽기 ────────────────────────────────────────────────
|
||
class Tab:
|
||
def __init__(self, rows, base):
|
||
self.rows, self.base = rows, base
|
||
|
||
|
||
def body_lines(path):
|
||
lines = path.read_text(encoding="utf-8").split("\n")
|
||
at = [i for i, x in enumerate(lines) if x.strip() == "---"]
|
||
return lines[at[1] + 1 :]
|
||
|
||
|
||
def parse_tables(path):
|
||
L = body_lines(path)
|
||
out, i = [], 0
|
||
while i < len(L):
|
||
if not L[i].startswith("|"):
|
||
i += 1
|
||
continue
|
||
j = i
|
||
while j < len(L) and L[j].startswith("|"):
|
||
j += 1
|
||
rows = []
|
||
for ln in L[i:j]:
|
||
cells = [c.strip() for c in ln.strip().strip("|").split("|")]
|
||
if all(re.fullmatch(r":?-+:?", c) for c in cells):
|
||
continue
|
||
rows.append(cells)
|
||
k = i - 1
|
||
while k >= 0 and not L[k].strip():
|
||
k -= 1
|
||
m = re.fullmatch(r"\((.*)\)", L[k].strip()) if k >= 0 else None
|
||
out.append(Tab(rows, m.group(1) if m else ""))
|
||
i = j
|
||
return out
|
||
|
||
|
||
FILES = {p.name[:4]: p for p in sorted(SRC.glob("3-*.md"))}
|
||
TABS = {k: parse_tables(p) for k, p in FILES.items()}
|
||
|
||
|
||
def sec_lines(ident):
|
||
"""항 번호(3-2-1)의 본문 줄(제목 포함, 같은·윗 제목 전까지)."""
|
||
L = body_lines(FILES[f"3-{int(ident.split('-')[1]):02d}"])
|
||
for at, ln in enumerate(L):
|
||
m = re.match(rf"^(#+)\s*{re.escape(ident)}(\s|\(|$)", ln)
|
||
if m:
|
||
end = len(L)
|
||
for k in range(at + 1, len(L)):
|
||
h = re.match(r"^(#+)\s", L[k])
|
||
if h and len(h.group(1)) <= len(m.group(1)):
|
||
end = k
|
||
break
|
||
return L[at:end]
|
||
raise KeyError(ident)
|
||
|
||
|
||
def sec_text(ident):
|
||
return unesc("\n".join(sec_lines(ident)))
|
||
|
||
|
||
def notes(ident, n=0):
|
||
"""항의 n번째 [주] 묶음 줄들(원문 글)."""
|
||
blocks, cur = [], None
|
||
for ln in sec_lines(ident):
|
||
s = ln.strip()
|
||
if s == "[주]":
|
||
cur = []
|
||
blocks.append(cur)
|
||
continue
|
||
if cur is None:
|
||
continue
|
||
if re.match(r"^#", s) or s.startswith("[참고") or re.match(r"^[가-힣]\. ", s):
|
||
cur = None
|
||
continue
|
||
if not s or s.startswith(("|", "<!--", "![")) or re.fullmatch(r"\(.*\)", s):
|
||
continue
|
||
cur.append(unesc(s))
|
||
return blocks[n]
|
||
|
||
|
||
def bigo(t, r, c):
|
||
items = []
|
||
for p in (x.strip() for x in t.rows[r][c].split("<br>")):
|
||
if p.startswith("- ") or not items:
|
||
items.append(p)
|
||
else:
|
||
items[-1] += " " + p
|
||
return ["비고 " + x for x in items]
|
||
|
||
|
||
def title(ident):
|
||
ln = re.sub(r"^#+\s*\S+\s+", "", sec_lines(ident)[0])
|
||
return re.sub(r"\('\d\d[^()]*\)$", "", ln).strip()
|
||
|
||
|
||
# ── 표 만들기 ──────────────────────────────────────────────
|
||
TABLES, KEYS = [], set()
|
||
|
||
|
||
def add(t, ident, label, cond, cols, rows, note=(), *, src=None, base=None, group=None, rule=None):
|
||
"""t = 원문 표(기준 밑수용) · label 없으면 항 제목 · src = 출처 꼬리(없으면 항 번호)."""
|
||
label = label or title(ident)
|
||
key = f"{ident} {label}"
|
||
assert key not in KEYS, key
|
||
KEYS.add(key)
|
||
base = (t.base if t else "") if base is None else base
|
||
out = {"열쇠": key, "이름": label, "기준": base, "출처": DIV + (src or ident)}
|
||
if group:
|
||
out["그룹"] = group
|
||
out["조건"] = cond
|
||
if rule:
|
||
out["범위규칙"] = rule
|
||
out.update({"값칸": cols, "줄": rows, "주": list(note)})
|
||
TABLES.append(out)
|
||
|
||
|
||
def sp(row, spec=1):
|
||
"""규격 글(없으면 None) — 값 칸 「<이름>규격」 으로 올림(수를 열쇠에 두지 않음)."""
|
||
return join(row[spec]) if spec is not None and row[spec] not in ("", "-") else None
|
||
|
||
|
||
def crew(t, r0, r1, name_c=0, spec=1, unit=2, qty=3):
|
||
cols, vals, prev = {}, {}, ""
|
||
for r in range(r0, r1 + 1):
|
||
row = t.rows[r]
|
||
n, u = join(row[name_c]), row[unit]
|
||
if u == "〃":
|
||
u = prev
|
||
elif u:
|
||
prev = u
|
||
cols[n] = u
|
||
s_ = sp(row, spec)
|
||
if s_:
|
||
cols[n + "규격"] = "글"
|
||
if row[qty] not in ("", "-"):
|
||
assert n not in vals, n
|
||
vals[n] = N(row[qty])
|
||
if s_:
|
||
vals[n + "규격"] = s_
|
||
return cols, vals
|
||
|
||
|
||
def sg_unit(t):
|
||
for c in t.rows[0] + t.rows[1]:
|
||
m = re.search(r"시공량\s*\((.*?)\)", join(c))
|
||
if m:
|
||
return m.group(1)
|
||
raise ValueError("시공량 단위")
|
||
|
||
|
||
def across(t, cond, hr, first, last, c0, nc, **kw):
|
||
"""조건이 가로 칸(c0..)에 있고 시공량이 첫 몸 줄에 한 번 있는 표."""
|
||
cols, vals = crew(t, first, last, **kw)
|
||
cols["시공량"] = sg_unit(t)
|
||
rows = []
|
||
for k in range(nc):
|
||
rows.append({cond: join(t.rows[hr][c0 + k]), **vals, "시공량": N(t.rows[first][c0 + k])})
|
||
return cols, rows
|
||
|
||
|
||
def grouped(t, cond, first, last, c0=3, step=2):
|
||
"""조건마다 (수량, 시공량) 두 칸."""
|
||
cols, _ = crew(t, first, last)
|
||
cols["시공량"] = sg_unit(t)
|
||
rows = []
|
||
for k in range((len(t.rows[0]) - c0) // step):
|
||
d = {cond: join(t.rows[0][c0 + step * k])}
|
||
for r in range(first, last + 1):
|
||
row, q = t.rows[r], t.rows[r][c0 + step * k]
|
||
if q not in ("", "-"):
|
||
d[join(row[0])] = N(q)
|
||
if sp(row):
|
||
d[join(row[0]) + "규격"] = sp(row)
|
||
d["시공량"] = N(t.rows[first][c0 + step * k + 1])
|
||
rows.append(d)
|
||
return cols, rows
|
||
|
||
|
||
def one(t, first, last, sg=None, **kw):
|
||
"""조건 없는 표 — 줄 하나. sg = 시공량 칸(열 번호)."""
|
||
cols, vals = crew(t, first, last, **kw)
|
||
if sg is not None:
|
||
cols["시공량"] = sg_unit(t)
|
||
vals["시공량"] = N(t.rows[first][sg])
|
||
return cols, [vals]
|
||
|
||
|
||
def rate(t, cond):
|
||
"""머리 = 조건, 둘째 줄 = 값 칸 하나(요율 %)."""
|
||
label = t.rows[1][0]
|
||
rows = [
|
||
{cond: t.rows[0][c], label: R(t.rows[1][c].rstrip("%"))} for c in range(1, len(t.rows[0]))
|
||
]
|
||
return {label: "%"}, rows
|
||
|
||
|
||
def coef(ident, label, suffix, rows):
|
||
"""글 속 계산용 수 — 정규식으로 본문에서 읽음. (구분, 식, 단위[, 원문식])"""
|
||
text = sec_text(ident)
|
||
out, has_orig = [], False
|
||
for lab, pat, unit, *orig in rows:
|
||
m = re.search(pat, text)
|
||
if not m:
|
||
raise ValueError(f"{ident} {lab}: {pat}")
|
||
g = [R(x) for x in m.groups()]
|
||
d = {"구분": lab, "값": g[0] if len(g) == 1 else g, "단위": unit}
|
||
if orig:
|
||
d["값원문"], has_orig = re.search(orig[0], text).group(0), True
|
||
out.append(d)
|
||
cols = {"값": "구분 따름", "단위": "글"}
|
||
if has_orig:
|
||
cols["값원문"] = "글"
|
||
add(None, ident, label, {"구분": "고르기"}, cols, out, src=f"{ident} {suffix}", group="계수")
|
||
|
||
|
||
def rng(text):
|
||
nums = [R(x) for x in re.findall(r"\d[\d,]*(?:\.\d+)?", text)]
|
||
lo, hi = "이상" in text, "미만" in text or "이하" in text
|
||
if lo and hi:
|
||
return [nums[0], nums[1]]
|
||
return [None, nums[0]] if hi else [nums[0], None]
|
||
|
||
|
||
PCT = "([\\d.]+)%"
|
||
LOSE = ("공구손료 및 경장비 인력품 비율", r"인력품의 ([\d.]+)%", "%")
|
||
|
||
|
||
# ── 쓰기 ───────────────────────────────────────────────────
|
||
def js(v):
|
||
if isinstance(v, Raw):
|
||
return str(v)
|
||
if v is None:
|
||
return "null"
|
||
if isinstance(v, str):
|
||
return json.dumps(v, ensure_ascii=False)
|
||
if isinstance(v, list):
|
||
return "[" + ", ".join(js(x) for x in v) + "]"
|
||
if isinstance(v, dict):
|
||
return "{ " + ", ".join(f"{js(k)}: {js(x)}" for k, x in v.items()) + " }" if v else "{}"
|
||
raise TypeError(v)
|
||
|
||
|
||
def write():
|
||
L = ["{", ' "그룹": "소요량",', ' "원문": "건설품셈",', ' "판": "2026",', ' "표": [']
|
||
for ti, t in enumerate(TABLES):
|
||
L.append(" {")
|
||
keys = list(t)
|
||
for ki, k in enumerate(keys):
|
||
end = "," if ki < len(keys) - 1 else ""
|
||
v = t[k]
|
||
if k in ("줄", "주") and v:
|
||
L.append(f" {js(k)}: [")
|
||
L += [
|
||
f" {js(x)}" + ("," if xi < len(v) - 1 else "") for xi, x in enumerate(v)
|
||
]
|
||
L.append(f" ]{end}")
|
||
else:
|
||
L.append(f" {js(k)}: {js(v)}{end}")
|
||
L.append(" }" + ("," if ti < len(TABLES) - 1 else ""))
|
||
L += [" ]", "}", ""]
|
||
OUT.write_text("\n".join(L), encoding="utf-8", newline="\n")
|