Files
OnebotCatalog/onebot-data/tools/gen-clamp-series.py
wangruiguo 0584ba67b4 v1.0: 夹紧缸模块真实数据替换 + 仓库瘦身
数据 (CLM 气动夹紧缸分类):
- 1 系列 demo -> 11 个官方系列, 6 型号 -> 859 个真实型号
- 参数化升级: 仅缸径 -> 最多 5 维 (缸径/臂位置/臂类型/臂样式/传感器)
- 11 系列各含官方图册附图 (84 张) + datasheet PDF
- 修复: 枚举维度空串必须为合法值 (否则级联过滤后下拉全空)
- 修复: catalog.json 分类 series 数组未更新导致新系列不进包
- 修复: _step 副本后缀归一化错误 (产生假型号)

工具:
- gen-clamp-series.py     数据清洗 (X盘数模 -> CSV/meta/STEP)
- copy-clamp-step.py      8线程并行复制 (网络盘 I/O 优化)
- gen-clamp-manual.py     官方图册 PDF -> 附图
- update-clamp-meta-attach.py / fix-clamp-meta-empty-enum.py

仓库瘦身:
- .gitignore 排除数模(5GB)/.opc(1.2GB)/编译产物/发布产物
- 移出索引: release/ bin产物 STEP数模 .opc 大型PDF 等
- 提交体积 17MB (原 15.97GB)

其他: SelfTest.cs 可解析选型逻辑放宽 (稀疏数据用真实变体回验)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-14 15:24:16 +08:00

