Files
Aislo/resources/master_data/scripts/_md표_읽기.py
T
eomsangdonandClaude Sonnet 5 c636ad6a87 feat(master_data): 산림 1장 계수 표 추가 · 건설 공통 2장 3장 소요량 옮김
- 계수_산림품셈_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
2026-09-19 16:55:43 +09:00

308 lines
9.7 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 -*-
"""건설품셈 공통 제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")