Files
Aislo/M02_MasterTemplete/M02_Machine_Find.py
T
eomsangdonandClaude Opus 5.5 ae8d098a10 fix(815-3): 합친 뒤 시험 셋 가름 · 시간당 경비 식 조종원 차례 · 호표 기계 글 머리 꼬리 · 합친 판 요약 다시 잼
- 809_1 이름 = 진짜 흠 + 낡은 기대 — 읽기기(812-1)가 지은 율 줄 이름 「율」이 엑셀 글 머리로 안 바뀜 → `Import_Shape._MADE` 에 율 더함 · 합계 = 차림 글줄 · 기계 = 한 줄(이름 · 규격 칸) 은 낡은 기대 고침 · 금액 15,935 · 336 · 723 · 16,994 그대로
- 812_9 별칭 = 낡은 기대 — 815-2 가 아스팔트페이버 별칭을 마스터 기계 줄 칸으로 옮겨 「넣기 전 못 찾음」 이 안 맞음 · 시험은 마스터 칸으로 봄 · 부르는 곳 없는 `M02_Machine_Find.learn`(옛 장부 쓰기) · `by_code` · `same_kind`(걷은 연결 찾기 몫) 지움
- 804_2 hourly = 낡은 기대 — 814-1 이 GC000268 목록을 117 → 209 로 늘려 겹친 116 대 고름이 GC000269 로 뒤집힘(116 대 모두 원 단위 같음) · 같은 로직 줄만 대던 시험 → 같은 갈래 식(부착 · 손료만 · 조종원) 줄 모두 댐
- 진짜 흠(브레인 갈래 가) — GC000272 「조종원 없는 기종」 목록에 기계표 조종원 1 인 9 대가 겹쳐 목록 길이 차례로 고름 → `logic_of` = 기계표 조종원 > 0 이면 조종원 호표 든 식 먼저 · 불러오기 `hourly_link` 도 같은 길 · 바뀐 4 대 = 진동롤러(핸드가이드식) 0.7ton 6,557 → 41,969 · 래머 80kg 1,959 → 23,708 · 플레이트 콤팩터 1.5ton 2,855 → 24,604 · 발전기 100kW 45,535 → 67,284(M01 문서 금액 그대로) · 시험에 9 대 굳힘
- 778_5 기계 표 = 진짜 흠 — 812-9 찾기가 호표 기계 글 「운 반 : 덤프트럭(4.5Ton)」 · 「…[hr] : 90%」 · 「(90%)」 머리 꼬리를 안 뗌 → `Hopyo_Fixed._eqs` 쌍점 조각 · 율 꼬리 뗌 · 이어짐 187 · 고를 것 1 · 못 이음 2(조합 한 줄 · 괄호 깨짐) · 모두 이음 블록 161
- 721 단추 자리 = 낡은 기대(808-1 [단가 갱신][재생성] tools 더함)
- 815-1 보고 요약에 「합친 뒤」 열 — 기계 이음 95.7 % · 우리 값 원파일 1,891 · 0 원 줄 블록 147

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:32:28 +09:00

