certflow.services.model_spec_parser 源代码

"""型号→口径/压力 解析器(消减导入黄标,TAPD #1000192 / BLUEPRINT §8.2 第③处缺口)

VBA 时代空白口径靠人工 inputbox 补录(对标 function.vb),PySide6 版当前
``normalize_spec`` 仅解析 ``product_spec`` 字符串、``normalize_pn`` 仅从型号后缀/字典
取压力,导致「合同口径空白」「型号内嵌 DN/PN 未被提取」两类行直接黄标。

本解析器从**产品型号**内嵌的显式标识反推口径/压力,作为
``DNService.resolve_caliber`` / ``PNService.resolve_pn`` 的**兜底**:

- 口径:型号内嵌 ``DN150`` / ``DN100*80`` → 口径值(``150`` / ``100*80``);
- 压力:显式 ``PN16``(PN 为 bar,MPa=PN/10)、英制 ``CLASS1500`` / ``CL1500``
  (磅级→Lb)、以及型号后缀嵌入的**标准公称压力代号**(如 ``J61Y-160`` 的 ``160`` → 16MPa、
  ``EF/PZ-320P`` 的 ``320`` → 32MPa、``BEDDZY-250`` 的 ``250`` → 25MPa)。仅白名单内标准
  代号采信,杜绝流水号误判(如 ``...-13`` 不再误作 1.3MPa)。

设计约束(对齐验收标准):
- **不改写 ``product_spec``(合同口径)**,只供 ``product_spec_norm`` / ``pressure_value``;
- 无型号源 / 未命中显式标识时**返回空、不伪造**,仍走黄标 + 校正队列;
- 纯正则解析、无副作用,便于单测锁定。
"""

from __future__ import annotations

import re

# 显式内嵌口径:DN150 / DN 100 / DN100*80 / DN100x80 / DN100×80
_DN_RE = re.compile(r"DN\s*(\d+(?:\s*[*x×]\s*\d+)?)", re.IGNORECASE)

# 显式内嵌公称压力:PN16 / PN 25 / PN2.5(PN 为 bar,MPa = PN/10)
_PN_RE = re.compile(r"PN\s*(\d+(?:\.\d+)?)", re.IGNORECASE)

# 英制磅级:CLASS1500 / CL1500 / CLASS 900
_CLASS_RE = re.compile(r"(?:CLASS|CL)\s*(\d+)", re.IGNORECASE)

# 型号内嵌压力代号:紧跟 - 或 / 的数字(如 J61Y-160 的 160、EF/PZ-320P 的 320、
# BEDDZY-250 的 250)。仅当数值属于「标准公称压力代号」白名单才采信,避免误吃
# 口径(如 DN150)或流水号(如 ...-13 → 1.3MPa 的误判)。
_EMBED_PN_RE = re.compile(r"[-/](\d+(?:\.\d+)?)(?=[A-Za-z]?(?:[-/\s]|$))")

# 标准公称压力代号(GB/T 阀门口径压力系列,代号 = 10 × MPa)。
# 仅这些数值视为合法型号内嵌压力代号:消除 13/80 等流水号误判,同时覆盖
# 常用标准代号(6, 10, 16, 25, 40, 64, 100, 160, 200, 250, 320, 400, 500, 630, 800, 1000)。
# 注:1(PN1=0.1MPa)极罕见且 /1 多为版本号,排除;2.5 等小数代号歧义大,排除,宁缺毋滥。
_STANDARD_PN_CODES = {6, 10, 16, 25, 40, 64, 100, 160, 200, 250, 320, 400, 500, 630, 800, 1000}

# 标准英制磅级(与 PNService.LB_TO_MPA_MAP 对齐;仅这些视为合法 CLASS)
_KNOWN_LB_LEVELS = {150, 300, 600, 800, 900, 1500, 2500}


