UG/NX二次开发实战:用Python脚本批量修改零件参数

📅 发布时间:2026/9/20 20:50:07
UG/NX二次开发实战:用Python脚本批量修改零件参数
1. 批量改参数的场景到底长什么样如果你在模具厂、汽配厂或者任何用UG/NX做产品设计的团队待过大概率见过这种画面设计变更通知单一来几十上百个零件的某个尺寸要统一调整——可能是把某个孔径从8mm改成10mm可能是把一批零件的材料属性从45钢换成铝合金也可能是把图号前缀统一替换。然后就是漫长的重复劳动打开零件、找到表达式、改值、保存、关闭下一个。我见过最夸张的一次一个工程师花了整整两天改完137个零件的壁厚参数改完之后手都在抖。更麻烦的是这种纯手工操作几乎必然出错——改漏一个、改错一个、保存时覆盖了不该覆盖的版本这些问题在批量场景下会被放大。UG/NX二次开发配合Python脚本就是专门解决这类问题的。核心思路很简单用NXOpen这套官方API让脚本代替人去遍历零件文件、定位目标参数、执行修改、保存退出。整个过程不需要打开NX界面跑一个脚本就能批量处理。这篇文章适合三类人看一是完全没接触过NX二次开发但会一点Python的工程师二是用过NX录制宏但不知道怎么改成批量脚本的人三是想搞清楚NXOpen到底能干什么、值不值得投入时间学习的技术负责人。我会从环境搭建讲到完整脚本把每一步的坑都摊开说。2. 动手之前先把环境这件事搞明白2.1 NX版本和Python版本的对应关系这是新手最容易翻车的地方。NX的二次开发接口和Python版本是绑定的不是随便装个Python就能用。NX版本内置Python版本推荐做法NX 10Python 2.7用NX自带解释器别折腾NX 12Python 2.7同上注意编码问题NX 1847系列Python 3.6可以用较新的语法NX 1980Python 3.8支持f-string等现代特性很多人搜nxopen文件夹里没有python库就是因为版本没对上。NX安装目录下有个NXBIN文件夹里面会有python相关的dll和nxopen包。如果你在系统Python里import nxopen报错说明你用的是系统Python而不是NX内置的。提示不要试图把nxopen包拷贝到系统Python里用它依赖NX的运行时环境脱离NX进程跑不起来。2.2 两种运行方式的选择NX的Python脚本有两种跑法第一种是NX内部运行。在NX界面里通过工具→运行脚本或者开发→执行脚本来跑。这种方式的好处是能实时看到NX的反应调试方便坏处是必须开着NX界面批量处理时效率低。第二种是外部模式batch模式。用run_journal命令或者直接调用ugraf.exe带参数运行。这种方式不需要打开NX界面适合批量处理。命令大概长这样C:\Program Files\Siemens\NX 12.0\NXBIN\run_journal.exe batch_update.py -args D:\parts实测下来外部模式处理100个零件的速度比内部模式快3到5倍因为省去了界面渲染的开销。2.3 录制宏是学习API的最快路径如果你完全不知道某个操作对应的API是什么最笨也最有效的办法是打开NX的录制功能手动操作一遍然后看录出来的代码。录制出来的代码通常是这样的import math import NXOpen import NXOpen.Features def main(): theSession NXOpen.Session.GetSession() workPart theSession.Parts.Work # ... 录制生成的操作代码这段代码虽然啰嗦但它告诉你了一件事所有操作都从Session开始然后拿到Parts再拿到具体对象。这个层级关系是NXOpen的核心逻辑理解了它后面查API文档就顺了。3. 脚本的核心骨架从打开文件到保存退出3.1 遍历文件夹里的所有prt文件批量处理的第一步是找到所有目标文件。Python的os和glob模块在这里很好用import os import glob def find_all_prt(folder_path): 递归查找文件夹下所有.prt文件 prt_files [] for root, dirs, files in os.walk(folder_path): for f in files: if f.lower().endswith(.prt): prt_files.append(os.path.join(root, f)) return prt_files这里有个细节要注意NX的零件文件后缀是.prt但有些备份文件可能是.prt.bak或者.prt.1这种用endswith(.prt)能过滤掉大部分。如果你要处理的是装配体下面的组件还要考虑是否跳过某些标准件库的文件。我一般会加一个过滤逻辑把文件名里带标准件、GB、ISO这类关键词的排除掉避免误改。3.2 打开零件的正确姿势打开零件有两种方式对应不同的场景# 方式一只读打开适合只需要读取信息的场景 basePart, status theSession.Parts.OpenBaseDisplay(file_path) # 方式二可写打开适合需要修改的场景 basePart, status theSession.Parts.OpenActiveDisplay(file_path, NXOpen.DisplayPartOption.AllowAdditional)批量改参数必须用可写方式打开否则改完保存不了。打开之后要检查status如果是PartOpenStatus.PartOpenOk才继续否则说明文件被占用或者损坏。注意打开零件时如果该零件已经被其他进程占用会返回失败状态。批量处理前最好确认没有其他人正在编辑这些文件。3.3 定位表达式改参数的关键一步NX里的参数分两种一种是表达式Expression在工具→表达式里能看到另一种是特征参数比如拉伸的长度、孔的直径这些需要通过特征对象去访问。改表达式相对简单def update_expression(workPart, expr_name, new_value): 更新指定名称的表达式 expr workPart.Expressions.FindObject(expr_name) if expr is None: return False expr.SetRightHandSide(str(new_value)) return True改特征参数就复杂一些需要先找到特征再找到对应的参数def update_hole_diameter(workPart, feature_name, new_dia): 更新指定孔特征的直径 feature workPart.Features.FindObject(feature_name) if feature is None: return False # 孔特征的直径通常在第几个参数需要查API文档确认 # 不同版本的NX参数索引可能不同 builder workPart.Features.CreateHoleBuilder(feature) builder.Diameter.SetValue(new_dia) builder.Commit() builder.Destroy() return True这里有个经验能用表达式改的就别去改特征参数。因为表达式是设计意图的载体改表达式更符合设计逻辑而且不容易出错。如果参数没有对应的表达式再考虑改特征。3.4 保存和关闭的坑改完之后保存看起来简单实际上有好几个坑# 保存 workPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue) # 关闭 workPart.Close(NXOpen.BasePart.CloseModified.CloseModified, NXOpen.BasePart.CloseAfterSave.FalseValue)第一个坑SaveComponents参数。如果是装配体这个参数决定是否连带保存组件。批量处理单个零件时设成TrueValue没问题但如果你不想动组件要设成FalseValue。第二个坑关闭时的CloseModified参数。如果设成CloseModifiedNX会检查是否有未保存的修改如果设成CloseForce会强制关闭不保存。批量处理时建议用CloseModified避免误丢数据。第三个坑保存路径。如果零件是从只读目录打开的保存会失败。批量处理前要确认目标文件有写权限。4. 一个完整的批量改参数脚本长什么样4.1 脚本结构设计我把整个脚本分成四个模块配置区、工具函数、核心处理逻辑、主入口。这样结构清晰改起来也方便。# -*- coding: utf-8 -*- UG/NX批量参数更新脚本 用途批量修改指定文件夹下所有prt文件的表达式值 import os import sys import time import NXOpen # 配置区 TARGET_FOLDER rD:\parts_to_update EXPR_NAME thickness # 要修改的表达式名称 NEW_VALUE 5.0 # 新值 LOG_FILE rD:\update_log.txt SKIP_KEYWORDS [标准件, GB, ISO, backup] # 工具函数 def log(msg): 写日志同时输出到控制台和文件 timestamp time.strftime(%Y-%m-%d %H:%M:%S) line f[{timestamp}] {msg} print(line) with open(LOG_FILE, a, encodingutf-8) as f: f.write(line \n) def should_skip(file_path): 判断文件是否应该跳过 filename os.path.basename(file_path) for kw in SKIP_KEYWORDS: if kw in filename: return True return False def find_all_prt(folder): 查找所有prt文件 result [] for root, dirs, files in os.walk(folder): for f in files: if f.lower().endswith(.prt) and not should_skip(f): result.append(os.path.join(root, f)) return result # 核心处理逻辑 def process_one_part(session, file_path): 处理单个零件返回处理结果 result {file: file_path, status: unknown, detail: } try: # 打开零件 basePart, status session.Parts.OpenActiveDisplay( file_path, NXOpen.DisplayPartOption.AllowAdditional ) if status ! NXOpen.PartOpenStatus.PartOpenOk: result[status] open_failed result[detail] f打开失败状态码{status} return result workPart session.Parts.Work # 查找表达式 expr workPart.Expressions.FindObject(EXPR_NAME) if expr is None: result[status] expr_not_found result[detail] f未找到表达式 {EXPR_NAME} workPart.Close(NXOpen.BasePart.CloseModified.CloseModified, NXOpen.BasePart.CloseAfterSave.FalseValue) return result # 记录旧值 old_value expr.RightHandSide # 更新表达式 expr.SetRightHandSide(NEW_VALUE) # 保存 workPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue) # 关闭 workPart.Close(NXOpen.BasePart.CloseModified.CloseModified, NXOpen.BasePart.CloseAfterSave.FalseValue) result[status] success result[detail] f{old_value} - {NEW_VALUE} except Exception as e: result[status] error result[detail] str(e) return result # 主入口 def main(): log( * 60) log(批量参数更新开始) session NXOpen.Session.GetSession() files find_all_prt(TARGET_FOLDER) log(f共找到 {len(files)} 个待处理文件) stats {success: 0, expr_not_found: 0, open_failed: 0, error: 0} for i, f in enumerate(files, 1): log(f[{i}/{len(files)}] 处理{os.path.basename(f)}) result process_one_part(session, f) stats[result[status]] stats.get(result[status], 0) 1 log(f 结果{result[status]} - {result[detail]}) log(- * 60) log(f处理完成成功 {stats[success]} f表达式未找到 {stats[expr_not_found]} f打开失败 {stats[open_failed]} f异常 {stats[error]}) log( * 60) if __name__ __main__: main()4.2 这个脚本的几个设计考量为什么用日志而不是直接print批量处理100个文件控制台输出会刷得很快出了问题根本找不到。写日志文件可以事后追溯而且能记录时间戳。为什么每个文件单独try-except批量处理最怕的就是一个文件出错导致整个脚本崩掉。单独捕获异常让脚本能继续处理后面的文件最后统一看日志。为什么先记录旧值这是为了可追溯。万一改错了日志里能看到原来是什么值方便回滚。为什么用OpenActiveDisplay而不是OpenBaseDisplay因为要修改并保存必须用可写方式打开。OpenBaseDisplay是只读的改了也保存不了。4.3 实测中的性能数据我在一台配置一般的工作站上测过i7-970016G内存NX 12.0处理100个中等复杂度的零件每个大概5-20MB处理方式总耗时平均每个手动操作约4小时2.4分钟内部模式脚本约25分钟15秒外部batch模式约8分钟4.8秒外部batch模式快是因为省去了界面加载和渲染。但外部模式调试起来麻烦建议先用内部模式调通再切到外部模式跑批量。5. 那些文档里不会写的踩坑经验5.1 表达式名称找不到的三种原因脚本跑起来最常见的报错就是表达式未找到。排查下来通常是这三个原因原因一表达式名称大小写不一致。NX的表达式名称是区分大小写的Thickness和thickness是两个不同的表达式。脚本里写死的名称要和实际零件里的一致。原因二表达式在子装配里而不是零件里。如果处理的是装配体表达式可能定义在组件里需要先激活对应组件才能访问。原因三参数不是表达式而是特征参数。有些参数没有对应的表达式比如直接建模时输入的尺寸。这种情况需要改特征参数不能用表达式的方式处理。我的做法是先写一个探测脚本把每个零件里所有表达式的名称和值都打印出来确认目标参数到底叫什么、在哪个层级。5.2 文件被占用的处理批量处理时如果某个文件正被其他人打开OpenActiveDisplay会返回失败。这时候有两种处理策略一种是跳过并记录继续处理后面的文件最后统一处理失败的文件。另一种是等待重试设置一个超时时间每隔几秒重试一次。我一般用第一种因为批量处理通常是在非工作时间跑的有人占用的概率不大。如果确实需要等待可以加一个重试逻辑def open_with_retry(session, file_path, max_retry3, interval5): for i in range(max_retry): basePart, status session.Parts.OpenActiveDisplay( file_path, NXOpen.DisplayPartOption.AllowAdditional) if status NXOpen.PartOpenStatus.PartOpenOk: return basePart, status time.sleep(interval) return None, status5.3 保存时的版本兼容问题如果团队里有人用NX 12有人用NX 1847批量处理时要注意版本兼容。高版本NX保存的文件低版本打不开。批量处理前要确认所有目标文件的版本一致或者统一用最低版本保存。另外NX有个保存时保留历史版本的设置批量处理时如果开着这个会在同目录下生成一堆.prt.1、.prt.2的备份文件。处理前最好确认这个设置避免磁盘被撑爆。5.4 编码问题Python 2.7处理中文路径时经常出问题。如果文件路径里有中文os.walk返回的可能是乱码。解决办法是在脚本开头加编码声明并且用unicode处理路径# -*- coding: utf-8 -*- import sys reload(sys) sys.setdefaultencoding(utf-8)Python 3就没这个问题但要注意NX内置的Python版本。NX 12内置的是Python 2.7所以上面这段代码在NX 12里是需要的。6. 从改表达式到改特征参数的进阶6.1 什么时候必须改特征参数表达式能覆盖大部分参数化设计的场景但有些情况必须直接操作特征参数没有对应的表达式是建模时直接输入的需要修改特征的拓扑结构比如把通孔改成盲孔需要批量修改特征的抑制状态改特征参数的核心是Builder模式。NXOpen里几乎所有的特征修改都是通过Builder来完成的创建Builder、设置参数、Commit提交、Destroy销毁。6.2 一个改孔直径的完整例子def update_hole_feature(workPart, feature_name, new_diameter): 修改指定孔特征的直径 feature workPart.Features.FindObject(feature_name) if feature is None: return False, 特征未找到 try: # 创建孔特征的Builder builder workPart.Features.CreateHoleBuilder(feature) # 设置直径 builder.Diameter.SetValue(new_diameter) # 提交修改 builder.Commit() # 销毁Builder释放资源 builder.Destroy() return True, 修改成功 except Exception as e: return False, str(e)这里的关键是CreateHoleBuilder这个方法名。不同特征对应不同的Builder比如拉伸是CreateExtrudeBuilder倒角是CreateChamferBuilder。具体用哪个查NXOpen的API文档或者录制宏都能找到。6.3 Builder模式的注意事项Builder用完之后必须Destroy否则会占用内存批量处理时可能导致NX崩溃。我一般用try-finally确保Destroy一定被执行builder workPart.Features.CreateHoleBuilder(feature) try: builder.Diameter.SetValue(new_diameter) builder.Commit() finally: builder.Destroy()另外Builder的Commit操作会触发模型重新计算。如果零件很复杂这个计算可能很耗时。批量处理时要有心理准备不是所有零件都能秒改。7. 怎么确认改对了验证与回滚7.1 改完之后的验证策略批量处理最怕的就是改错了还不知道。我的做法是分三层验证第一层脚本内验证。改完之后立即读回参数值确认和预期一致expr.SetRightHandSide(NEW_VALUE) actual_value expr.RightHandSide if actual_value ! NEW_VALUE: log(f警告设置值 {NEW_VALUE}实际值 {actual_value})第二层抽样人工检查。批量处理完后随机抽5-10个零件用NX打开确认参数确实改了、模型没有异常。第三层对比报告。脚本生成一份详细的处理报告列出每个文件的旧值、新值、处理状态。这份报告可以作为变更记录存档。7.2 回滚方案万一改错了要回滚有几种方案如果处理前做了备份直接从备份恢复。这是最稳妥的但需要额外的磁盘空间。如果没有备份但日志里记录了旧值可以写一个反向脚本把新值改回旧值。这要求日志足够详细。如果连日志都没有那就只能靠NX的版本历史或者团队的版本管理工具了。所以处理前备份和处理日志这两件事再强调也不为过。7.3 一个实用的备份脚本import shutil def backup_folder(src_folder, backup_folder): 备份整个文件夹 if os.path.exists(backup_folder): shutil.rmtree(backup_folder) shutil.copytree(src_folder, backup_folder) log(f备份完成{src_folder} - {backup_folder})这个备份脚本很简单但很实用。处理前跑一遍心里踏实。8. 批量处理之外这套思路还能干什么8.1 批量导出图纸同样的遍历逻辑把改表达式换成导出PDF或导出DXF就能实现批量出图。NXOpen里有对应的导出API核心逻辑和批量改参数一模一样。8.2 批量检查模型质量可以写脚本遍历所有零件检查一些常见的质量问题有没有未倒角的锐边、有没有厚度小于某个值的薄壁、有没有未使用的草图。这种自动化检查比人工翻模型效率高得多。8.3 批量更新属性零件的属性材料、图号、版本号也可以批量更新。这在产品数据管理PDM系统切换或者属性规范变更时特别有用。8.4 和外部系统对接如果团队有PDM或者ERP系统可以写脚本从系统里读取参数变更指令自动更新NX模型。这就把二次开发从批量处理工具升级成了系统集成方案。9. 给准备入坑的人几句实在话NX二次开发的学习曲线不算平缓但投入产出比很高。我的建议是从解决一个具体的重复劳动问题开始不要一上来就想着做通用工具。先写一个能跑通的小脚本哪怕只处理一个文件跑通了再扩展。API文档是英文的看起来费劲但录制宏能解决大部分问题。遇到不知道怎么写的地方录一遍宏看NX自己生成的代码比翻文档快得多。还有一点脚本写完一定要在测试环境跑。我见过有人直接在生产环境的零件上跑脚本结果改错了一批文件加班三天才恢复。备份、日志、抽样验证这三件事一个都不能省。最后Python只是NX二次开发的选项之一。C和C#也能做而且性能更好。但Python的优势是上手快、改起来方便对于批量处理这种场景Python的效率已经足够了。除非你要做的是实时交互的复杂工具否则没必要上C。这套批量改参数的脚本我从第一次写到现在迭代了七八个版本踩过的坑基本都在这篇文章里了。如果你照着跑遇到问题大概率是版本差异或者路径问题对着日志排查基本都能解决。