317 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.
"""M02 기계 찾기(PLAN 812-9 · 요구 718 · 719) — 가져온 기계 글(이름 · 규격) → 우리 EQ · 서버 단독.
과녁 = M01 기계표 전체(EQ) · 부르는 곳 = 불러오기 이음(`M02_Hopyo_Row.m01_picks` · `M02_San_Emit._m01`) ·
일위대가 호표(`M02_Hopyo_Elem.machine(s)` · `M02_Hopyo_Fixed._eqs`).
차례:
1. 별칭(M01 기계 줄 「별칭」 칸 · 815-1 `M02_Elem_Alias`) — 「이름 규격」 통째 별칭 = 그 EQ(여럿 =
후보 여럿) · 이름만 별칭 = 그 EQ 기종으로 바꿔 2 로
2. 기종 = EQ 이름(괄호 뗌) · 괄호 붙임 · 상세구분과 같은 글 › 그 글을 품은 EQ(표기 정규화 `norm`)
3. 괄호 낱말 — 기계표 괄호에 있는 낱말(무한궤도 · 타이어 · 자주식 …)만 거름 · 표에 없는 낱말(발파 · 리핑)은 안 봄
4. 규격 = 수 + 단위(범위 · 여럿) 모두 EQ 규격에 같은 값(단위 한쪽 없음 = 봄) › 없으면 EQ 범위 안
하나 = 이음 · 여럿 = 「후보 여럿」 · 0 = 「못 찾음」(가까운 규격으로 대신 잇지 않음)
조합 기계(「+」 · 괄호 속 다른 기종 · 「…조합」 · 「덤프트럭(자동덮개)」) = `parts` 로 갈라 각각 찾음 ·
규격 없는 쪽은 앞 쪽 규격.
"""
from __future__ import annotations
import math
import re
import time
import unicodedata
from functools import lru_cache
from typing import Any
from M01_MasterData import M01_MasterData_Store as m01
# 소리만 다른 표기 → 한 글(양쪽에 같이 씀 · 차례대로)
SOUND = (
("로우더", "로더"), ("로울러", "롤러"), ("브레이카", "브레이커"), ("브레카", "브레이커"),
("굴삭기", "굴착기"), ("굴삭", "굴착"), ("불도우저", "불도저"), ("램머", "래머"),
("크로울러", "크롤러"), ("콤프레샤", "공기압축기"), ("컴프레셔", "공기압축기"),
("컴프레서", "공기압축기"), ("컴팩터", "콤팩터"), ("타이어형", "타이어"), ("젯트", "제트"),
("후렉시블", "플렉시블"), ("스프레야", "스프레이어"), ("코어", "코아"),
) # fmt: skip
_UNIT = (
("m3/min", "m3/min"), ("m3/분", "m3/min"), ("m3/hr", "m3/hr"), ("m3/h", "m3/hr"),
("km/hr", "km/hr"), ("km/h", "km/hr"), ("l/min", "l/min"), ("t/hr", "t/hr"),
("kg/cm2", "kg/cm2"), ("kg/min", "kg/min"), ("m3", "m3"), ("톤용", "ton"), ("ton", "ton"),
("톤", "ton"), ("t", "ton"), ("kw", "kw"), ("mm", "mm"), ("cm", "cm"), ("kg", "kg"),
("km", "km"), ("m", "m"), ("l", "l"), ("hp", "hp"), ("amp", "a"), ("cc", "cc"), ('"', "in"),
("inch", "in"), ("인치", "in"),
) # fmt: skip
_NUM = r"\d+(?:\.\d+)?"
_TOKEN = re.compile(
rf"({_NUM})(?:\s*([~∼\-])\s*({_NUM}))?\s*("
+ "|".join(re.escape(u) for u, _ in _UNIT)
+ r")?(?![a-z])"
)
_PAREN = re.compile(r"[(\[]([^()\[\]]*)[)\]]")
_EXTRA = re.compile(
r"[:,]?\s*\(?할증율?[^)]*\)?"
) # 「0.2㎥:할증120%」 · 「(할증125%)」 = 율(규격 아님)
NOTE = "참고 · "
LINKED, ALIAS, MANY, NONE = "이어짐", "별칭", "후보 여럿", "못 찾음"
SHOW = 8 # 후보 글 · 목록 상한
def norm(text: Any) -> str:
"""표기 정규화 — NFKC · 작은 글자 · 빈칸 뺌 · 소리 표기 한 글."""
t = re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or "")).lower())
for old, new in SOUND:
t = t.replace(old, new)
return t
def _clean(text: Any) -> str:
t = unicodedata.normalize("NFKC", str(text or "")).lower()
t = _EXTRA.sub(" ", t)
return re.sub(r"(?<=\d),(?=\d{3}(?!\d))", "", t) # 천 자리 쉼표
def tokens(text: Any) -> list[tuple[float, float, str | None]]:
"""규격 글 → [(아래, 위, 단위)] — 범위 「10~12」 · 여럿 「32m,80~95㎥/hr」 · 단위 없음 = None."""
out = []
for m in _TOKEN.finditer(_clean(text)):
lo, sep, hi, unit = float(m.group(1)), m.group(2), m.group(3), m.group(4)
top = float(hi) if hi else lo
if sep == "-" and top <= lo: # 「50-2.6m」 = 범위 아님 — 앞 수만
top = lo
canon = dict(_UNIT).get(unit) if unit else None
if canon == "in": # 인치 = ㎝(×2.54 · 우리 표는 ㎝)
lo, top, canon = round(lo * 2.54, 6), round(top * 2.54, 6), "cm"
out.append((lo, top, canon))
return out
def _unit_ok(a: str | None, b: str | None) -> bool:
return a is None or b is None or a == b or {a, b} <= {"ton", "t/hr"}
def spec_fit(mine: list, theirs: list) -> str | None:
"""가져온 규격 수가 EQ 규격에 다 있음 = 「같음」 · EQ 범위 안 = 「안」 · 아니면 None."""
if not mine:
return "같음" if not theirs else None
same = all(
any(abs(a - c) < 1e-9 and abs(b - d) < 1e-9 and _unit_ok(u, w) for c, d, w in theirs)
for a, b, u in mine
)
if same:
return "같음"
inside = all(
any(c - 1e-9 <= a and b <= d + 1e-9 and c < d and _unit_ok(u, w) for c, d, w in theirs)
for a, b, u in mine
)
return "안" if inside else None
def _words(inner: str) -> list[str]:
return [norm(w) for w in re.split(r"[\s,;·/]+", inner) if w.strip()]
def split_name(text: Any) -> tuple[str, list[list[str]], list[str]]:
"""이름(+규격) 글 → (기종 글 정규화, 괄호 낱말 묶음들, 규격 조각들) — 괄호 속 수 = 규격."""
raw = _clean(text)
groups, specs = [], []
for inner in _PAREN.findall(raw):
bits = [b for b in re.split(r"[,;]", inner) if b.strip()]
specs += [b for b in bits if re.search(r"\d", b)]
groups.append([w for b in bits if not re.search(r"\d", b) for w in _words(b)])
base = _PAREN.sub(" ", raw)
specs += [m.group(0) for m in _TOKEN.finditer(base)]
base = _TOKEN.sub(" ", base)
return norm(base), groups, specs
# ── 기계표 ────────────────────────────────────────────────────────────
class Row:
__slots__ = ("raw", "key", "base", "detail", "joined", "full", "words", "spec", "said")
def __init__(self, r: dict) -> None:
name = str(r.get("이름") or "")
self.raw, self.key = r, str(r.get("키"))
inner = _PAREN.findall(unicodedata.normalize("NFKC", name))
self.words = {w for x in inner for w in _words(x)}
self.base = norm(_PAREN.sub("", unicodedata.normalize("NFKC", name)))
self.detail = norm(r.get("상세구분"))
self.joined = {w + self.base for w in self.words} | {self.base + w for w in self.words}
self.full = norm(name)
self.spec = tokens(r.get("규격"))
self.said = norm(
_TOKEN.sub(" ", _clean(r.get("규격")))
) # 규격 글 낱말(「더블」 · 「엔진식」)
@lru_cache(maxsize=2)
def _table(_whole: int) -> tuple[list[Row], frozenset[str], dict[str, Row]]:
rows = [Row(r) for r in m01.loaded()[1].numbers.get("EQ", {}).values()]
words = frozenset(w for r in rows for w in r.words)
return rows, words, {r.key: r for r in rows}
_SEEN: list = [0.0, None] # (때, 표) — M01 판 확인(파일 도장)은 1 초에 한 번
def table() -> tuple[list[Row], frozenset[str], dict[str, Row]]:
now = time.monotonic()
if _SEEN[1] is None or now - _SEEN[0] > 1.0:
_SEEN[:] = [now, _table(id(m01.loaded()[1]))]
return _SEEN[1]
def kind_rows(base: str) -> list[Row]:
"""기종 글 → EQ 줄 — 이름 · 괄호 붙임 같음 › 상세구분 같음 › 그 글을 품음(첫 층만)."""
if len(base) < 2:
return []
rows = table()[0]
for test in (
lambda r: base == r.base or base in r.joined,
lambda r: base == r.detail,
lambda r: base in r.full or base in r.detail,
):
got = [r for r in rows if test(r)]
if got:
return got
return []
def _kind_word(word: str, base: str) -> str | None:
"""괄호 낱말이 다른 기종(조합)이면 그 기종 글 — 「부착용집게」 · 「브레이커조합」 · 「자동덮개」."""
w = word.removesuffix("조합")
if len(w) < 2 or w == base:
return None
rows = table()[0]
if any(w in (r.base, r.detail) for r in rows):
return w
if any(r.base.startswith(base + w) for r in rows):
return base + w
return None
# ── 조합 기계 ─────────────────────────────────────────────────────────
def parts(name: Any, spec: Any = "") -> list[tuple[str, str]]:
"""기계 글 → [(이름, 규격)] — 「+」 · 괄호 속 다른 기종이면 둘 이상 · 규격 없는 쪽 = 앞 쪽 규격 ·
하나면 받은 그대로."""
text = f"{name or ''} {spec or ''}".strip()
known = table()[1]
out: list[tuple[str, str]] = []
for piece in [p for p in text.split("+") if p.strip()] or [text]:
main, base, extra = _clean(piece), split_name(piece)[0], []
for m in _PAREN.finditer(_clean(piece)):
bits = [b for b in re.split(r"[,;]", m.group(1)) if b.strip()]
words = [w for b in bits if not re.search(r"\d", b) for w in _words(b)]
kind = next((k for w in words if w not in known and (k := _kind_word(w, base))), None)
if kind:
main = main.replace(m.group(0), " ")
extra.append((kind, ",".join(b for b in bits if re.search(r"\d", b))))
bare = _TOKEN.sub(
" ", _PAREN.sub(lambda m: " " if re.search(r"\d", m.group(1)) else m.group(0), main)
)
own = " ".join(split_name(main)[2])
out += [(" ".join(bare.split()).strip(" ,"), own), *extra]
if len(out) < 2:
return [(str(name or ""), str(spec or ""))]
first = next((s for _, s in out if s), "")
return [(n, s or first) for n, s in out]
# ── 별칭(M01 기계 줄 「별칭」 칸 · 815-1) ───────────────────────────────────
def alias(name: Any, spec: Any) -> list[str]:
"""M01 기계 줄 별칭(`M02_Elem_Alias`) — 규격이 있으면 「이름 규격」 통째 별칭(규격 속 할증 글은 뺌) ·
규격 빔 = 이름 별칭 · 없으면 []."""
from M02_MasterTemplete import M02_Elem_Alias as ea
rows = ea.book()["별칭"].get("EQ", {})
return list(rows.get(ea.am.norm(name) + ea.am.norm(_clean(spec)), []))
# ── 찾기 ──────────────────────────────────────────────────────────────
def _brief(r: Row) -> dict[str, Any]:
raw = r.raw
return {"키": r.key, "이름": raw.get("이름"), "규격": raw.get("규격") or "",
"원문번호": raw.get("원문번호")} # fmt: skip
def _query(rows: list[Row], base: str) -> str:
names = {str(r.raw.get("이름")) for r in rows}
if len(names) == 1:
return names.pop()
details = {str(r.raw.get("상세구분")) for r in rows}
return details.pop() if len(details) == 1 else base
def _ans(kind: str, rows: list[Row], query: str, why: str = "") -> dict[str, Any]:
key = rows[0].key if kind in (LINKED, ALIAS) else None
return {"판정": kind, "키": key, "후보": [_brief(r) for r in rows[:SHOW]], "찾기어": query,
"까닭": why, "수": len(rows)} # fmt: skip
def find(name: Any, spec: Any = "") -> dict[str, Any]:
"""기계 하나(조합 아님) → `{판정, 키, 후보, 찾기어, 까닭, 수}`."""
rows_by = table()[2]
hit = [rows_by[k] for k in alias(name, spec) if k in rows_by]
if hit: # 별칭 하나 = 그 EQ · 여럿 = 후보 여럿
if len(hit) == 1:
return _ans(ALIAS, hit, str(hit[0].raw.get("이름")))
return _ans(MANY, hit, _query(hit, hit[0].base), f"후보 {len(hit)}")
base = split_name(name)[0]
_, groups, specs = split_name(f"{name or ''} {spec or ''}") # 규격 괄호 낱말 「3.6m(일반용)」
groups.append(
_words(_TOKEN.sub(" ", _PAREN.sub(" ", _clean(spec))))
) # 규격 글 낱말 「탑승유압식」
named = [rows_by[k] for k in alias(name, "") if k in rows_by]
if named and len({r.base for r in named}) == 1: # 이름만 별칭 = 그 EQ 기종으로
base = named[0].base
mine = tokens(" ".join(specs))
kinds = kind_rows(base)
known = table()[1]
rows = kinds
for w in (w for g in groups for w in g):
if w in known: # 기계표 괄호 낱말 = 꼭 맞아야 함
rows = [r for r in rows if w in r.full]
elif len(w) > 1 and (kept := [r for r in rows if w in r.said]): # 규격 글 낱말 = 좁힐 때만
rows = kept
if not mine: # 규격 수 없음 = 그 기종 전부(괄호 · 규격 낱말로 거른 것)
got = rows
else:
same = [r for r in rows if spec_fit(mine, r.spec) == "같음"]
got = same or [r for r in rows if spec_fit(mine, r.spec) == "안"]
if len(got) == 1: # 이름만 별칭으로 기종을 정했으면 「별칭」
return _ans(ALIAS if named else LINKED, got, str(got[0].raw.get("이름")))
if got:
return _ans(MANY, got, _query(got, base), f"후보 {len(got)}")
if not kinds:
return _ans(NONE, [], base, "기종 모름 — 별칭")
why = "우리 표에 없음(규격)" if rows else "우리 표에 없음(괄호 낱말)"
return _ans(
NONE, sorted(rows or kinds, key=lambda r: _gap(mine, r.spec)), _query(kinds, base), why
)
def _gap(mine: list, theirs: list) -> float:
"""가까운 규격 차례(보여주기만) — 첫 수끼리 비."""
if not mine or not theirs or mine[0][0] <= 0 or theirs[0][0] <= 0:
return math.inf
return abs(math.log(mine[0][0] / theirs[0][0]))
def find_all(name: Any, spec: Any = "") -> list[dict[str, Any]]:
"""기계 글 → 부분마다 `find` + {이름, 규격}(조합 = 둘 이상)."""
out: list[dict[str, Any]] = []
for n, s in parts(name, spec):
if out and s and not split_name(n)[2] and s == out[0]["규격"] and out[0]["키"]:
s = str(out[0]["후보"][0]["규격"]) # 앞 줄 규격을 받은 쪽 = 이은 우리 규격(단위까지)
out.append({**find(n, s), "이름": n, "규격": s})
return out
def mark(got: dict[str, Any], name: Any, spec: Any) -> str | None:
"""못 이은 기계 표지 글(「참고 · 」 머리) — 이음 · 별칭 = None."""
said = f"「{' '.join(x for x in (str(name or ''), str(spec or '')) if x)}」"
if got["판정"] == MANY:
names = " · ".join(f"{c['이름']} {c['규격']}".strip() for c in got["후보"])
return f"{NOTE}기계 후보 {got['수']} — 고르기 {said}: {names}"
if got["판정"] == NONE:
return f"{NOTE}기계 못 찾음 — {said} {got['까닭']}"
return None