# -*- coding: utf-8 -*- """ 未完成分类真实数据清洗 —— 定位销(PIN)/勾销缸(HOK)/翻转缸(ROT)/电缸(ELEC) 命名规则来自 Univer 官方图册 CODIFICATION KEY(pdftotext 提取核对)。 解析采用「官方值域校验 + 左对齐优先」的 DFS 填空,可正确处理省略了空位的型号 (如 ULP50040SK0 = rod 空 + orient=S + sensor=K)。 用法: PYTHONUTF8=1 python gen-pending-series.py --scan # 只扫 X 盘并缓存 PYTHONUTF8=1 python gen-pending-series.py --dry-run # 解析统计,不写文件 PYTHONUTF8=1 python gen-pending-series.py --cat PIN # 只处理某分类 PYTHONUTF8=1 python gen-pending-series.py # 全部生成 CSV/meta PYTHONUTF8=1 python gen-pending-series.py --copy # 同时复制 STEP(X盘慢) """ import os, re, sys, json, shutil, collections BASE = r'X:/新开发产品-Research and Development/1_产品3D数模(发客户仿真)' DST = r'D:/开发/OnebotCatalog/onebot-data/catalog' CACHE = r'D:/开发/OnebotCatalog/onebot-data/tools/_scan_cache.json' ARGS = sys.argv[1:] DRY = '--dry-run' in ARGS DO_COPY = '--copy' in ARGS DO_SCAN = '--scan' in ARGS ONLY_CAT = ARGS[ARGS.index('--cat') + 1].upper() if '--cat' in ARGS else None # 所有待扫目录(各分类共用,一次扫完缓存) ALL_DIRS = ['2-伸缩销', '2019 Math Data 铁臂', '2019-NEW product', '2020UNICLAMP数模汇总', '3-勾销缸', '4-翻转缸', '1-夹紧缸/电缸', '5-夹臂', 'GR8_REV02'] # ---------------------------------------------------------------- 官方值域 D_SENSOR_PIN = set('NKDJY') # 定位销传感器 D_SENSOR_PIV = set('NKYJA') # 翻转缸传感器 D_SENSOR_PC = set('NKDJY') # 勾销缸传感器 D_ORIENT = set('XEONS') D_MOUNT = set('AS') D_ROD_ULP = set('ABCDEFHILM') D_ROD_ULA = set('AGNP') D_ROD_ULB = set('BCE') D_ROD_ULT = set('AG') D_MODEL = set('JRF') D_PDIA = set('123456') D_LOC = set('01') D_HOOK = set('SD') D_HOOKPOS = set('BC') D_TPOS = set('OPVZ') D_BRAKE = set('BMW') D_CONN = set('SDFPC') D_LAEPOS = set('NOV') D_LAESTY = set('CDSN') # ---------------------------------------------------------------- 中文显示名 SENSOR_PIN = {'N': '无传感器(带保护板)', 'K': '电子 PNP M12 (DF-K)', 'D': '气动传感器 (DF-UPNW)', 'J': '电子 NPN 光学 (DF-J)', 'Y': '电子 PNP M12 白LED (DF-Y)', '': '不适用'} SENSOR_PIV = {'N': '无传感器(带保护板)', 'K': '电子 PNP M12 (DF-K)', 'Y': '电子 PNP M12 白LED (DF-Y)', 'J': '电子 NPN M12 (DF-J)', 'A': '电子 PNP 光学 M12 + 制动传感器', '': '不适用'} ORIENT = {'X': '对称', 'E': '东', 'O': '西', 'N': '北', 'S': '南', '': '不适用'} MOUNT = {'A': '55 mm', 'S': '标准', '': '不适用'} ROD_ULP = {'A': '偏移销杆', 'B': '成型端杆', 'C': '十字成型端 内Ø16', 'D': '偏移销杆(加大销孔)', 'E': '十字成型端 内Ø10', 'F': 'NAAMS 兼容 内Ø16', 'H': '平端 内Ø10', 'I': '平端 内Ø12', 'L': '内Ø12', 'M': '内Ø20', '': '不适用'} ROD_ULA = {'A': '偏移销杆(2螺钉1销)', 'N': '偏移销杆反孔(1螺钉2销)', 'G': '偏移销杆 杆面与本体中轴对齐', 'P': '偏移销杆反孔 杆面与中轴对齐', '': '不适用'} ROD_ULB = {'B': '成型端杆', 'C': '十字成型端 内Ø16', 'E': '十字成型端 内Ø10', '': '不适用'} ROD_ULT = {'A': '偏移销杆', 'G': '偏移销杆 杆面与本体中轴对齐', '': '不适用'} MODEL_DISP = {'J': '标准', 'R': '带机械锁紧/解锁装置', 'F': '无补偿', '': '不适用'} LOCATING = {'0': '不含定位销', '1': '含定位销', '': '不适用'} HOOK = {'S': '单钩', 'D': '双钩', '': '不适用'} HOOKPOS = {'B': '标准', 'C': '旋转180°', '': '不适用'} TABLEPOS = {'O': '水平 90°', 'P': '水平 O 镜像', 'V': '垂直 180°', 'Z': '垂直 V 镜像', '': '不适用'} BRAKE = {'B': '带制动', 'M': '带制动+手动解锁', 'W': '无制动', '': '不适用'} CONN = {'S': '左侧(标准)', 'D': '右侧', 'F': '前侧', 'P': '后侧', '': '不适用'} # LAGP 旧码特有值(官方图册已改名 UAGP,无记载,按数据实际取值标注) SENSOR_PIV_LEGACY = dict(SENSOR_PIV, U='旧码传感器位') LAEPOS = {'N': '无臂', 'O': '180°', 'V': '90°', '': '不适用'} LAESTY = {'C': '中央 15mm offset', 'D': '右侧 15mm offset', 'S': '左侧 15mm offset', 'N': '无臂', '': '不适用'} # 维度元信息: code -> (zh, en, keywords, unit, type) DIM_META = { 'size': ('规格', 'Size', 'guige size', 'mm', 'number'), 'stroke': ('行程', 'Stroke', 'xingcheng stroke', 'mm', 'number'), 'rod': ('杆端样式', 'Rod End Style', 'ganduan rod end', '', 'enum'), 'orient': ('杆方向', 'Rod Orientation', 'ganfangxiang orientation', '', 'enum'), 'mount': ('侧装方式', 'Side Mounting', 'cezhuang mounting', '', 'enum'), 'sensor': ('传感器', 'Sensor', 'chuanganqi sensor', '', 'enum'), 'pcmodel': ('型式', 'Model', 'xingshi pin clamp model', '', 'enum'), 'prog': ('流水号', 'Progressive No.', 'liushuihao prog special', '', 'enum'), 'pin_dia': ('销径', 'Pin Diameter', 'xiaojing pin dia', 'mm', 'number'), 'locating': ('定位销', 'Locating Pin', 'dingweixiao locating pin', '', 'enum'), 'hook': ('挂钩', 'Hook', 'guagou hook', '', 'enum'), 'hook_pos': ('挂钩位置', 'Hook Position', 'guagouweizhi hook pos', '', 'enum'), 'table_pos': ('工作台位置', 'Table Position', 'gongzuotai table pos', '', 'enum'), 'rotation': ('翻转角度', 'Pivot Rotation', 'fanzhuanjiaodu rotation', '°', 'number'), 'brake': ('制动系统', 'Brake System', 'zhidong brake', '', 'enum'), 'conn': ('接口方向', 'Connections', 'jiekou connections', '', 'enum'), 'pos': ('臂位置', 'Arm Position', 'biweizhi arm pos', '', 'enum'), 'style': ('臂样式', 'Arm Style', 'biyangshi arm style', '', 'enum'), 'control': ('控制单元', 'Control Unit', 'kongzhi control unit', '', 'enum'), } DIM_DISPLAY = { 'sensor': None, 'orient': ORIENT, 'mount': MOUNT, 'pcmodel': MODEL_DISP, 'locating': LOCATING, 'hook': HOOK, 'hook_pos': HOOKPOS, 'table_pos': TABLEPOS, 'brake': BRAKE, 'conn': CONN, 'pos': LAEPOS, 'style': LAESTY, } # ---------------------------------------------------------------- 工具 def fill_dims(s, dims, domains, prefer_left=True): """把字符串 s 填进 dims(允许维度为空),左对齐优先。返回 dict 或 None。""" def dfs(i, j): if i == len(s): return {d: '' for d in dims[j:]} if j >= len(dims): return None order = [True, False] if prefer_left else [False, True] for eat in order: if eat: if s[i] in domains.get(dims[j], set()): r = dfs(i + 1, j + 1) if r is not None: return dict(r, **{dims[j]: s[i]}) else: # 当前维度为空 if j + 1 <= len(dims): r = dfs(i, j + 1) if r is not None: return dict(r, **{dims[j]: ''}) return None return dfs(0, 0) def norm(raw): """去全部扩展名 / 大写 / 去空格 / 组合件取 '-' 前主码""" base = raw.strip() while True: # 处理 .stp.STP 这类多重扩展名 stem, ext = os.path.splitext(base) if ext.lower() in ('.stp', '.step'): base = stem else: break s = re.sub(r'\s+', '', base.upper()) if '-' in s: # 与定位销组合的勾销缸:取主码 s = s.split('-')[0] return s def dig(s, n): return s[:n] if len(s) >= n else None # ---------------------------------------------------------------- 解析器 def parse_pin(stem, ver, rod_dom): """UL[]""" m = re.match(r'^UL' + ver + r'(\d{2})(\d{3})(.+)$', stem) if not m: return None size, stroke, rest = m.groups() if rest.endswith('X') and len(rest) > 4: # ATEX rest = rest[:-1] if len(rest) < 2: return None if not rest[-1].isdigit(): # product revision return None body = rest[:-1] dims = {'A': ['rod', 'mount', 'sensor'], 'B': ['rod', 'sensor'], 'L': ['rod', 'orient', 'sensor'], 'M': ['rod', 'orient', 'sensor'], 'O': ['rod', 'sensor'], 'P': ['rod', 'orient', 'sensor'], 'R': ['rod', 'orient', 'sensor'], 'T': ['rod', 'sensor'], 'X': ['rod', 'sensor'], 'Y': ['rod', 'orient', 'sensor'], 'Z': ['rod', 'orient', 'sensor']}[ver] domains = {'rod': rod_dom, 'orient': D_ORIENT, 'mount': D_MOUNT, 'sensor': D_SENSOR_PIN} got = fill_dims(body, dims, domains) if got is None: return None return dict(got, series='UL' + ver, size=int(size), stroke=int(stroke)) def parse_usp(stem, series): """US[P|L][] _ = 该位为空""" m = re.match(r'^US[PL](\d{2})([A-Z_])([0-9_])([0-9_])([0-9_]{3})([A-Z_])([0-9_])[A-Z]*$', stem) if not m: return None size, mdl, pdia, loc, pg, sen, _rev = m.groups() return {'series': series, 'size': int(size), 'pcmodel': '' if mdl == '_' else mdl, 'pin_dia': int(pdia) if pdia.isdigit() else None, 'locating': '' if loc == '_' else loc, 'prog': '' if set(pg) == {'_'} else pg, 'sensor': '' if sen == '_' else sen} def parse_usr(stem): """USR _ = 该位为空""" m = re.match(r'^USR(\d{2})([A-Z_])([0-9_])([0-9_])([A-Z_])([0-9_]{3})([A-Z_])([0-9_])$', stem) if not m: return None size, mdl, pdia, loc, hook, pg, sen, _rev = m.groups() return {'series': 'USR', 'size': int(size), 'pcmodel': '' if mdl == '_' else mdl, 'pin_dia': int(pdia) if pdia.isdigit() else None, 'locating': '' if loc == '_' else loc, 'hook': '' if hook == '_' else hook, 'prog': '' if set(pg) == {'_'} else pg, 'sensor': '' if sen == '_' else sen} def parse_uss(stem, series): """US[SL|S]""" m = re.match(r'^US(?:SL|S)(\d{2})([A-Z])([A-Z])(\d)(\d)(\d{3})([A-Z])(\d)$', stem) if not m: return None size, mdl, hpos, pdia, loc, pg, sen, _rev = m.groups() return {'series': series, 'size': int(size), 'pcmodel': mdl, 'hook_pos': hpos, 'pin_dia': int(pdia), 'locating': loc, 'prog': pg, 'sensor': sen} def parse_uagp(stem): """UAGP _ 表示该位为空""" m = re.match(r'^UAGP(\d{3})([A-Z_])([A-Z_])(\d{3})([A-Z])([A-Z_])[A-Z0-9_]*$', stem) if not m: return None size, tp, sen, rot, brk, cn = m.groups() return {'series': 'UAGP', 'size': int(size), 'table_pos': '' if tp == '_' else tp, 'sensor': '' if sen == '_' else sen, 'rotation': int(rot), 'brake': brk, 'conn': '' if cn == '_' else cn} def parse_lagp(stem): """LAGP[] (旧码,部分无 conn)""" m = re.match(r'^LAGP(\d{3})([A-Z_])([A-Z_])(\d{3})([A-Z])([A-Z]?)$', stem) if not m: return None size, tp, sen, rot, brk, cn = m.groups() if sen not in D_SENSOR_PIV and sen != '_': sen = 'U' # LAGP063 早期码用 U 表示传感器位 return {'series': 'LAGP', 'size': int(size), 'table_pos': '' if tp == '_' else tp, 'sensor': '' if sen == '_' else sen, 'rotation': int(rot), 'brake': brk, 'conn': cn or ''} def parse_lae(stem): """LAE""" m = re.match(r'^LAE(\d{3})([A-Z])([A-Z])(\d+)$', stem) if not m: return None size, pos, sty, ctl = m.groups() return {'series': 'LAE', 'size': int(size), 'pos': pos, 'style': sty, 'control': ctl} # ---------------------------------------------------------------- 系列配置 SPECS = [ dict(cat='PIN', series='ULA', parse=lambda s: parse_pin(s, 'A', D_ROD_ULA), rod=ROD_ULA, zh='ULA 气动可伸缩定位销(双杆可调)', en='ULA Pneumatic Retractable Locating Pin Unit, double orientable rod', kw='ula dingweixiao locating pin'), dict(cat='PIN', series='ULB', parse=lambda s: parse_pin(s, 'B', D_ROD_ULB), rod=ROD_ULB, zh='ULB 气动可伸缩定位销(后置传感器)', en='ULB Pneumatic Retractable Locating Pin Unit, rear sensor, single rod', kw='ulb dingweixiao locating pin'), dict(cat='PIN', series='ULL', parse=lambda s: parse_pin(s, 'L', D_ROD_ULP), rod=ROD_ULP, zh='ULL 手动可伸缩定位销(手杆)', en='ULL Manual Retractable Locating Pin Unit with Hand Lever', kw='ull dingweixiao shoudong manual'), dict(cat='PIN', series='ULM', parse=lambda s: parse_pin(s, 'M', D_ROD_ULP), rod=ROD_ULP, zh='ULM 带手杆气动可伸缩定位销', en='ULM Pneumatic Retractable Locating Pin Unit with Hand Lever', kw='ulm dingweixiao shougan lever'), dict(cat='PIN', series='ULO', parse=lambda s: parse_pin(s, 'O', D_ROD_ULA), rod=ROD_ULA, zh='ULO 气动可伸缩定位销(双杆带手杆)', en='ULO Pneumatic Retractable Locating Pin Unit, double rod with hand lever', kw='ulo dingweixiao double rod'), dict(cat='PIN', series='ULP', parse=lambda s: parse_pin(s, 'P', D_ROD_ULP), rod=ROD_ULP, zh='ULP 气动可伸缩定位销(单杆)', en='ULP Pneumatic Retractable Locating Pin Unit, single rod', kw='ulp dingweixiao locating pin'), dict(cat='PIN', series='ULR', parse=lambda s: parse_pin(s, 'R', D_ROD_ULP), rod=ROD_ULP, zh='ULR 紧凑型气动可伸缩定位销', en='ULR Compact Pneumatic Retractable Locating Pin Unit', kw='ulr dingweixiao compact'), dict(cat='PIN', series='ULT', parse=lambda s: parse_pin(s, 'T', D_ROD_ULT), rod=ROD_ULT, zh='ULT 肘节机构气动可伸缩定位销(不可逆)', en='ULT Irreversible Pneumatic Retractable Locating Pin Unit, double rod', kw='ult dingweixiao toggle'), dict(cat='PIN', series='ULX', parse=lambda s: parse_pin(s, 'X', D_ROD_ULT), rod=ROD_ULT, zh='ULX 肘节机构气动可伸缩定位销(带手杆)', en='ULX Irreversible Pneumatic Locating Pin Unit with Hand Lever', kw='ulx dingweixiao toggle lever'), dict(cat='PIN', series='ULY', parse=lambda s: parse_pin(s, 'Y', D_ROD_ULP), rod=ROD_ULP, zh='ULY 肘节机构气动可伸缩定位销(单杆带手杆)', en='ULY Irreversible Pneumatic Locating Pin Unit, single rod with hand lever', kw='uly dingweixiao toggle'), dict(cat='PIN', series='ULZ', parse=lambda s: parse_pin(s, 'Z', D_ROD_ULP), rod=ROD_ULP, zh='ULZ 肘节机构气动可伸缩定位销(单杆)', en='ULZ Irreversible Pneumatic Retractable Locating Pin Unit, single rod', kw='ulz dingweixiao toggle'), dict(cat='HOK', series='USP', parse=lambda s: parse_usp(s, 'USP'), rod=None, zh='USP 气动勾销缸', en='USP Pneumatic Pin Clamp', kw='usp gouxiao pin clamp'), dict(cat='HOK', series='USL', parse=lambda s: parse_usp(s, 'USL'), rod=None, zh='USL 手动勾销缸(手杆)', en='USL Manual Pin Clamp with Hand Lever', kw='usl gouxiao manual'), dict(cat='HOK', series='USR', parse=parse_usr, rod=None, zh='USR 紧凑型气动勾销缸', en='USR Pneumatic Compact Pin Clamp', kw='usr gouxiao compact'), dict(cat='HOK', series='USS', parse=lambda s: parse_uss(s, 'USS'), rod=None, zh='USS 带补偿气动勾销缸', en='USS Pneumatic Pin Clamp with Compensation', kw='uss gouxiao compensation'), dict(cat='HOK', series='USSL', parse=lambda s: parse_uss(s, 'USSL'), rod=None, zh='USSL 激光焊用气动勾销缸', en='USSL Pneumatic Pin Clamp for Laser Beam Applications', kw='ussl gouxiao laser'), dict(cat='ROT', series='UAGP', parse=parse_uagp, rod=None, zh='UAGP 气动翻转缸', en='UAGP Pneumatic Power Pivot', kw='uagp fanzhuan power pivot'), dict(cat='ROT', series='LAGP', parse=parse_lagp, rod=None, zh='LAGP 气动翻转缸(旧型号)', en='LAGP Pneumatic Power Pivot (Legacy)', kw='lagp fanzhuan power pivot legacy'), dict(cat='ELEC', series='LAE', parse=parse_lae, rod=None, zh='LAE 电动夹紧缸', en='LAE Electric Power Clamp', kw='lae diandong electric clamp'), ] # ---------------------------------------------------------------- 扫描 / 缓存 def do_scan(): found = collections.defaultdict(list) for d in ALL_DIRS: root = os.path.join(BASE, d) if not os.path.isdir(root): print(f' ! 目录不存在: {d}') continue n0 = len(found) for dirpath, _, files in os.walk(root): for f in files: if f.lower().endswith(('.stp', '.step')): found[f].append(os.path.join(dirpath, f).replace('\\', '/')) print(f' {d}: +{len(found)-n0} 文件') with open(CACHE, 'w', encoding='utf-8') as fp: json.dump(found, fp) print(f'缓存写入 {CACHE} 共 {len(found)} 个唯一文件名') def load_scan(): if not os.path.exists(CACHE): sys.exit(f'缺少扫描缓存,请先运行: --scan ({CACHE})') with open(CACHE, encoding='utf-8') as fp: return json.load(fp) # ---------------------------------------------------------------- 生成 def collect(spec, scanned): by_model, unresolved = {}, [] for fname, paths in scanned.items(): stem = norm(fname) if not stem.startswith(spec['series']): continue try: rec = spec['parse'](stem) except Exception as e: unresolved.append((stem, f'ERR {e}')) continue if rec is None or rec.get('series') != spec['series']: unresolved.append((stem, fname)) continue # 占位/测试文件(如 USP50J_0___K0 销径为空)不是真实型号,丢弃 if any(v is None for k, v in rec.items() if k not in ('series',)): unresolved.append((stem, fname + ' (关键参数为空, 丢弃)')) continue by_model.setdefault(stem, dict(rec, model=stem, src=paths[0])) return by_model, unresolved def build_meta(spec, records): order = ['size', 'stroke', 'rod', 'orient', 'mount', 'pos', 'style', 'pcmodel', 'hook_pos', 'hook', 'pin_dia', 'locating', 'prog', 'table_pos', 'rotation', 'brake', 'conn', 'control', 'sensor'] # 主键 model=完整型号,绝不能当维度 dims = [d for d in order if d != 'model' and any(d in r for r in records)] params = [] for d in dims: zh, en, kw, unit, typ = DIM_META[d] vals = sorted({r[d] for r in records if r.get(d) not in (None, '')}) if typ == 'number': params.append({"code": d, "nameZh": zh, "nameEn": en, "keywords": kw, "unit": unit, "type": "number", "values": vals, "display": {}}) continue has_empty = any(r.get(d, '') == '' for r in records) vlist = ([''] if has_empty else []) + [str(v) for v in vals] if d == 'sensor': dmap = SENSOR_PIV if spec['cat'] == 'ROT' else SENSOR_PIN if spec['series'] == 'LAGP': dmap = SENSOR_PIV_LEGACY elif d == 'rod': dmap = spec.get('rod') or ROD_ULP else: dmap = DIM_DISPLAY.get(d) or {} params.append({"code": d, "nameZh": zh, "nameEn": en, "keywords": kw, "unit": unit, "type": "enum", "values": vlist, "display": {k: dmap.get(k, k) for k in vlist}}) # 剔除非信息维度: 值域为空(无可选值), 或枚举维度只有 [空串] 一种取值。 # 保留"单一真值"维度(如单型号系列的 pcmodel=['J']) —— 否则参数全空导致 # CSV 表头出现空列名, 构建器报"CSV 行字段不足"。 params = [p for p in params if p['values'] and not (p['type'] == 'enum' and p['values'] == [''])] # 附件按 manual/<系列>/ 实际 png 数量生成 —— 不能硬编码 p01。 # (曾踩坑: 先跑 gen-pending-manual 生成多页, 再跑本脚本重写 meta 时把附件 # 覆盖回单页, 导致包内只有 1 张附图。此处扫目录自动对齐。) atts = [] mdir = os.path.join(DST, 'manual', spec['series']) if os.path.isdir(mdir): for f in sorted(os.listdir(mdir)): if f.lower().endswith('.png'): atts.append({"kind": "manual", "lang": "", "source": f"manual/{spec['series']}/{f}", "model": None}) for f in sorted(os.listdir(mdir)): if f.lower().endswith('.pdf'): atts.append({"kind": "datasheet", "lang": "", "source": f"manual/{spec['series']}/{f}", "model": None}) if not atts: atts = [ {"kind": "manual", "lang": "", "source": f"manual/{spec['series']}/p01.png", "model": None}, {"kind": "datasheet", "lang": "", "source": f"manual/{spec['series']}/{spec['series']}_manual.pdf", "model": None}, ] return { "series": { "code": spec['series'], "nameZh": spec['zh'], "nameEn": spec['en'], "keywords": spec['kw'], "parameters": params, "rules": [], "naming": {"stepNameTemplate": "{model}.step"}, "attachments": [], "variants": [] }, "attachments": atts } def main(): if DO_SCAN: do_scan() return scanned = load_scan() print(f'扫描缓存: {len(scanned)} 个唯一文件名') csv_dir = os.path.join(DST, 'csv') series_dir = os.path.join(DST, 'series') step_dir = os.path.join(DST, 'step') summary = collections.defaultdict(list) for spec in SPECS: if ONLY_CAT and spec['cat'] != ONLY_CAT: continue records, unresolved = collect(spec, scanned) recs = sorted(records.values(), key=lambda r: r['model']) print(f"\n{'='*74}\n## {spec['cat']} / {spec['series']} 型号 {len(recs)} 个") summary[spec['cat']].append((spec['series'], len(recs))) if not recs: print(' (X盘无数据 —— 保留原 demo)') continue print(' 例:', {k: v for k, v in recs[0].items() if k != 'src'}) if unresolved: print(f' 未解析 {len(unresolved)}:', [u[0] for u in unresolved[:8]]) meta = build_meta(spec, recs) for p in meta['series']['parameters']: print(f" {p['code']:10s} {p['type']:6s} {p['values']}") # 唯一性门禁: 参数组合必须唯一确定型号,否则选型会静默给错货 pcs = [p['code'] for p in meta['series']['parameters']] seen = {} for r in recs: seen.setdefault(tuple(str(r.get(d, '')) for d in pcs), []).append(r['model']) dup = {k: v for k, v in seen.items() if len(v) > 1} if dup: print(f' !! 参数组合冲突 {len(dup)} 组 —— 会被静默错配:') for k, v in list(dup.items())[:5]: print(' ', dict(zip(pcs, k)), '->', v) else: print(f' [OK] 参数组合唯一 ({len(seen)} 组合 / {len(recs)} 型号)') if DRY: continue dims = [p['code'] for p in meta['series']['parameters']] def cv(x): return '' if x is None else str(x) rows = ['model_code,' + ','.join(dims) + ',step_file'] for r in recs: rows.append(','.join([r['model']] + [cv(r.get(d, '')) for d in dims] + [r['model'] + '.step'])) with open(os.path.join(csv_dir, spec['series'] + '.csv'), 'w', encoding='utf-8-sig', newline='') as fp: fp.write('\r\n'.join(rows) + '\r\n') with open(os.path.join(series_dir, spec['series'] + '.meta.json'), 'w', encoding='utf-8') as fp: json.dump(meta, fp, ensure_ascii=False, indent=1) print(f" -> 写入 {spec['series']}.csv / .meta.json") if DO_COPY: n = 0 for r in recs: dst = os.path.join(step_dir, r['model'] + '.step') if not os.path.exists(dst): shutil.copy2(r['src'], dst) n += 1 print(f' -> 复制 STEP {n} 个') print(f"\n{'='*74}\n### 汇总") for cat, lst in summary.items(): tot = sum(n for _, n in lst) print(f' {cat:5s} 共 {tot:4d} 型号 ' + ', '.join(f'{s}:{n}' for s, n in lst)) if __name__ == '__main__': main()