def _fmt_mpa(mpa: float) -> str:
    """格式化 MPa 显示值:去尾零(2.0→'2',1.6→'1.6',10.0→'10')。"""
    return f"{mpa:.2f}".rstrip("0").rstrip(".")


[文档] class ModelSpecParser: """从产品型号提取口径/压力的纯解析器(兜底 DNService/PNService)。 所有方法均为无副作用的静态/类方法,输入型号字符串,输出解析结果; 无法解析时返回空串/空 ``pn_display``,绝不伪造。 """ # ============================================================ # 口径 # ============================================================
[文档] @classmethod def parse_caliber(cls, model: str | None) -> str: """从型号内嵌的 ``DNxxx`` 提取口径值。 Args: model: 产品型号,如 ``"BQ647Y-25P/DN150"`` / ``"DN100*80"`` Returns: str: 口径值(``"150"`` / ``"100*80"``);无内嵌 DN 时返回 ``""``。 """ if not model: return "" # 归一:多行/制表符合并为空格,避免 \n 切断型号导致漏判 m = _DN_RE.search(re.sub(r"\s+", " ", str(model)).strip()) if not m: return "" # 归一:去空格,分隔符统一为 "*" raw = m.group(1) return re.sub(r"\s+", "", raw).replace("x", "*").replace("×", "*").replace("X", "*")
# ============================================================ # 压力 # ============================================================
[文档] @classmethod def parse_pressure(cls, model: str | None) -> dict[str, str]: """从型号内嵌标识反推公称压力。 优先级:显式 ``PNxx`` > 英制 ``CLASSxx`` > 型号后缀嵌入压力编码。 Args: model: 产品型号,如 ``"KHBF-25-PN320"`` / ``"CLASS1500-150A"`` Returns: dict: ``{"pn_value", "pn_unit", "pn_display", "source"}``; 无法解析时 ``pn_display`` 为 ``""``、``source`` 为 ``"unknown"``。 """ result = {"pn_value": "", "pn_unit": "", "pn_display": "", "source": "unknown"} if not model: return result # 归一:多行/制表符合并为空格,避免 \n 切断型号导致漏判(如 BEDDZY-250\n(...)) text = re.sub(r"\s+", " ", str(model)).strip() # === 显式 PN:PN 为 bar,MPa = PN/10 === pn = _PN_RE.search(text) if pn: try: mpa = float(pn.group(1)) / 10.0 except ValueError: mpa = 0.0 if mpa > 0: val = _fmt_mpa(mpa) result.update( pn_value=val, pn_unit="MPa", pn_display=f"{val}MPa", source="model_pn" ) return result # === 英制 CLASS/CL → 磅级(Lb) === cl = _CLASS_RE.search(text) if cl: level = int(cl.group(1)) if level in _KNOWN_LB_LEVELS: result.update( pn_value=str(level), pn_unit="Lb", pn_display=f"{level}Lb", source="model_class", ) return result # === 型号后缀/中段嵌入标准压力代号(白名单校验,÷10 得 MPa) === embed = cls._parse_embed_pn(text) if embed: return embed return result
@classmethod def _parse_embed_pn(cls, text: str) -> dict[str, str] | None: """从型号内嵌标准压力代号反推压力(白名单校验)。 仅采信 ``_STANDARD_PN_CODES`` 内的标准公称压力代号(÷10 得 MPa), 杜绝流水号/口径误判(如 ``...-13`` 不再误作 1.3MPa)。 Returns: 命中时返回压力 dict;否则返回 ``None``。 """ for m in _EMBED_PN_RE.finditer(text): try: code = float(m.group(1)) except ValueError: continue # 小数代号歧义大(如 2.5 可能直接表示 MPa),跳过 if code != int(code): continue if int(code) in _STANDARD_PN_CODES: val = _fmt_mpa(code / 10.0) return { "pn_value": val, "pn_unit": "MPa", "pn_display": f"{val}MPa", "source": "model_suffix", } return None