249 lines
12 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 -*-
"""
夹紧缸数据清洗脚本 v2(Univer 真实数模 -> OnebotCatalog 目录数据)
最终方案:11 个系列 + 5 维参数(缸径/臂位置/臂类型/臂样式/传感器)
用法:
PYTHONUTF8=1 python gen-clamp-series.py --dry-run # 只分析打印,不写文件
PYTHONUTF8=1 python gen-clamp-series.py # 正式生成
"""
import os, re, sys, shutil, collections, json
SRC = r'X:/新开发产品-Research and Development/1_产品3D数模(发客户仿真)/1-夹紧缸'
DST = r'D:/开发/OnebotCatalog/onebot-data/catalog'
BORES = ['25缸', '32缸', '40缸', '50缸', '63缸', '80缸']
DRY = '--dry-run' in sys.argv
LEGAL_BORES = [80, 63, 50, 40, 32, 25]
# 系列前缀映射(旧码 -> 官方 UC 前缀码;UFL/UHL 单独系列)
PREFIX_MAP = [
('UCBP', 'UCBP'), ('UBP', 'UCBP'),
('UCBM', 'UCBM'), ('UBM', 'UCBM'),
('UCBT', 'UCBT'), ('UBT', 'UCBT'),
('UCBQ', 'UCBQ'), ('UBQ', 'UCBQ'),
('UCBH', 'UCBH'), ('UBH', 'UCBH'),
('UCDP', 'UCDP'), ('UDP', 'UCDP'),
('UCIM', 'UCIM'), ('IBM', 'UCIM'),
('UCIP', 'UCIP'), ('IBP', 'UCIP'),
('UCNM', 'UCNM'), ('UNM', 'UCNM'),
('UCNP', 'UCNP'), ('UNP', 'UCNP'),
('UFL', 'UFL'), ('UHL', 'UFL'),
]
SERIES_NAME = {
'UCBP': ('UCBP 标准气动夹紧缸', 'UCBP Standard Pneumatic Power Clamp', 'ucbp jiajin clamping standard'),
'UCBM': ('UCBM 带手杆气动夹紧缸', 'UCBM Pneumatic Power Clamp with Hand Lever', 'ucbm jiajin lever'),
'UCBT': ('UCBT 带中央手杆气动夹紧缸', 'UCBT Pneumatic Power Clamp with Central Hand Lever', 'ucbt jiajin central lever'),
'UCBQ': ('UCBQ 带手杆补偿夹紧缸', 'UCBQ Power Clamp with Hand Lever & Compensation', 'ucbq jiajin compensation'),
'UCBH': ('UCBH 夹紧缸 Ø63/Ø22', 'UCBH Power Clamp Ø63/Ø22', 'ucbh jiajin'),
'UCDP': ('UCDP 双夹臂气动夹紧缸', 'UCDP Double Arm Pneumatic Power Clamp', 'ucdp jiajin double arm'),
'UCIM': ('UCIM 可互换带手杆夹紧缸', 'UCIM Interchangeable Power Clamp with Hand Lever', 'ucim jiajin interchangeable'),
'UCIP': ('UCIP 可互换气动夹紧缸', 'UCIP Interchangeable Pneumatic Power Clamp', 'ucip jiajin interchangeable'),
'UCNM': ('UCNM NAAMS 带手杆夹紧缸', 'UCNM NAAMS Power Clamp with Hand Lever', 'ucnm naams jiajin'),
'UCNP': ('UCNP NAAMS 气动夹紧缸', 'UCNP NAAMS Pneumatic Power Clamp', 'ucnp naams jiajin'),
'UFL': ('UFL/UHL 停产气动夹紧缸', 'UFL/UHL Legacy Pneumatic Power Clamp (Discontinued)', 'ufl uhl jiajin legacy'),
}
POS_DISPLAY = {'V': '90°', 'O': '180°', 'N': '无臂', '': '不适用'}
TYPE_DISPLAY = {'C': '中央 15mm', 'D': '右 15mm', 'S': '左 15mm', 'E': '中央 45mm',
'F': '右 45mm', 'L': '左 45mm', 'N': '无臂', '': '不适用'}
STYLE_DISPLAY = {'A': '铝臂', 'S': '钢臂', 'N': '无臂', '': '不适用'}
SENSOR_DISPLAY = {'K': '电子 PNP M12', 'N': '无传感器', 'Y': '电子 PNP 白LED', 'D': '气动传感器', '': '不适用'}
# 杂音标记(这些出现说明文件是测试/副本,归一化时优先丢弃)
NOISE = re.compile(r'20\d{12,14}|PROVA|_ALLCATPART|ALLCATPART|-\d{2}-\d{2}-\d{4}')
def _extract_bore(s):
for b in LEGAL_BORES:
bs = str(b)
if s.startswith(bs):
return b, s[len(bs):]
return None
def normalize(name):
"""归一化 -> (model_code, series, bore, pos, type, style, sensor) 或 None"""
raw = os.path.splitext(name)[0]
# 排除含非 ASCII(中文夹臂/大孔等)
if not all(ord(c) < 128 for c in raw):
return None
base = raw.upper()
# 先剥离 "_step"/"_1" 副本后缀(必须在去下划线之前,否则 _ 已被抹掉匹配不到)
base = re.sub(r'[_\s]*_?STEP$', '', base) # UCBM40OESK0_step -> UCBM40OESK0
base = re.sub(r'_1$', '', base)
base = base.replace(' ', '').replace('_', '')
# 记录是否含杂音(用于去重优先级)
has_noise = bool(NOISE.search(base))
base = NOISE.sub('', base)
base = base.replace('-S', '\x01') # 钢臂标记占位(保护 -S 不被后续清理)
base = base.replace('-', '') # 去连字符
base = base.replace('\x01', '-S') # 恢复 -S
if not base:
return None
# 系列识别
series = rest = None
for pfx, code in PREFIX_MAP:
if base.startswith(pfx):
series, rest = code, base[len(pfx):]
break
if series is None or rest == '':
return None
# UFL/UHL 特殊缸径:UFL450->50, UHL463->63
if series == 'UFL':
if rest.startswith('450'):
bore, tail = 50, rest[3:]
elif rest.startswith('463'):
bore, tail = 63, rest[3:]
else:
return None
else:
r = _extract_bore(rest)
if r is None:
return None
bore, tail = r
# 解析 pos/type/style/sensor
pos = type_ = style = sensor = ''
i = 0
if i < len(tail) and tail[i] in 'VON':
pos, i = tail[i], i + 1
if i < len(tail) and tail[i] in 'CDSEFLN':
type_, i = tail[i], i + 1
if i < len(tail) and tail[i] in 'ASN':
style, i = tail[i], i + 1
if i < len(tail) and tail[i] in 'KNYD':
sensor, i = tail[i], i + 1
# 旧命名 -S 后缀 => 钢臂
if tail.endswith('-S') and style == '':
style = 'S'
return (base, series, bore, pos, type_, style, sensor, has_noise)
def collect():
search_roots = [os.path.join(SRC, b) for b in BORES] + [SRC]
found = collections.defaultdict(list)
for root in search_roots:
for dirpath, dirs, files in os.walk(root):
for f in files:
if f.lower().endswith(('.stp', '.step')):
found[f].append(os.path.join(dirpath, f))
# 候选:model -> 最佳源文件
best = {}
for fname, paths in found.items():
r = normalize(fname)
if r is None:
continue
model, series, bore, pos, type_, style, sensor, has_noise = r
cur = best.get(model)
# 优先级:干净名 > 杂音名;同优先级取 mtime 新
better = (cur is None
or (cur['has_noise'] and not has_noise)
or (cur['has_noise'] == has_noise
and os.path.getmtime(paths[0]) > os.path.getmtime(cur['src'])))
if better:
best[model] = dict(model=model, series=series, bore=bore, pos=pos,
type=type_, style=style, sensor=sensor,
src=paths[0], has_noise=has_noise)
return best
def main():
models = collect()
by_series = collections.defaultdict(list)
for m in models.values():
by_series[m['series']].append(m)
print('=== 归一化去重后型号总数:', len(models), '===')
for series in sorted(by_series, key=lambda s: -len(by_series[s])):
ms = by_series[series]
bs = sorted(set(m['bore'] for m in ms))
print(f'{series:6s} 型号 {len(ms):4d} 缸径 {bs}')
if DRY:
print()
for series in sorted(by_series):
ms = sorted(by_series[series], key=lambda m: (m['bore'], m['model']))
print(f'\n## {series} ({len(ms)} 个)')
for m in ms:
code = f"pos={m['pos'] or '-'} type={m['type'] or '-'} style={m['style'] or '-'} sensor={m['sensor'] or '-'}"
print(f" {m['model']:30s} bore={m['bore']:<3} {code}")
return
# 1) CSV
csv_dir = os.path.join(DST, 'csv')
for series, ms in by_series.items():
ms = sorted(ms, key=lambda m: (m['bore'], m['model']))
rows = ['model_code,bore,pos,type,style,sensor,step_file']
for m in ms:
rows.append(f"{m['model']},{m['bore']},{m['pos']},{m['type']},{m['style']},{m['sensor']},{m['model']}.step")
with open(os.path.join(csv_dir, series + '.csv'), 'w', encoding='utf-8-sig', newline='') as fp:
fp.write('\r\n'.join(rows) + '\r\n')
print(f'写 CSV: {series}.csv ({len(ms)} 行)')
# 2) meta.json
series_dir = os.path.join(DST, 'series')
for series, ms in by_series.items():
bores = sorted(set(m['bore'] for m in ms))
# 枚举维度: 空串 '' 是合法值(表示该维度"无/未指定"), 必须保留在 values 里
# (参考 KC 的 magnet values=['', 'M']) —— 否则 DomainValid 会把 "" 判非法,
# AllowedOptions 过滤后下拉全空。仅当除空串外无真值时, 才剔除该维度。
poss = [''] + sorted({m['pos'] for m in ms if m['pos']})
types = [''] + sorted({m['type'] for m in ms if m['type']})
styles = [''] + sorted({m['style'] for m in ms if m['style']})
sensors = [''] + sorted({m['sensor'] for m in ms if m['sensor']})
poss = poss if len(poss) > 1 else []
types = types if len(types) > 1 else []
styles = styles if len(styles) > 1 else []
sensors = sensors if len(sensors) > 1 else []
name_zh, name_en, kw = SERIES_NAME[series]
params = [
{"code": "bore", "nameZh": "缸径", "nameEn": "Bore", "keywords": "bore gangjing", "unit": "mm", "type": "number", "values": bores, "display": {}},
{"code": "pos", "nameZh": "臂位置", "nameEn": "Arm Position", "keywords": "pos arm position weizhi", "unit": "", "type": "enum", "values": poss, "display": {k: POS_DISPLAY.get(k, k) for k in poss}},
{"code": "type", "nameZh": "臂类型", "nameEn": "Arm Type", "keywords": "type arm leixing", "unit": "", "type": "enum", "values": types, "display": {k: TYPE_DISPLAY.get(k, k) for k in types}},
{"code": "style", "nameZh": "臂样式", "nameEn": "Arm Style", "keywords": "style arm yangshi", "unit": "", "type": "enum", "values": styles, "display": {k: STYLE_DISPLAY.get(k, k) for k in styles}},
{"code": "sensor", "nameZh": "传感器", "nameEn": "Sensor", "keywords": "sensor chuanganqi", "unit": "", "type": "enum", "values": sensors, "display": {k: SENSOR_DISPLAY.get(k, k) for k in sensors}},
]
# 值域为空的参数维度直接剔除(避免 UI 出现无值下拉;该维度在本系列数据里不存在)
params = [p for p in params if p['values']]
meta = {
"series": {
"code": series, "nameZh": name_zh, "nameEn": name_en, "keywords": kw,
"parameters": params,
"rules": [], "naming": {"stepNameTemplate": "{model}.step"},
"attachments": [], "variants": []
},
"attachments": [
{"kind": "manual", "lang": "", "source": f"manual/{series}/p01.png", "model": None},
{"kind": "datasheet", "lang": "", "source": f"manual/{series}/{series}_manual.pdf", "model": None},
]
}
with open(os.path.join(series_dir, series + '.meta.json'), 'w', encoding='utf-8') as fp:
json.dump(meta, fp, ensure_ascii=False, indent=1)
print(f'写 meta: {series}.meta.json bore={bores} pos={poss} type={types} style={styles} sensor={sensors}')
# 3) 复制 STEP
step_dir = os.path.join(DST, 'step')
copied = 0
for m in models.values():
dst_file = os.path.join(step_dir, m['model'] + '.step')
if not os.path.exists(dst_file):
shutil.copy2(m['src'], dst_file)
copied += 1
print(f'复制 STEP: {copied} 个(共 {len(models)} 型号)')
# 4) 更新 catalog.json:CLM 分类 series 数组
cat_path = os.path.join(DST, 'catalog.json')
with open(cat_path, 'r', encoding='utf-8-sig') as fp:
cat = json.load(fp)
series_order = ['UCBP', 'UCBM', 'UCBT', 'UCBQ', 'UCBH', 'UCDP', 'UCIM', 'UCIP', 'UCNM', 'UCNP', 'UFL']
for c in cat['categories']:
if c['code'] == 'CLM':
c['series'] = series_order
with open(cat_path, 'w', encoding='utf-8-sig') as fp:
json.dump(cat, fp, ensure_ascii=False, indent=1)
print('更新 catalog.json: CLM.series =', series_order)
if __name__ == '__main__':
main()